diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c2e49b783..1000fca05 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -5,4 +5,4 @@ * @liujuping @JackLian /modules/material-parser @akirakai -/modules/code-generator @leoyuan +/modules/code-generator @qingniaotonghua diff --git a/.github/workflows/publish docs.yml b/.github/workflows/publish docs.yml index e85d08f2a..139b70239 100644 --- a/.github/workflows/publish docs.yml +++ b/.github/workflows/publish docs.yml @@ -33,7 +33,7 @@ jobs: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Get version id: get_version - run: echo "::set-output name=version::$(node -p "require('./docs/package.json').version")" + run: echo "version=$(node -p "require('./docs/package.json').version")" >> $GITHUB_OUTPUT comment-pr: needs: publish-docs diff --git a/.github/workflows/publish engine beta.yml b/.github/workflows/publish engine beta.yml index 32c5b4c15..ed4c37475 100644 --- a/.github/workflows/publish engine beta.yml +++ b/.github/workflows/publish engine beta.yml @@ -27,4 +27,4 @@ jobs: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Get version id: get_version - run: echo "::set-output name=version::$(node -p "require('./package.json').version")" + run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT diff --git a/.github/workflows/publish engine.yml b/.github/workflows/publish engine.yml index ab63eec52..dcbf0547e 100644 --- a/.github/workflows/publish engine.yml +++ b/.github/workflows/publish engine.yml @@ -30,4 +30,4 @@ jobs: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Get version id: get_version - run: echo "::set-output name=version::$(node -p "require('./package.json').version")" + run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT diff --git a/.gitignore b/.gitignore index 05159f047..6a19ae3e0 100644 --- a/.gitignore +++ b/.gitignore @@ -108,3 +108,5 @@ typings/ # codealike codealike.json .node + +.must.config.js \ No newline at end of file diff --git a/docs/docs/api/common.md b/docs/docs/api/common.md index 966e2277f..6613547ea 100644 --- a/docs/docs/api/common.md +++ b/docs/docs/api/common.md @@ -145,6 +145,22 @@ const { intl, getLocale, setLocale } = common.utils.createIntl({ ``` +#### intl + +i18n 转换方法 + +```typescript +/** + * i18n 转换方法 + */ +intl(data: IPublicTypeI18nData | string, params?: object): string; +``` + +##### 示例 +``` +const title = common.utils.intl(node.title) +``` + ### skeletonCabin #### Workbench 编辑器框架 View diff --git a/docs/docs/api/commonUI.md b/docs/docs/api/commonUI.md new file mode 100644 index 000000000..c0bbda588 --- /dev/null +++ b/docs/docs/api/commonUI.md @@ -0,0 +1,210 @@ +--- +title: commonUI - UI 组件库 +sidebar_position: 11 +--- + +## 简介 +CommonUI API 是一个专为低代码引擎设计的组件 UI 库,使用它开发的插件,可以保证在不同项目和主题切换中能够保持一致性和兼容性。 + +## 组件列表 + +### Tip + +提示组件 + +| 参数 | 说明 | 类型 | 默认值 | +|-----------|--------------|---------------------------------------|--------| +| className | className | string (optional) | | +| children | tip 的内容 | IPublicTypeI18nData \| ReactNode | | +| direction | tip 的方向 | 'top' \| 'bottom' \| 'left' \| 'right' | | + + +### HelpTip + +带 help icon 的提示组件 + +| 参数 | 说明 | 类型 | 默认值 | +|-----------|--------|-----------------------------------|--------| +| help | 描述 | IPublicTypeHelpTipConfig | | +| direction | 方向 | IPublicTypeTipConfig['direction'] | 'top' | +| size | 方向 | IconProps['size'] | 'small'| + +### Title + +标题组件 + +| 参数 | 说明 | 类型 | 默认值 | +|-----------|------------|-----------------------------|--------| +| title | 标题内容 | IPublicTypeTitleContent | | +| className | className | string (optional) | | +| onClick | 点击事件 | () => void (optional) | | + +### ContextMenu + +| 参数 | 说明 | 类型 | 默认值 | +|--------|----------------------------------------------------|------------------------------------|--------| +| menus | 定义上下文菜单的动作数组 | IPublicTypeContextMenuAction[] | | +| children | 组件的子元素 | React.ReactElement[] | | + +**IPublicTypeContextMenuAction Interface** + +| 参数 | 说明 | 类型 | 默认值 | +|------------|--------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------|----------------------------------------| +| name | 动作的唯一标识符
Unique identifier for the action | string | | +| title | 显示的标题,可以是字符串或国际化数据
Display title, can be a string or internationalized data | string \| IPublicTypeI18nData (optional) | | +| type | 菜单项类型
Menu item type | IPublicEnumContextMenuType (optional) | IPublicEnumContextMenuType.MENU_ITEM | +| action | 点击时执行的动作,可选
Action to execute on click, optional | (nodes: IPublicModelNode[]) => void (optional) | | +| items | 子菜单项或生成子节点的函数,可选,仅支持两级
Sub-menu items or function to generate child node, optional | Omit[] \| ((nodes: IPublicModelNode[]) => Omit[]) (optional) | | +| condition | 显示条件函数
Function to determine display condition | (nodes: IPublicModelNode[]) => boolean (optional) | | +| disabled | 禁用条件函数,可选
Function to determine disabled condition, optional | (nodes: IPublicModelNode[]) => boolean (optional) | | + +**ContextMenu 示例** + +```typescript +const App = () => { + const menuItems: IPublicTypeContextMenuAction[] = [ + { + name: 'a', + title: '选项 1', + action: () => console.log('选项 1 被点击'), + }, + { + name: 'b', + title: '选项 2', + action: () => console.log('选项 2 被点击'), + }, + ]; + + const ContextMenu = ctx.commonUI.ContextMenu; + + return ( +
+ +
右键点击这里
+
+
+ ); +}; + +export default App; +``` + +**ContextMenu.create 示例** + +```typescript +const App = () => { + const menuItems: IPublicTypeContextMenuAction[] = [ + { + name: 'a', + title: '选项 1', + action: () => console.log('选项 1 被点击'), + }, + { + name: 'b', + title: '选项 2', + action: () => console.log('选项 2 被点击'), + }, + ]; + + const ContextMenu = ctx.commonUI.ContextMenu; + + return ( +
+
{ + ContextMenu.create(menuItems, e); + }}>点击这里
+
+ ); +}; + +export default App; +``` + +### Balloon + +详细文档: [Balloon Documentation](https://fusion.design/pc/component/balloon) + +### Breadcrumb +详细文档: [Breadcrumb Documentation](https://fusion.design/pc/component/breadcrumb) + +### Button +详细文档: [Button Documentation](https://fusion.design/pc/component/button) + +### Card +详细文档:[Card Documentation](https://fusion.design/pc/component/card) + +### Checkbox +详细文档:[Checkbox Documentation](https://fusion.design/pc/component/checkbox) + +### DatePicker +详细文档:[DatePicker Documentation](https://fusion.design/pc/component/datepicker) + +### Dialog +详细文档:[Dialog Documentation](https://fusion.design/pc/component/dialog) + +### Dropdown +详细文档:[Dropdown Documentation](https://fusion.design/pc/component/dropdown) + +### Form +详细文档:[Form Documentation](https://fusion.design/pc/component/form) + +### Icon +详细文档:[Icon Documentation](https://fusion.design/pc/component/icon) + +引擎默认主题支持的 icon 列表:https://fusion.design/64063/component/icon?themeid=20133 + + +### Input +详细文档:[Input Documentation](https://fusion.design/pc/component/input) + +### Loading +详细文档:[Loading Documentation](https://fusion.design/pc/component/loading) + +### Message +详细文档:[Message Documentation](https://fusion.design/pc/component/message) + +### Overlay +详细文档:[Overlay Documentation](https://fusion.design/pc/component/overlay) + +### Pagination +详细文档:[Pagination Documentation](https://fusion.design/pc/component/pagination) + +### Radio +详细文档:[Radio Documentation](https://fusion.design/pc/component/radio) + +### Search +详细文档:[Search Documentation](https://fusion.design/pc/component/search) + +### Select +详细文档:[Select Documentation](https://fusion.design/pc/component/select) + +### SplitButton +详细文档:[SplitButton Documentation](https://fusion.design/pc/component/splitbutton) + +### Step +详细文档:[Step Documentation](https://fusion.design/pc/component/step) + +### Switch +详细文档:[Switch Documentation](https://fusion.design/pc/component/switch) + +### Tab +详细文档:[Tab Documentation](https://fusion.design/pc/component/tab) + +### Table +详细文档:[Table Documentation](https://fusion.design/pc/component/table) + +### Tree +详细文档:[Tree Documentation](https://fusion.design/pc/component/tree) + +### TreeSelect +详细文档:[TreeSelect Documentation](https://fusion.design/pc/component/treeselect) + +### Upload +详细文档:[Upload Documentation](https://fusion.design/pc/component/upload) + +### Divider +详细文档:[Divider Documentation](https://fusion.design/pc/component/divider) + +## 说明 + +如果需要其他组件,可以提 issue 给我们。 diff --git a/docs/docs/api/configOptions.md b/docs/docs/api/configOptions.md index aaea28261..67d2fae2c 100644 --- a/docs/docs/api/configOptions.md +++ b/docs/docs/api/configOptions.md @@ -185,6 +185,12 @@ config.set('enableCondition', false) `@type {boolean}` `@default {false}` +#### enableContextMenu - 开启右键菜单 + +`@type {boolean}` `@default {false}` + +是否开启右键菜单 + #### disableDetecting `@type {boolean}` `@default {false}` @@ -216,6 +222,12 @@ config.set('enableCondition', false) 是否在只有一个 item 的时候隐藏设置 tabs +#### hideComponentAction + +`@type {boolean}` `@default {false}` + +隐藏设计器辅助层 + #### thisRequiredInJSE `@type {boolean}` `@default {true}` diff --git a/docs/docs/api/material.md b/docs/docs/api/material.md index 51ed4e305..d237180ca 100644 --- a/docs/docs/api/material.md +++ b/docs/docs/api/material.md @@ -237,7 +237,90 @@ material.modifyBuiltinComponentAction('remove', (action) => { }); ``` +### 右键菜单项 +#### addContextMenuOption + +添加右键菜单项 + +```typescript +/** + * 添加右键菜单项 + * @param action + */ +addContextMenuOption(action: IPublicTypeContextMenuAction): void; +``` + +示例 + +```typescript +import { IPublicEnumContextMenuType } from '@alilc/lowcode-types'; + +material.addContextMenuOption({ + name: 'parentItem', + title: 'Parent Item', + condition: (nodes) => true, + items: [ + { + name: 'childItem1', + title: 'Child Item 1', + action: (nodes) => console.log('Child Item 1 clicked', nodes), + condition: (nodes) => true + }, + // 分割线 + { + type: IPublicEnumContextMenuType.SEPARATOR + name: 'separator.1' + } + // 更多子菜单项... + ] +}); + +``` + +#### removeContextMenuOption + +删除特定右键菜单项 + +```typescript +/** + * 删除特定右键菜单项 + * @param name + */ +removeContextMenuOption(name: string): void; +``` + +#### adjustContextMenuLayout + +调整右键菜单项布局,每次调用都会覆盖之前注册的调整函数,只有最后注册的函数会被应用。 + +```typescript +/** + * 调整右键菜单项布局 + * @param actions + */ +adjustContextMenuLayout(fn: (actions: IPublicTypeContextMenuItem[]) => IPublicTypeContextMenuItem[]): void; +``` + +**示例** + +通过 adjustContextMenuLayout 补充分割线 + +```typescript +material.adjustContextMenuLayout((actions: IPublicTypeContextMenuAction) => { + const names = ['a', 'b']; + const newActions = []; + actions.forEach(d => { + newActions.push(d); + if (names.include(d.name)) { + newActions.push({ type: 'separator' }) + } + }); + return newActions +}) +``` + ### 物料元数据 + #### getComponentMeta 获取指定名称的物料元数据 diff --git a/docs/docs/guide/appendix/npms.md b/docs/docs/guide/appendix/npms.md index 93980f32c..b0292c273 100644 --- a/docs/docs/guide/appendix/npms.md +++ b/docs/docs/guide/appendix/npms.md @@ -12,8 +12,6 @@ sidebar_position: 3 | @alilc/lowcode-engine | [https://github.com/alibaba/lowcode-engine](https://github.com/alibaba/lowcode-engine) | packages/engine | | @alilc/lowcode-plugin-designer | [https://github.com/alibaba/lowcode-engine](https://github.com/alibaba/lowcode-engine) | packages/plugin-designer | | @alilc/lowcode-plugin-outline-pane | [https://github.com/alibaba/lowcode-engine](https://github.com/alibaba/lowcode-engine) | packages/plugin-outline-pane | -| @alilc/lowcode-rax-renderer | [https://github.com/alibaba/lowcode-engine](https://github.com/alibaba/lowcode-engine) | packages/rax-renderer | -| @alilc/lowcode-rax-simulator-renderer | [https://github.com/alibaba/lowcode-engine](https://github.com/alibaba/lowcode-engine) | packages/rax-simulator-renderer | | @alilc/lowcode-react-renderer | [https://github.com/alibaba/lowcode-engine](https://github.com/alibaba/lowcode-engine) | packages/react-renderer | | @alilc/lowcode-react-simulator-renderer | [https://github.com/alibaba/lowcode-engine](https://github.com/alibaba/lowcode-engine) | packages/react-simulator-renderer | | @alilc/lowcode-renderer-core | [https://github.com/alibaba/lowcode-engine](https://github.com/alibaba/lowcode-engine) | packages/renderer-core | diff --git a/docs/docs/guide/appendix/repos.md b/docs/docs/guide/appendix/repos.md index 4fbfdc12c..87852258d 100644 --- a/docs/docs/guide/appendix/repos.md +++ b/docs/docs/guide/appendix/repos.md @@ -15,15 +15,13 @@ sidebar_position: 2 5. ignitor 6. plugin-designer 7. plugin-outline-pane -8. rax-renderer -9. rax-simulator-renderer -10. react-renderer -11. react-simulator-renderer -12. renderer-core -13. types -14. utils -15. material-parser -16. code-generator +8. react-renderer +9. react-simulator-renderer +10. renderer-core +11. types +12. utils +13. material-parser +14. code-generator ## 2. 引擎官方扩展包 包含了常用的设置器(setter)、跟 setter 绑定的插件等 diff --git a/docs/docs/guide/design/renderer.md b/docs/docs/guide/design/renderer.md index 4c3021b54..4a8c43f32 100644 --- a/docs/docs/guide/design/renderer.md +++ b/docs/docs/guide/design/renderer.md @@ -11,7 +11,6 @@ sidebar_position: 4 ## npm 包与仓库信息 - React 框架渲染 npm 包:@alilc/lowcode-react-renderer -- Rax 框架渲染 npm 包:@alilc/lowcode-rax-renderer - 仓库:[https://github.com/alibaba/lowcode-engine](https://github.com/alibaba/lowcode-engine) 下的 - packages/renderer-core - packages/react-renderer diff --git a/docs/docs/guide/expand/editor/theme.md b/docs/docs/guide/expand/editor/theme.md index ef0e04d28..897b6b360 100644 --- a/docs/docs/guide/expand/editor/theme.md +++ b/docs/docs/guide/expand/editor/theme.md @@ -53,6 +53,11 @@ sidebar_position: 9 - `--color-text-reverse`: 反色情况下,文字颜色 - `--color-text-disabled`: 禁用态文字颜色 +#### 菜单颜色 +- `--color-context-menu-text`: 菜单项颜色 +- `--color-context-menu-text-hover`: 菜单项 hover 颜色 +- `--color-context-menu-text-disabled`: 菜单项 disabled 颜色 + #### 字段和边框颜色 - `--color-field-label`: field 标签颜色 @@ -128,6 +133,7 @@ sidebar_position: 9 - `--pane-title-height`: 面板标题高度 - `--pane-title-font-size`: 面板标题字体大小 - `--pane-title-padding`: 面板标题边距 +- `--context-menu-item-height`: 右键菜单项高度 diff --git a/docs/docs/guide/expand/runtime/renderer.md b/docs/docs/guide/expand/runtime/renderer.md index 71eb75598..4e6d914bb 100644 --- a/docs/docs/guide/expand/runtime/renderer.md +++ b/docs/docs/guide/expand/runtime/renderer.md @@ -40,7 +40,6 @@ ReactDOM.render(( ), document.getElementById('root')); ``` -- rax-renderer:npm 包替换为 @alilc/lowcode-rax-renderer #### ### 项目使用示例 > [设计器 demo](https://lowcode-engine.cn/demo/demo-general/index.html) diff --git a/docs/docs/participate/index.md b/docs/docs/participate/index.md index 4540fa640..e09f2ddad 100644 --- a/docs/docs/participate/index.md +++ b/docs/docs/participate/index.md @@ -47,15 +47,7 @@ npm install && npm run setup [ "https?://uipaas-assets.com/prod/npm/@alilc/lowcode-engine/(.*)/dist/css/react-simulator-renderer.css", "http://localhost:5555/css/ReactSimulatorRenderer.css" - ], - [ - "https?://uipaas-assets.com/prod/npm/@alilc/lowcode-engine/(.*)/dist/js/rax-simulator-renderer.js", - "http://localhost:5555/js/RaxSimulatorRenderer.js" - ], - [ - "https?://uipaas-assets.com/prod/npm/@alilc/lowcode-engine/(.*)/dist/css/rax-simulator-renderer.css", - "http://localhost:5555/css/RaxSimulatorRenderer.css" - ], + ] ] } ``` diff --git a/docs/docs/specs/lowcode-spec.md b/docs/docs/specs/lowcode-spec.md index 59ff85586..c27721410 100644 --- a/docs/docs/specs/lowcode-spec.md +++ b/docs/docs/specs/lowcode-spec.md @@ -499,7 +499,6 @@ try { - 说明:组件即将从 DOM 中移除 - componentDidCatch(error, info) - 说明:组件捕获到异常 -- Rax:目前没有使用生命周期,使用 hooks 替代生命周期; 该对象由一系列 key-value 组成,key 为生命周期方法名,value 为 JSFunction 的描述,详见下方示例: diff --git a/docs/package.json b/docs/package.json index ab6edf5cb..643bdace2 100644 --- a/docs/package.json +++ b/docs/package.json @@ -1,6 +1,6 @@ { "name": "@alilc/lowcode-engine-docs", - "version": "1.2.17", + "version": "1.2.27", "description": "低代码引擎版本化文档", "license": "MIT", "files": [ diff --git a/lerna.json b/lerna.json index 117030917..33d4899d0 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "lerna": "4.0.0", - "version": "1.2.5", + "version": "1.3.0", "npmClient": "yarn", "useWorkspaces": true, "packages": [ diff --git a/modules/code-generator/README.md b/modules/code-generator/README.md index 03ce94b54..1d67b3aa1 100644 --- a/modules/code-generator/README.md +++ b/modules/code-generator/README.md @@ -94,16 +94,26 @@ await CodeGenerator.init(); 4. 出码 ```js -const result = await CodeGenerator.generateCode({ +const project = await CodeGenerator.generateCode({ solution: 'icejs', // 出码方案 (目前内置有 icejs 和 rax ) schema, // 编排搭建出来的 schema }); -console.log(result); // 出码结果(默认是递归结构描述的,可以传 flattenResult: true 以生成扁平结构的结果) +console.log(project); // 出码结果(默认是递归结构描述的,可以传 flattenResult: true 以生成扁平结构的结果) ``` 注:一般来说在浏览器中出码适合做即时预览功能。 +5. 下载 zip 包 + +```js +// 写入到 zip 包 +await CodeGenerator.publishers.zip().publish({ + project, // 上一步生成的 project + projectSlug: 'your-project-slug', // 项目标识 -- 对应下载 your-project-slug.zip 文件 +}); +``` + ### 5)自定义出码 前端框架灵活多变,默认内置的出码方案很难满足所有人的需求,好在此代码生成器支持非常灵活的插件机制 -- 欢迎参考 ./src/plugins/xxx 来编写您自己的出码插件,然后参考 ./src/solutions/xxx 将各种插件组合成一套适合您的业务场景的出码方案。 diff --git a/modules/code-generator/package.json b/modules/code-generator/package.json index 6ff1ecb63..cd114fa06 100644 --- a/modules/code-generator/package.json +++ b/modules/code-generator/package.json @@ -1,6 +1,6 @@ { "name": "@alilc/lowcode-code-generator", - "version": "1.1.6", + "version": "1.1.7", "description": "出码引擎 for LowCode Engine", "license": "MIT", "main": "lib/index.js", @@ -80,6 +80,7 @@ "change-case": "^3.1.0", "commander": "^6.1.0", "debug": "^4.3.2", + "file-saver": "^2.0.5", "fp-ts": "^2.11.9", "fs-extra": "9.x", "glob": "^7.2.0", @@ -109,6 +110,7 @@ "devDependencies": { "@iceworks/spec": "^1.4.2", "@types/babel__traverse": "^7.11.0", + "@types/file-saver": "^2.0.7", "@types/jest": "^27.0.2", "@types/lodash": "^4.14.162", "@types/node": "^14.14.20", diff --git a/modules/code-generator/src/publisher/zip/index.ts b/modules/code-generator/src/publisher/zip/index.ts index cc2f082b2..0ac0b6f67 100644 --- a/modules/code-generator/src/publisher/zip/index.ts +++ b/modules/code-generator/src/publisher/zip/index.ts @@ -2,9 +2,9 @@ import { ResultDir } from '@alilc/lowcode-types'; import { PublisherFactory, IPublisher, IPublisherFactoryParams, PublisherError } from '../../types'; import { getErrorMessage } from '../../utils/errors'; import { isNodeProcess, writeZipToDisk, generateProjectZip } from './utils'; +import { saveAs } from 'file-saver'; -// export type ZipBuffer = Buffer | Blob; -export type ZipBuffer = Buffer; +export type ZipBuffer = Buffer | Blob; declare type ZipPublisherResponse = string | ZipBuffer; @@ -44,10 +44,16 @@ export const createZipPublisher: PublisherFactory => { let zip = new JSZip(); zip = writeFolderToZip(project, zip, true); - // const zipType = isNodeProcess() ? 'nodebuffer' : 'blob'; - const zipType = 'nodebuffer'; // 目前先只支持 node 调用 + const zipType = isNodeProcess() ? 'nodebuffer' : 'blob'; return zip.generateAsync({ type: zipType }); }; diff --git a/modules/code-generator/tests/public/publisher/zip/zip.test.ts b/modules/code-generator/tests/public/publisher/zip/zip.test.ts index ed37980d0..d51957004 100644 --- a/modules/code-generator/tests/public/publisher/zip/zip.test.ts +++ b/modules/code-generator/tests/public/publisher/zip/zip.test.ts @@ -1,6 +1,14 @@ import CodeGen from '../../../../src'; +import fileSaver from 'file-saver'; +import * as utils from '../../../../src/publisher/zip/utils'; + +jest.mock('file-saver'); describe('public/publisher/zip/zip', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + it('should works', async () => { const zip = CodeGen.publishers.zip({ outputPath: 'demo-output', @@ -19,15 +27,15 @@ describe('public/publisher/zip/zip', () => { ], }; - expect(zip.getOutputPath()).toMatchInlineSnapshot(`"demo-output"`); + expect(zip.getOutputPath()).toMatchInlineSnapshot('"demo-output"'); - expect(zip.getProject()).toMatchInlineSnapshot(`undefined`); + expect(zip.getProject()).toMatchInlineSnapshot('undefined'); zip.setProject(demoProject); expect(zip.getProject()).toBeTruthy(); - expect(zip.getOutputPath()).toMatchInlineSnapshot(`"demo-output"`); + expect(zip.getOutputPath()).toMatchInlineSnapshot('"demo-output"'); expect(zip.setOutputPath('output')).toBe(undefined); - expect(zip.getOutputPath()).toMatchInlineSnapshot(`"output"`); + expect(zip.getOutputPath()).toMatchInlineSnapshot('"output"'); const publishRes = await zip.publish({ project: demoProject, @@ -41,4 +49,39 @@ describe('public/publisher/zip/zip', () => { const zip = CodeGen.publishers.zip({}); expect(zip.publish()).rejects.toBeTruthy(); }); + + it('should publish the project as a zip file in the browser', async () => { + const zipContent = 'zip content'; + const zipName = 'example-project'; + jest.spyOn(utils, 'isNodeProcess').mockReturnValue(false); + // new Zip 里面也有平台判断,所以这里 mock + jest.spyOn(utils, 'generateProjectZip').mockResolvedValue(zipContent as any); + const spy = jest.spyOn(fileSaver, 'saveAs'); + + const zip = CodeGen.publishers.zip({ + projectSlug: zipName, + }); + + const demoProject = { + name: 'demo', + dirs: [], + files: [ + { + name: 'package', + ext: 'json', + content: '{ "name": "demo", "version": "1.0.0" }', + }, + ], + }; + + zip.setProject(demoProject); + const publishRes = await zip.publish({ + project: demoProject, + }); + + expect(publishRes.success).toBeTruthy(); + expect(spy).toBeCalledWith(zipContent, `${zipName}.zip`); + spy.mockReset(); + spy.mockRestore(); + }); }); diff --git a/package.json b/package.json index 6562cec3b..8b566e0f0 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "yarn": "^1.22.17", "rimraf": "^3.0.2", "@types/react-router": "5.1.18", - "@alilc/build-plugin-lce": "^0.0.3", + "@alilc/build-plugin-lce": "^0.0.4", "babel-jest": "^26.5.2", "@alilc/lowcode-test-mate": "^1.0.1" }, diff --git a/packages/designer/package.json b/packages/designer/package.json index 0e6e67e88..5ecf6c139 100644 --- a/packages/designer/package.json +++ b/packages/designer/package.json @@ -1,6 +1,6 @@ { "name": "@alilc/lowcode-designer", - "version": "1.2.5", + "version": "1.3.0", "description": "Designer for Ali LowCode Engine", "main": "lib/index.js", "module": "es/index.js", @@ -15,9 +15,9 @@ }, "license": "MIT", "dependencies": { - "@alilc/lowcode-editor-core": "1.2.5", - "@alilc/lowcode-types": "1.2.5", - "@alilc/lowcode-utils": "1.2.5", + "@alilc/lowcode-editor-core": "1.3.0", + "@alilc/lowcode-types": "1.3.0", + "@alilc/lowcode-utils": "1.3.0", "classnames": "^2.2.6", "react": "^16", "react-dom": "^16.7.0", diff --git a/packages/designer/src/builtin-simulator/bem-tools/border-selecting.tsx b/packages/designer/src/builtin-simulator/bem-tools/border-selecting.tsx index 4f452727d..143c67e02 100644 --- a/packages/designer/src/builtin-simulator/bem-tools/border-selecting.tsx +++ b/packages/designer/src/builtin-simulator/bem-tools/border-selecting.tsx @@ -9,7 +9,7 @@ import { ComponentType, } from 'react'; import classNames from 'classnames'; -import { observer, computed, Tip } from '@alilc/lowcode-editor-core'; +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'; @@ -47,14 +47,18 @@ export class BorderSelectingInstance extends Component<{ }); const { hideSelectTools } = observed.node.componentMeta.advanced; + const hideComponentAction = engineConfig.get('hideComponentAction'); if (hideSelectTools) { return null; } return ( -
- {!dragging && } +
+ {(!dragging && !hideComponentAction) ? : null}
); } diff --git a/packages/designer/src/builtin-simulator/host.ts b/packages/designer/src/builtin-simulator/host.ts index eb855e449..57f856932 100644 --- a/packages/designer/src/builtin-simulator/host.ts +++ b/packages/designer/src/builtin-simulator/host.ts @@ -39,6 +39,7 @@ import { isDragAnyObject, isDragNodeObject, isLocationData, + Logger, } from '@alilc/lowcode-utils'; import { isShaken, @@ -72,6 +73,8 @@ import { IScroller } from '../designer/scroller'; import { isElementNode, isDOMNodeVisible } from '../utils/misc'; import { debounce } from 'lodash'; +const logger = new Logger({ level: 'warn', bizName: 'designer' }); + export type LibraryItem = IPublicTypePackage & { package: string; library: string; @@ -122,21 +125,6 @@ const defaultSimulatorUrl = (() => { return urls; })(); -const defaultRaxSimulatorUrl = (() => { - const publicPath = getPublicPath(); - let urls; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [_, prefix = '', dev] = /^(.+?)(\/js)?\/?$/.exec(publicPath) || []; - if (dev) { - urls = [`${prefix}/css/rax-simulator-renderer.css`, `${prefix}/js/rax-simulator-renderer.js`]; - } else if (process.env.NODE_ENV === 'production') { - urls = [`${prefix}/rax-simulator-renderer.css`, `${prefix}/rax-simulator-renderer.js`]; - } else { - urls = [`${prefix}/rax-simulator-renderer.css`, `${prefix}/rax-simulator-renderer.js`]; - } - return urls; -})(); - const defaultEnvironment = [ // https://g.alicdn.com/mylib/??react/16.11.0/umd/react.production.min.js,react-dom/16.8.6/umd/react-dom.production.min.js,prop-types/15.7.2/prop-types.min.js assetItem( @@ -151,17 +139,6 @@ const defaultEnvironment = [ ), ]; -const defaultRaxEnvironment = [ - assetItem( - AssetType.JSText, - 'window.Rax=parent.Rax;window.React=parent.React;window.ReactDOM=parent.ReactDOM;window.VisualEngineUtils=parent.VisualEngineUtils;window.VisualEngine=parent.VisualEngine', - ), - assetItem( - AssetType.JSText, - 'window.PropTypes=parent.PropTypes;React.PropTypes=parent.PropTypes; window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = window.parent.__REACT_DEVTOOLS_GLOBAL_HOOK__;', - ), -]; - export class BuiltinSimulatorHost implements ISimulatorHost { readonly isSimulator = true; @@ -467,11 +444,15 @@ export class BuiltinSimulatorHost implements ISimulatorHost { const targetElement = e.target as HTMLElement; const nodeInst = this.getNodeInstanceFromElement(targetElement); + const editor = this.designer?.editor; if (!nodeInst) { + editor?.eventBus.emit('designer.builtinSimulator.contextmenu', { + originalEvent: e, + }); return; } const node = nodeInst.node || this.project.currentDocument?.focusNode; if (!node) { + editor?.eventBus.emit('designer.builtinSimulator.contextmenu', { + originalEvent: e, + }); return; } // dirty code should refector - const editor = this.designer?.editor; const npm = node?.componentMeta?.npm; const selected = [npm?.package, npm?.componentName].filter((item) => !!item).join('-') || diff --git a/packages/designer/src/component-meta.ts b/packages/designer/src/component-meta.ts index 00c495621..1ee1154f1 100644 --- a/packages/designer/src/component-meta.ts +++ b/packages/designer/src/component-meta.ts @@ -48,13 +48,17 @@ export function buildFilter(rule?: string | string[] | RegExp | IPublicTypeNesti return rule; } if (isRegExp(rule)) { - return (testNode: Node | IPublicTypeNodeSchema) => rule.test(testNode.componentName); + return (testNode: Node | IPublicTypeNodeSchema) => { + return rule.test(testNode.componentName); + }; } const list = ensureAList(rule); if (!list) { return null; } - return (testNode: Node | IPublicTypeNodeSchema) => list.includes(testNode.componentName); + return (testNode: Node | IPublicTypeNodeSchema) => { + return list.includes(testNode.componentName); + }; } export interface IComponentMeta extends IPublicModelComponentMeta { diff --git a/packages/designer/src/context-menu-actions.scss b/packages/designer/src/context-menu-actions.scss new file mode 100644 index 000000000..863c92944 --- /dev/null +++ b/packages/designer/src/context-menu-actions.scss @@ -0,0 +1,10 @@ +.engine-context-menu { + &.next-menu.next-ver .next-menu-item { + padding-right: 30px; + + .next-menu-item-inner { + height: var(--context-menu-item-height, 30px); + line-height: var(--context-menu-item-height, 30px); + } + } +} \ No newline at end of file diff --git a/packages/designer/src/context-menu-actions.ts b/packages/designer/src/context-menu-actions.ts new file mode 100644 index 000000000..c88e03ac6 --- /dev/null +++ b/packages/designer/src/context-menu-actions.ts @@ -0,0 +1,233 @@ +import { IPublicTypeContextMenuAction, IPublicEnumContextMenuType, IPublicTypeContextMenuItem, IPublicApiMaterial, IPublicModelPluginContext } from '@alilc/lowcode-types'; +import { IDesigner, INode } from './designer'; +import { createContextMenu, parseContextMenuAsReactNode, parseContextMenuProperties, uniqueId } from '@alilc/lowcode-utils'; +import { Menu } from '@alifd/next'; +import { engineConfig } from '@alilc/lowcode-editor-core'; +import './context-menu-actions.scss'; + +export interface IContextMenuActions { + actions: IPublicTypeContextMenuAction[]; + + adjustMenuLayoutFn: (actions: IPublicTypeContextMenuItem[]) => IPublicTypeContextMenuItem[]; + + addMenuAction: IPublicApiMaterial['addContextMenuOption']; + + removeMenuAction: IPublicApiMaterial['removeContextMenuOption']; + + adjustMenuLayout: IPublicApiMaterial['adjustContextMenuLayout']; +} + +let adjustMenuLayoutFn: Function = (actions: IPublicTypeContextMenuAction[]) => actions; + +export class GlobalContextMenuActions { + enableContextMenu: boolean; + + dispose: Function[]; + + contextMenuActionsMap: Map = 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[] = []; + let contextMenu: ContextMenuActions = this.contextMenuActionsMap.values().next().value; + this.contextMenuActionsMap.forEach((contextMenu) => { + actions.push(...contextMenu.actions); + }); + + let destroyFn: Function | 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?.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 implements IContextMenuActions { + actions: IPublicTypeContextMenuAction[] = []; + + designer: IDesigner; + + dispose: Function[]; + + 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: Function | 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(action: IPublicTypeContextMenuAction) { + this.actions.push({ + type: IPublicEnumContextMenuType.MENU_ITEM, + ...action, + }); + } + + removeMenuAction(name: string) { + const i = this.actions.findIndex((action) => action.name === name); + if (i > -1) { + this.actions.splice(i, 1); + } + } + + adjustMenuLayout(fn: (actions: IPublicTypeContextMenuItem[]) => IPublicTypeContextMenuItem[]) { + adjustMenuLayoutFn = fn; + } +} \ No newline at end of file diff --git a/packages/designer/src/designer/clipboard.ts b/packages/designer/src/designer/clipboard.ts index 941f91442..34ce2b5b5 100644 --- a/packages/designer/src/designer/clipboard.ts +++ b/packages/designer/src/designer/clipboard.ts @@ -36,6 +36,10 @@ class Clipboard implements IClipboard { private waitFn?: (data: any, e: ClipboardEvent) => void; + constructor() { + this.injectCopyPaster(document); + } + isCopyPasteEvent(e: Event) { this.isCopyPaster(e.target); } @@ -69,7 +73,13 @@ class Clipboard implements IClipboard { } const copyPaster = document.createElement<'textarea'>('textarea'); copyPaster.style.cssText = 'position: absolute;left: -9999px;top:-100px'; - document.body.appendChild(copyPaster); + if (document.body) { + document.body.appendChild(copyPaster); + } else { + document.addEventListener('DOMContentLoaded', () => { + document.body.appendChild(copyPaster); + }); + } const dispose = this.initCopyPaster(copyPaster); return () => { dispose(); diff --git a/packages/designer/src/designer/designer-view.tsx b/packages/designer/src/designer/designer-view.tsx index 0db2a3fa3..aaf0c9583 100644 --- a/packages/designer/src/designer/designer-view.tsx +++ b/packages/designer/src/designer/designer-view.tsx @@ -4,7 +4,6 @@ import BuiltinDragGhostComponent from './drag-ghost'; import { Designer, DesignerProps } from './designer'; import { ProjectView } from '../project'; import './designer.less'; -import { clipboard } from './clipboard'; type IProps = DesignerProps & { designer?: Designer; @@ -44,7 +43,6 @@ export class DesignerView extends Component { if (onMount) { onMount(this.designer); } - clipboard.injectCopyPaster(document); this.designer.postEvent('mount', this.designer); } diff --git a/packages/designer/src/designer/designer.ts b/packages/designer/src/designer/designer.ts index d7e17e84c..1dd4bc04e 100644 --- a/packages/designer/src/designer/designer.ts +++ b/packages/designer/src/designer/designer.ts @@ -20,7 +20,7 @@ import { } 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 { Node, DocumentModel, insertChildren, INode, ISelection } from '../document'; import { ComponentMeta, IComponentMeta } from '../component-meta'; import { INodeSelector, Component } from '../simulator'; import { Scroller } from './scroller'; @@ -32,6 +32,7 @@ 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' }); @@ -72,12 +73,16 @@ export interface IDesigner { get componentActions(): ComponentActions; + get contextMenuActions(): ContextMenuActions; + get editor(): IPublicModelEditor; get detecting(): Detecting; get simulatorComponent(): ComponentType | undefined; + get currentSelection(): ISelection; + createScroller(scrollable: IPublicTypeScrollable): IPublicModelScroller; refreshComponentMetasMap(): void; @@ -122,6 +127,8 @@ export class Designer implements IDesigner { readonly componentActions = new ComponentActions(); + readonly contextMenuActions: IContextMenuActions; + readonly activeTracker = new ActiveTracker(); readonly detecting = new Detecting(); @@ -198,6 +205,8 @@ export class Designer implements IDesigner { this.postEvent('dragstart', e); }); + this.contextMenuActions = new ContextMenuActions(this); + this.dragon.onDrag((e) => { if (this.props?.onDrag) { this.props.onDrag(e); diff --git a/packages/designer/src/designer/setting/setting-top-entry.ts b/packages/designer/src/designer/setting/setting-top-entry.ts index f89ff8a62..85be74b7f 100644 --- a/packages/designer/src/designer/setting/setting-top-entry.ts +++ b/packages/designer/src/designer/setting/setting-top-entry.ts @@ -1,4 +1,4 @@ -import { IPublicTypeCustomView, IPublicModelEditor, IPublicModelSettingTopEntry } from '@alilc/lowcode-types'; +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'; @@ -6,7 +6,6 @@ import { ISettingField, SettingField } from './setting-field'; import { INode } from '../../document'; import type { IComponentMeta } from '../../component-meta'; import { IDesigner } from '../designer'; -import { Setters } from '@alilc/lowcode-shell'; function generateSessionId(nodes: INode[]) { return nodes @@ -19,18 +18,18 @@ export interface ISettingTopEntry extends ISettingEntry, IPublicModelSettingTopE INode, ISettingField > { - purge(): void; - - items: Array; - readonly top: ISettingTopEntry; readonly parent: ISettingTopEntry; readonly path: never[]; + items: Array; + componentMeta: IComponentMeta | null; + purge(): void; + getExtraPropValue(propName: string): void; setExtraPropValue(propName: string, value: any): void; @@ -92,7 +91,7 @@ export class SettingTopEntry implements ISettingTopEntry { readonly designer: IDesigner | undefined; - readonly setters: Setters; + readonly setters: IPublicApiSetters; disposeFunctions: any[] = []; @@ -103,7 +102,7 @@ export class SettingTopEntry implements ISettingTopEntry { this.id = generateSessionId(nodes); this.first = nodes[0]; this.designer = this.first.document?.designer; - this.setters = editor.get('setters') as Setters; + this.setters = editor.get('setters') as IPublicApiSetters; // setups this.setupComponentMeta(); diff --git a/packages/designer/src/document/node/node.ts b/packages/designer/src/document/node/node.ts index 4071fb913..c8363d058 100644 --- a/packages/designer/src/document/node/node.ts +++ b/packages/designer/src/document/node/node.ts @@ -392,7 +392,7 @@ export class Node this.isInited = true; this.emitter = createModuleEventBus('Node'); - const editor = this.document.designer.editor; + const { editor } = this.document.designer; this.onVisibleChange((visible: boolean) => { editor?.eventBus.emit(EDITOR_EVENT.NODE_VISIBLE_CHANGE, this, visible); }); @@ -1219,11 +1219,18 @@ export class Node /** * 获取磁贴相关信息 */ - getRGL() { + getRGL(): { + isContainerNode: boolean; + isEmptyNode: boolean; + isRGLContainerNode: boolean; + isRGLNode: boolean; + isRGL: boolean; + rglNode: Node | null; + } { const isContainerNode = this.isContainer(); const isEmptyNode = this.isEmpty(); const isRGLContainerNode = this.isRGLContainer; - const isRGLNode = this.getParent()?.isRGLContainer; + const isRGLNode = (this.getParent()?.isRGLContainer) as boolean; const isRGL = isRGLContainerNode || (isRGLNode && (!isContainerNode || !isEmptyNode)); let rglNode = isRGLContainerNode ? this : isRGL ? this?.getParent() : null; return { isContainerNode, isEmptyNode, isRGLContainerNode, isRGLNode, isRGL, rglNode }; diff --git a/packages/designer/src/index.ts b/packages/designer/src/index.ts index 489d81482..11e6453b8 100644 --- a/packages/designer/src/index.ts +++ b/packages/designer/src/index.ts @@ -6,3 +6,4 @@ export * from './project'; export * from './builtin-simulator'; export * from './plugin'; export * from './types'; +export * from './context-menu-actions'; diff --git a/packages/designer/src/plugin/plugin-context.ts b/packages/designer/src/plugin/plugin-context.ts index 94e296fd1..7f26a2b4f 100644 --- a/packages/designer/src/plugin/plugin-context.ts +++ b/packages/designer/src/plugin/plugin-context.ts @@ -19,6 +19,7 @@ import { IPublicApiWorkspace, IPublicEnumPluginRegisterLevel, IPublicModelWindow, + IPublicApiCommonUI, } from '@alilc/lowcode-types'; import { IPluginContextOptions, @@ -45,6 +46,8 @@ export default class PluginContext implements workspace: IPublicApiWorkspace; registerLevel: IPublicEnumPluginRegisterLevel; editorWindow: IPublicModelWindow; + commonUI: IPublicApiCommonUI; + isPluginRegisteredInWorkspace: false; constructor( options: IPluginContextOptions, diff --git a/packages/designer/src/plugin/plugin-types.ts b/packages/designer/src/plugin/plugin-types.ts index 6091170f0..d648067a3 100644 --- a/packages/designer/src/plugin/plugin-types.ts +++ b/packages/designer/src/plugin/plugin-types.ts @@ -18,6 +18,7 @@ import { IPublicTypePluginRegisterOptions, IPublicModelWindow, IPublicEnumPluginRegisterLevel, + IPublicApiCommonUI, } from '@alilc/lowcode-types'; import PluginContext from './plugin-context'; @@ -61,6 +62,7 @@ export interface ILowCodePluginContextPrivate { set editorWindow(window: IPublicModelWindow); set registerLevel(level: IPublicEnumPluginRegisterLevel); set isPluginRegisteredInWorkspace(flag: boolean); + set commonUI(commonUI: IPublicApiCommonUI); } export interface ILowCodePluginContextApiAssembler { assembleApis( diff --git a/packages/editor-core/package.json b/packages/editor-core/package.json index 087558450..888642adf 100644 --- a/packages/editor-core/package.json +++ b/packages/editor-core/package.json @@ -1,6 +1,6 @@ { "name": "@alilc/lowcode-editor-core", - "version": "1.2.5", + "version": "1.3.0", "description": "Core Api for Ali lowCode engine", "license": "MIT", "main": "lib/index.js", @@ -14,8 +14,8 @@ }, "dependencies": { "@alifd/next": "^1.19.16", - "@alilc/lowcode-types": "1.2.5", - "@alilc/lowcode-utils": "1.2.5", + "@alilc/lowcode-types": "1.3.0", + "@alilc/lowcode-utils": "1.3.0", "classnames": "^2.2.6", "debug": "^4.1.1", "intl-messageformat": "^9.3.1", diff --git a/packages/editor-core/src/config.ts b/packages/editor-core/src/config.ts index 85e609780..c4ff407b9 100644 --- a/packages/editor-core/src/config.ts +++ b/packages/editor-core/src/config.ts @@ -44,7 +44,7 @@ const VALID_ENGINE_OPTIONS = { }, renderEnv: { type: 'string', - enum: ['react', 'rax', 'any string value'], + enum: ['react', 'any string value'], default: 'react', description: '渲染器类型', }, @@ -159,6 +159,16 @@ const VALID_ENGINE_OPTIONS = { type: 'function', description: '应用级设计模式下,窗口为空时展示的占位组件', }, + enableContextMenu: { + type: 'boolean', + description: '是否开启右键菜单', + default: false, + }, + hideComponentAction: { + type: 'boolean', + description: '是否隐藏设计器辅助层', + default: false, + }, }; const getStrictModeValue = (engineOptions: IPublicTypeEngineOptions, defaultValue: boolean): boolean => { diff --git a/packages/editor-core/src/intl/index.ts b/packages/editor-core/src/intl/index.ts index 6d9d840c3..99e99a4fb 100644 --- a/packages/editor-core/src/intl/index.ts +++ b/packages/editor-core/src/intl/index.ts @@ -3,6 +3,7 @@ import { IntlMessageFormat } from 'intl-messageformat'; import { globalLocale } from './global-locale'; import { isI18nData } from '@alilc/lowcode-utils'; import { observer } from '../utils'; +import { IPublicTypeI18nData } from '@alilc/lowcode-types'; function generateTryLocales(locale: string) { const tries = [locale, locale.replace('-', '_')]; @@ -26,18 +27,9 @@ function injectVars(msg: string, params: any, locale: string): string { } const formater = new IntlMessageFormat(msg, locale); return formater.format(params as any) as string; - /* - - return template.replace(/({\w+})/g, (_, $1) => { - const key = (/\d+/.exec($1) || [])[0] as any; - if (key && params[key] != null) { - return params[key]; - } - return $1; - }); */ } -export function intl(data: any, params?: object): ReactNode { +export function intl(data: IPublicTypeI18nData | string, params?: object): ReactNode { if (!isI18nData(data)) { return data; } diff --git a/packages/editor-core/src/widgets/tip/help-tips.tsx b/packages/editor-core/src/widgets/tip/help-tips.tsx new file mode 100644 index 000000000..ab5f65050 --- /dev/null +++ b/packages/editor-core/src/widgets/tip/help-tips.tsx @@ -0,0 +1,40 @@ +import { IPublicTypeHelpTipConfig, IPublicTypeTipConfig } from '@alilc/lowcode-types'; +import { Tip } from './tip'; +import { Icon } from '@alifd/next'; +import { IconProps } from '@alifd/next/types/icon'; + +export function HelpTip({ + help, + direction = 'top', + size = 'small', +}: { + help: IPublicTypeHelpTipConfig; + direction?: IPublicTypeTipConfig['direction']; + size?: IconProps['size']; +}) { + if (typeof help === 'string') { + return ( +
+ + {help} +
+ ); + } + + if (typeof help === 'object' && help.url) { + return ( +
+ + + + {help.content} +
+ ); + } + return ( +
+ + {help.content} +
+ ); +} \ No newline at end of file diff --git a/packages/editor-core/src/widgets/tip/index.ts b/packages/editor-core/src/widgets/tip/index.ts index dba4412c7..d2b376800 100644 --- a/packages/editor-core/src/widgets/tip/index.ts +++ b/packages/editor-core/src/widgets/tip/index.ts @@ -2,3 +2,4 @@ import './style.less'; export * from './tip'; export * from './tip-container'; +export * from './help-tips'; diff --git a/packages/editor-core/src/widgets/title/index.tsx b/packages/editor-core/src/widgets/title/index.tsx index 88a15ab29..7df2676f9 100644 --- a/packages/editor-core/src/widgets/title/index.tsx +++ b/packages/editor-core/src/widgets/title/index.tsx @@ -1,7 +1,7 @@ import { Component, isValidElement, ReactNode } from 'react'; import classNames from 'classnames'; import { createIcon, isI18nData, isTitleConfig } from '@alilc/lowcode-utils'; -import { IPublicTypeTitleContent, IPublicTypeI18nData, IPublicTypeTitleConfig } from '@alilc/lowcode-types'; +import { IPublicTypeI18nData, IPublicTypeTitleConfig, IPublicTypeTitleProps } from '@alilc/lowcode-types'; import { intl } from '../../intl'; import { Tip } from '../tip'; import './title.less'; @@ -36,13 +36,7 @@ import './title.less'; return fragments; } -export class Title extends Component<{ - title: IPublicTypeTitleContent; - className?: string; - onClick?: () => void; - match?: boolean; - keywords?: string; -}> { +export class Title extends Component { constructor(props: any) { super(props); this.handleClick = this.handleClick.bind(this); diff --git a/packages/editor-skeleton/package.json b/packages/editor-skeleton/package.json index 648ed5be1..58c5f2e18 100644 --- a/packages/editor-skeleton/package.json +++ b/packages/editor-skeleton/package.json @@ -1,6 +1,6 @@ { "name": "@alilc/lowcode-editor-skeleton", - "version": "1.2.5", + "version": "1.3.0", "description": "alibaba lowcode editor skeleton", "main": "lib/index.js", "module": "es/index.js", @@ -19,10 +19,10 @@ ], "dependencies": { "@alifd/next": "^1.20.12", - "@alilc/lowcode-designer": "1.2.5", - "@alilc/lowcode-editor-core": "1.2.5", - "@alilc/lowcode-types": "1.2.5", - "@alilc/lowcode-utils": "1.2.5", + "@alilc/lowcode-designer": "1.3.0", + "@alilc/lowcode-editor-core": "1.3.0", + "@alilc/lowcode-types": "1.3.0", + "@alilc/lowcode-utils": "1.3.0", "classnames": "^2.2.6", "react": "^16.8.1", "react-dom": "^16.8.1" diff --git a/packages/editor-skeleton/src/components/widget-views/index.tsx b/packages/editor-skeleton/src/components/widget-views/index.tsx index 2b2f6931c..7cdff4c01 100644 --- a/packages/editor-skeleton/src/components/widget-views/index.tsx +++ b/packages/editor-skeleton/src/components/widget-views/index.tsx @@ -1,7 +1,6 @@ import { Component, ReactElement } from 'react'; -import { Icon } from '@alifd/next'; import classNames from 'classnames'; -import { Title, observer, Tip } from '@alilc/lowcode-editor-core'; +import { Title, observer, HelpTip } from '@alilc/lowcode-editor-core'; import { DockProps } from '../../types'; import { PanelDock } from '../../widget/panel-dock'; import { composeTitle } from '../../widget/utils'; @@ -26,25 +25,6 @@ export function DockView({ title, icon, description, size, className, onClick }: ); } -function HelpTip({ tip }: any) { - if (tip && tip.url) { - return ( -
- - - - {tip.content} -
- ); - } - return ( -
- - {tip.content} -
- ); -} - @observer export class PanelDockView extends Component { private lastActived = false; @@ -328,7 +308,7 @@ class PanelTitle extends Component<{ panel: Panel; className?: string }> { data-name={panel.name} > - {panel.help ? <HelpTip tip={panel.help} /> : null} + {panel.help ? <HelpTip help={panel.help} /> : null} </div> ); } diff --git a/packages/engine/README-zh_CN.md b/packages/engine/README-zh_CN.md index c99e98cf6..5442aa58b 100644 --- a/packages/engine/README-zh_CN.md +++ b/packages/engine/README-zh_CN.md @@ -126,7 +126,7 @@ https://cdn.jsdelivr.net/npm/@alilc/lowcode-react-simulator-renderer@1.0.18/dist ``` #### 方式 5:使用自有 cdn -将源码中 packages/engine/dist 和 packages/(react|rax)-simulator-renderer/dist 下的文件传至你的 cdn 提供商 +将源码中 packages/engine/dist 和 packages/react-simulator-renderer/dist 下的文件传至你的 cdn 提供商 ## 🔗 相关链接 diff --git a/packages/engine/README.md b/packages/engine/README.md index 52bce8616..ae4e7fd43 100644 --- a/packages/engine/README.md +++ b/packages/engine/README.md @@ -126,7 +126,7 @@ https://cdn.jsdelivr.net/npm/@alilc/lowcode-react-simulator-renderer@1.0.18/dist ``` #### Method 5: Use your own cdn -Pass the files under packages/engine/dist and packages/(react|rax)-simulator-renderer/dist in the source code to your cdn provider +Pass the files under packages/engine/dist and packages/react-simulator-renderer/dist in the source code to your cdn provider ## 🔗 Related Links diff --git a/packages/engine/package.json b/packages/engine/package.json index 4caf32c00..7eef6514b 100644 --- a/packages/engine/package.json +++ b/packages/engine/package.json @@ -1,6 +1,6 @@ { "name": "@alilc/lowcode-engine", - "version": "1.2.5", + "version": "1.3.0", "description": "An enterprise-class low-code technology stack with scale-out design / 一套面向扩展设计的企业级低代码技术体系", "main": "lib/engine-core.js", "module": "es/engine-core.js", @@ -19,15 +19,15 @@ "license": "MIT", "dependencies": { "@alifd/next": "^1.19.12", - "@alilc/lowcode-designer": "1.2.5", - "@alilc/lowcode-editor-core": "1.2.5", - "@alilc/lowcode-editor-skeleton": "1.2.5", + "@alilc/lowcode-designer": "1.3.0", + "@alilc/lowcode-editor-core": "1.3.0", + "@alilc/lowcode-editor-skeleton": "1.3.0", "@alilc/lowcode-engine-ext": "^1.0.0", - "@alilc/lowcode-plugin-designer": "1.2.5", - "@alilc/lowcode-plugin-outline-pane": "1.2.5", - "@alilc/lowcode-shell": "1.2.5", - "@alilc/lowcode-utils": "1.2.5", - "@alilc/lowcode-workspace": "1.2.5", + "@alilc/lowcode-plugin-designer": "1.3.0", + "@alilc/lowcode-plugin-outline-pane": "1.3.0", + "@alilc/lowcode-shell": "1.3.0", + "@alilc/lowcode-utils": "1.3.0", + "@alilc/lowcode-workspace": "1.3.0", "react": "^16.8.1", "react-dom": "^16.8.1" }, diff --git a/packages/engine/src/engine-core.ts b/packages/engine/src/engine-core.ts index 3ac4c32a7..3ccfdf51d 100644 --- a/packages/engine/src/engine-core.ts +++ b/packages/engine/src/engine-core.ts @@ -51,6 +51,7 @@ import { Canvas, Workspace, Config, + CommonUI, } from '@alilc/lowcode-shell'; import { isPlainObject } from '@alilc/lowcode-utils'; import './modules/live-editing'; @@ -61,6 +62,7 @@ import { setterRegistry } from './inner-plugins/setter-registry'; import { defaultPanelRegistry } from './inner-plugins/default-panel-registry'; import { shellModelFactory } from './modules/shell-model-factory'; import { builtinHotkey } from './inner-plugins/builtin-hotkey'; +import { defaultContextMenu } from './inner-plugins/default-context-menu'; import { OutlinePlugin } from '@alilc/lowcode-plugin-outline-pane'; export * from './modules/skeleton-types'; @@ -77,6 +79,7 @@ async function registryInnerPlugin(designer: IDesigner, editor: IEditor, plugins await plugins.register(defaultPanelRegistryPlugin); await plugins.register(builtinHotkey); await plugins.register(registerDefaults, {}, { autoInit: true }); + await plugins.register(defaultContextMenu); return () => { plugins.delete(OutlinePlugin.pluginName); @@ -85,6 +88,7 @@ async function registryInnerPlugin(designer: IDesigner, editor: IEditor, plugins plugins.delete(defaultPanelRegistryPlugin.pluginName); plugins.delete(builtinHotkey.pluginName); plugins.delete(registerDefaults.pluginName); + plugins.delete(defaultContextMenu.pluginName); }; } @@ -111,6 +115,7 @@ const innerSetters = new InnerSetters(); const setters = new Setters(innerSetters); const material = new Material(editor); +const commonUI = new CommonUI(editor); editor.set('project', project); editor.set('setters' as any, setters); editor.set('material', material); @@ -138,8 +143,10 @@ const pluginContextApiAssembler: ILowCodePluginContextApiAssembler = { context.plugins = plugins; context.logger = new Logger({ level: 'warn', bizName: `plugin:${pluginName}` }); context.workspace = workspace; + context.commonUI = commonUI; context.registerLevel = IPublicEnumPluginRegisterLevel.Default; context.isPluginRegisteredInWorkspace = false; + editor.set('pluginContext', context); }, }; @@ -161,6 +168,7 @@ export { common, workspace, canvas, + commonUI, }; // declare this is open-source version export const isOpenSource = true; diff --git a/packages/engine/src/inner-plugins/default-context-menu.ts b/packages/engine/src/inner-plugins/default-context-menu.ts new file mode 100644 index 000000000..81978d920 --- /dev/null +++ b/packages/engine/src/inner-plugins/default-context-menu.ts @@ -0,0 +1,223 @@ +import { + IPublicEnumContextMenuType, + IPublicEnumDragObjectType, + IPublicEnumTransformStage, + IPublicModelNode, + IPublicModelPluginContext, + IPublicTypeDragNodeDataObject, + IPublicTypeNodeSchema, +} from '@alilc/lowcode-types'; +import { isProjectSchema } from '@alilc/lowcode-utils'; +import { Message } from '@alifd/next'; +import { intl } from '../locale'; + +function getNodesSchema(nodes: IPublicModelNode[]) { + const componentsTree = nodes.map((node) => node?.exportSchema(IPublicEnumTransformStage.Clone)); + const data = { type: 'nodeSchema', componentsMap: {}, componentsTree }; + return data; +} + +async function getClipboardText(): Promise<IPublicTypeNodeSchema[]> { + return new Promise((resolve, reject) => { + // 使用 Clipboard API 读取剪贴板内容 + navigator.clipboard.readText().then( + (text) => { + try { + const data = JSON.parse(text); + if (isProjectSchema(data)) { + resolve(data.componentsTree); + } else { + Message.error(intl('NotValidNodeData')); + reject( + new Error(intl('NotValidNodeData')), + ); + } + } catch (error) { + Message.error(intl('NotValidNodeData')); + reject(error); + } + }, + (err) => { + reject(err); + }, + ); + }); +} + +export const defaultContextMenu = (ctx: IPublicModelPluginContext) => { + const { material, canvas, common } = ctx; + const { clipboard } = canvas; + const { intl: utilsIntl } = common.utils; + + return { + init() { + material.addContextMenuOption({ + name: 'selectComponent', + title: intl('SelectComponents'), + condition: (nodes = []) => { + return nodes.length === 1; + }, + items: [ + { + name: 'nodeTree', + type: IPublicEnumContextMenuType.NODE_TREE, + }, + ], + }); + + material.addContextMenuOption({ + name: 'copyAndPaste', + title: intl('CopyAndPaste'), + disabled: (nodes = []) => { + return nodes?.filter((node) => !node?.canPerformAction('copy')).length > 0; + }, + condition: (nodes) => { + return nodes?.length === 1; + }, + action(nodes) { + const node = nodes?.[0]; + if (!node) { + return; + } + const { document: doc, parent, index } = node; + const data = getNodesSchema(nodes); + clipboard.setData(data); + + if (parent) { + const newNode = doc?.insertNode(parent, node, (index ?? 0) + 1, true); + newNode?.select(); + } + }, + }); + + material.addContextMenuOption({ + name: 'copy', + title: intl('Copy'), + disabled: (nodes = []) => { + return nodes?.filter((node) => !node?.canPerformAction('copy')).length > 0; + }, + condition(nodes = []) { + return nodes?.length > 0; + }, + action(nodes) { + if (!nodes || nodes.length < 1) { + return; + } + + const data = getNodesSchema(nodes); + clipboard.setData(data); + }, + }); + + material.addContextMenuOption({ + name: 'pasteToBottom', + title: intl('PasteToTheBottom'), + condition: (nodes) => { + return nodes?.length === 1; + }, + async action(nodes) { + if (!nodes || nodes.length < 1) { + return; + } + + const node = nodes[0]; + const { document: doc, parent, index } = node; + + try { + const nodeSchema = await getClipboardText(); + if (nodeSchema.length === 0) { + return; + } + if (parent) { + let canAddNodes = nodeSchema.filter((nodeSchema: IPublicTypeNodeSchema) => { + const dragNodeObject: IPublicTypeDragNodeDataObject = { + type: IPublicEnumDragObjectType.NodeData, + data: nodeSchema, + }; + return doc?.checkNesting(parent, dragNodeObject); + }); + if (canAddNodes.length === 0) { + Message.error(`${nodeSchema.map(d => utilsIntl(d.title || d.componentName)).join(',')}等组件无法放置到${utilsIntl(parent.title || parent.componentName as any)}内`); + return; + } + const nodes: IPublicModelNode[] = []; + canAddNodes.forEach((schema, schemaIndex) => { + const node = doc?.insertNode(parent, schema, (index ?? 0) + 1 + schemaIndex, true); + node && nodes.push(node); + }); + doc?.selection.selectAll(nodes.map((node) => node?.id)); + } + } catch (error) { + console.error(error); + } + }, + }); + + material.addContextMenuOption({ + name: 'pasteToInner', + title: intl('PasteToTheInside'), + condition: (nodes) => { + return nodes?.length === 1; + }, + disabled: (nodes = []) => { + // 获取粘贴数据 + const node = nodes?.[0]; + return !node.isContainerNode; + }, + async action(nodes) { + const node = nodes?.[0]; + if (!node) { + return; + } + const { document: doc } = node; + + try { + const nodeSchema = await getClipboardText(); + const index = node.children?.size || 0; + if (nodeSchema.length === 0) { + return; + } + let canAddNodes = nodeSchema.filter((nodeSchema: IPublicTypeNodeSchema) => { + const dragNodeObject: IPublicTypeDragNodeDataObject = { + type: IPublicEnumDragObjectType.NodeData, + data: nodeSchema, + }; + return doc?.checkNesting(node, dragNodeObject); + }); + if (canAddNodes.length === 0) { + Message.error(`${nodeSchema.map(d => utilsIntl(d.title || d.componentName)).join(',')}等组件无法放置到${utilsIntl(node.title || node.componentName as any)}内`); + return; + } + + const nodes: IPublicModelNode[] = []; + nodeSchema.forEach((schema, schemaIndex) => { + const newNode = doc?.insertNode(node, schema, (index ?? 0) + 1 + schemaIndex, true); + newNode && nodes.push(newNode); + }); + doc?.selection.selectAll(nodes.map((node) => node?.id)); + } catch (error) { + console.error(error); + } + }, + }); + + material.addContextMenuOption({ + name: 'delete', + title: intl('Delete'), + disabled(nodes = []) { + return nodes?.filter((node) => !node?.canPerformAction('remove')).length > 0; + }, + condition(nodes = []) { + return nodes.length > 0; + }, + action(nodes) { + nodes?.forEach((node) => { + node.remove(); + }); + }, + }); + }, + }; +}; + +defaultContextMenu.pluginName = '___default_context_menu___'; diff --git a/packages/engine/src/locale/en-US.json b/packages/engine/src/locale/en-US.json new file mode 100644 index 000000000..e93160707 --- /dev/null +++ b/packages/engine/src/locale/en-US.json @@ -0,0 +1,9 @@ +{ + "NotValidNodeData": "Not valid node data", + "SelectComponents": "Select components", + "CopyAndPaste": "Copy and Paste", + "Copy": "Copy", + "PasteToTheBottom": "Paste to the bottom", + "PasteToTheInside": "Paste to the inside", + "Delete": "Delete" +} diff --git a/packages/engine/src/locale/index.ts b/packages/engine/src/locale/index.ts new file mode 100644 index 000000000..ca89840b0 --- /dev/null +++ b/packages/engine/src/locale/index.ts @@ -0,0 +1,14 @@ +import { createIntl } from '@alilc/lowcode-editor-core'; +import enUS from './en-US.json'; +import zhCN from './zh-CN.json'; + +const { intl, getLocale } = createIntl?.({ + 'en-US': enUS, + 'zh-CN': zhCN, +}) || { + intl: (id) => { + return zhCN[id]; + }, +}; + +export { intl, enUS, zhCN, getLocale }; diff --git a/packages/engine/src/locale/zh-CN.json b/packages/engine/src/locale/zh-CN.json new file mode 100644 index 000000000..9b68b7149 --- /dev/null +++ b/packages/engine/src/locale/zh-CN.json @@ -0,0 +1,9 @@ +{ + "NotValidNodeData": "不是有效的节点数据", + "SelectComponents": "选择组件", + "CopyAndPaste": "复制", + "Copy": "拷贝", + "PasteToTheBottom": "粘贴至下方", + "PasteToTheInside": "粘贴至内部", + "Delete": "删除" +} diff --git a/packages/ignitor/build.json b/packages/ignitor/build.json index 218373247..f1956cf52 100644 --- a/packages/ignitor/build.json +++ b/packages/ignitor/build.json @@ -1,8 +1,7 @@ { "entry": { "AliLowCodeEngine": "../engine/src/index.ts", - "ReactSimulatorRenderer": "../react-simulator-renderer/src/index.ts", - "RaxSimulatorRenderer": "../rax-simulator-renderer/src/index.ts" + "ReactSimulatorRenderer": "../react-simulator-renderer/src/index.ts" }, "vendor": false, "devServer": { diff --git a/packages/ignitor/package.json b/packages/ignitor/package.json index f131dc14c..cb87c297f 100644 --- a/packages/ignitor/package.json +++ b/packages/ignitor/package.json @@ -1,6 +1,6 @@ { "name": "@alilc/lowcode-ignitor", - "version": "1.2.5", + "version": "1.3.0", "description": "点火器,bootstrap lce project", "main": "lib/index.js", "private": true, diff --git a/packages/plugin-designer/package.json b/packages/plugin-designer/package.json index d764f7d9e..a2c0fcf19 100644 --- a/packages/plugin-designer/package.json +++ b/packages/plugin-designer/package.json @@ -1,6 +1,6 @@ { "name": "@alilc/lowcode-plugin-designer", - "version": "1.2.5", + "version": "1.3.0", "description": "alibaba lowcode editor designer plugin", "files": [ "es", @@ -18,9 +18,9 @@ ], "author": "xiayang.xy", "dependencies": { - "@alilc/lowcode-designer": "1.2.5", - "@alilc/lowcode-editor-core": "1.2.5", - "@alilc/lowcode-utils": "1.2.5", + "@alilc/lowcode-designer": "1.3.0", + "@alilc/lowcode-editor-core": "1.3.0", + "@alilc/lowcode-utils": "1.3.0", "react": "^16.8.1", "react-dom": "^16.8.1" }, diff --git a/packages/plugin-outline-pane/package.json b/packages/plugin-outline-pane/package.json index f864356fc..073830381 100644 --- a/packages/plugin-outline-pane/package.json +++ b/packages/plugin-outline-pane/package.json @@ -1,6 +1,6 @@ { "name": "@alilc/lowcode-plugin-outline-pane", - "version": "1.2.5", + "version": "1.3.0", "description": "Outline pane for Ali lowCode engine", "files": [ "es", @@ -13,8 +13,8 @@ }, "dependencies": { "@alifd/next": "^1.19.16", - "@alilc/lowcode-types": "1.2.5", - "@alilc/lowcode-utils": "1.2.5", + "@alilc/lowcode-types": "1.3.0", + "@alilc/lowcode-utils": "1.3.0", "classnames": "^2.2.6", "react": "^16", "react-dom": "^16.7.0", diff --git a/packages/plugin-outline-pane/src/views/filter-tree.ts b/packages/plugin-outline-pane/src/views/filter-tree.ts index 2a07e15c6..793aa03cc 100644 --- a/packages/plugin-outline-pane/src/views/filter-tree.ts +++ b/packages/plugin-outline-pane/src/views/filter-tree.ts @@ -77,6 +77,11 @@ export const matchTreeNode = ( return matchTreeNode(childNode, keywords, filterOps); }).find(Boolean); + // 如果命中了子节点,需要将该节点展开 + if (matchChild && treeNode.expandable) { + treeNode.setExpanded(true); + } + treeNode.setFilterReult({ filterWorking: true, matchChild, diff --git a/packages/plugin-outline-pane/src/views/style.less b/packages/plugin-outline-pane/src/views/style.less index 8521883b4..8a38ba749 100644 --- a/packages/plugin-outline-pane/src/views/style.less +++ b/packages/plugin-outline-pane/src/views/style.less @@ -356,7 +356,7 @@ // 选中节点处理 &.selected { & > .tree-node-title { - background: var(--color-block-background-shallow); + background: var(--color-block-background-light); } & > .tree-node-branches::before { diff --git a/packages/plugin-outline-pane/src/views/tree-node.tsx b/packages/plugin-outline-pane/src/views/tree-node.tsx index be6e6ae2c..11bd95d12 100644 --- a/packages/plugin-outline-pane/src/views/tree-node.tsx +++ b/packages/plugin-outline-pane/src/views/tree-node.tsx @@ -34,7 +34,7 @@ class ModalTreeNodeView extends PureComponent<{ } componentDidMount(): void { - const rootTreeNode = this.rootTreeNode; + const { rootTreeNode } = this; rootTreeNode.onExpandableChanged(() => { this.setState({ treeChildren: rootTreeNode.children, @@ -53,7 +53,7 @@ class ModalTreeNodeView extends PureComponent<{ } render() { - const rootTreeNode = this.rootTreeNode; + const { rootTreeNode } = this; const { expanded } = rootTreeNode; const hasVisibleModalNode = !!this.modalNodesManager?.getVisibleModalNode(); @@ -98,6 +98,9 @@ export default class TreeNodeView extends PureComponent<{ conditionFlow: boolean; expandable: boolean; treeChildren: TreeNode[] | null; + filterWorking: boolean; + matchChild: boolean; + matchSelf: boolean; } = { expanded: false, selected: false, @@ -110,6 +113,9 @@ export default class TreeNodeView extends PureComponent<{ conditionFlow: false, expandable: false, treeChildren: [], + filterWorking: false, + matchChild: false, + matchSelf: false, }; eventOffCallbacks: Array<IPublicTypeDisposable | undefined> = []; @@ -154,6 +160,10 @@ export default class TreeNodeView extends PureComponent<{ treeChildren: treeNode.children, }); }); + treeNode.onFilterResultChanged(() => { + const { filterWorking: newFilterWorking, matchChild: newMatchChild, matchSelf: newMatchSelf } = treeNode.filterReult; + this.setState({ filterWorking: newFilterWorking, matchChild: newMatchChild, matchSelf: newMatchSelf }); + }); this.eventOffCallbacks.push( doc?.onDropLocationChanged(() => { this.setState({ @@ -216,7 +226,7 @@ export default class TreeNodeView extends PureComponent<{ let shouldShowModalTreeNode: boolean = this.shouldShowModalTreeNode(); // filter 处理 - const { filterWorking, matchChild, matchSelf } = treeNode.filterReult; + const { filterWorking, matchChild, matchSelf } = this.state; if (!isRootNode && filterWorking && !matchChild && !matchSelf) { // 条件过滤生效时,如果未命中本节点或子节点,则不展示该节点 // 根节点始终展示 diff --git a/packages/plugin-outline-pane/src/views/tree-title.tsx b/packages/plugin-outline-pane/src/views/tree-title.tsx index c8c0e75b0..f822bd644 100644 --- a/packages/plugin-outline-pane/src/views/tree-title.tsx +++ b/packages/plugin-outline-pane/src/views/tree-title.tsx @@ -29,9 +29,15 @@ export default class TreeTitle extends PureComponent<{ title: string; condition?: boolean; visible?: boolean; + filterWorking: boolean; + keywords: string; + matchSelf: boolean; } = { editing: false, title: '', + filterWorking: false, + keywords: '', + matchSelf: false, }; private lastInput?: HTMLInputElement; @@ -100,6 +106,10 @@ export default class TreeTitle extends PureComponent<{ visible: !hidden, }); }); + treeNode.onFilterResultChanged(() => { + const { filterWorking: newFilterWorking, keywords: newKeywords, matchSelf: newMatchSelf } = treeNode.filterReult; + this.setState({ filterWorking: newFilterWorking, keywords: newKeywords, matchSelf: newMatchSelf }); + }); } deleteClick = () => { const { treeNode } = this.props; @@ -109,7 +119,7 @@ export default class TreeTitle extends PureComponent<{ render() { const { treeNode, isModal } = this.props; const { pluginContext } = treeNode; - const { editing } = this.state; + const { editing, filterWorking, matchSelf, keywords } = this.state; const isCNode = !treeNode.isRoot(); const { node } = treeNode; const { componentMeta } = node; @@ -125,11 +135,9 @@ export default class TreeTitle extends PureComponent<{ marginLeft: -indent, }; } - const { filterWorking, matchSelf, keywords } = treeNode.filterReult; const Extra = pluginContext.extraTitle; const { intlNode, common, config } = pluginContext; - const Tip = common.editorCabin.Tip; - const Title = common.editorCabin.Title; + const { Tip, Title } = common.editorCabin; const couldHide = availableActions.includes('hide'); const couldLock = availableActions.includes('lock'); const couldUnlock = availableActions.includes('unlock'); @@ -253,7 +261,7 @@ class RenameBtn extends PureComponent<{ }> { render() { const { intl, common } = this.props.treeNode.pluginContext; - const Tip = common.editorCabin.Tip; + const { Tip } = common.editorCabin; return ( <div className="tree-node-rename-btn" @@ -274,7 +282,7 @@ class LockBtn extends PureComponent<{ render() { const { treeNode, locked } = this.props; const { intl, common } = this.props.treeNode.pluginContext; - const Tip = common.editorCabin.Tip; + const { Tip } = common.editorCabin; return ( <div className="tree-node-lock-btn" @@ -300,7 +308,7 @@ class HideBtn extends PureComponent<{ render() { const { treeNode, hidden } = this.props; const { intl, common } = treeNode.pluginContext; - const Tip = common.editorCabin.Tip; + const { Tip } = common.editorCabin; return ( <div className="tree-node-hide-btn" diff --git a/packages/rax-renderer/README.md b/packages/rax-renderer/README.md deleted file mode 100644 index 7b430de63..000000000 --- a/packages/rax-renderer/README.md +++ /dev/null @@ -1,49 +0,0 @@ -# Rax Renderer - -Rax 渲染模块。 - -## 安装 - -``` -$ npm install @alilc/lowcode-rax-renderer --save -``` - -## 使用 - -```js -import { createElement, render } from 'rax'; -import DriverUniversal from 'driver-universal'; -import RaxRenderer from '@ali/lowcode-rax-renderer'; - -const components = { - View, - Text -}; - -const schema = { - componentName: 'Page', - fileName: 'home', - children: [ - { - componentName: 'View', - children: [ - { - componentName: 'Text', - props: { - type: 'primary' - }, - children: ['Welcome to Your Rax App'] - } - ] - } - ] -}; - -render( - <RaxRenderer - schema={schema} - components={components} - />, - document.getElementById('root'), { driver: DriverUniversal } -); -``` diff --git a/packages/rax-renderer/build.json b/packages/rax-renderer/build.json deleted file mode 100644 index 3edf14380..000000000 --- a/packages/rax-renderer/build.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "plugins": [ - [ - "build-plugin-rax-component", - { - "type": "rax", - "targets": ["web"] - } - ] - ] -} diff --git a/packages/rax-renderer/demo/index.jsx b/packages/rax-renderer/demo/index.jsx deleted file mode 100644 index cfcae2a20..000000000 --- a/packages/rax-renderer/demo/index.jsx +++ /dev/null @@ -1,35 +0,0 @@ -import { createElement, render } from 'rax'; -import DriverUniversal from 'driver-universal'; -import View from 'rax-view'; -import Text from 'rax-text'; -import { Engine } from '../src/index'; - -const components = { - View, - Text, -}; - -const schema = { - componentName: 'Page', - fileName: 'home', - props: {}, - children: [ - { - componentName: 'View', - props: {}, - children: [ - { - componentName: 'Text', - props: { - type: 'primary', - }, - children: ['Welcome to Your Rax App!'], - }, - ], - }, - ], -}; - -render(<Engine schema={schema} components={components} />, document.getElementById('root'), { - driver: DriverUniversal, -}); diff --git a/packages/rax-renderer/demo/miniapp/app.js b/packages/rax-renderer/demo/miniapp/app.js deleted file mode 100644 index 348293551..000000000 --- a/packages/rax-renderer/demo/miniapp/app.js +++ /dev/null @@ -1 +0,0 @@ -App({}); diff --git a/packages/rax-renderer/demo/miniapp/app.json b/packages/rax-renderer/demo/miniapp/app.json deleted file mode 100644 index 94127c774..000000000 --- a/packages/rax-renderer/demo/miniapp/app.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "pages": ["pages/index"], - "window": { - "defaultTitle": "demo" - } -} diff --git a/packages/rax-renderer/demo/miniapp/pages/index.acss b/packages/rax-renderer/demo/miniapp/pages/index.acss deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/rax-renderer/demo/miniapp/pages/index.axml b/packages/rax-renderer/demo/miniapp/pages/index.axml deleted file mode 100644 index 41b536b4c..000000000 --- a/packages/rax-renderer/demo/miniapp/pages/index.axml +++ /dev/null @@ -1 +0,0 @@ -<my-component></my-component> diff --git a/packages/rax-renderer/demo/miniapp/pages/index.js b/packages/rax-renderer/demo/miniapp/pages/index.js deleted file mode 100644 index 687d87e19..000000000 --- a/packages/rax-renderer/demo/miniapp/pages/index.js +++ /dev/null @@ -1,4 +0,0 @@ -Page({ - onLoad() {}, - onShow() {}, -}); diff --git a/packages/rax-renderer/demo/miniapp/pages/index.json b/packages/rax-renderer/demo/miniapp/pages/index.json deleted file mode 100644 index 89b15c54c..000000000 --- a/packages/rax-renderer/demo/miniapp/pages/index.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "defaultTitle": "Miniapp Rax Text demo", - "usingComponents": { - "my-component": "../components/Target/index" - } -} diff --git a/packages/rax-renderer/demo/wechat-miniprogram/app.js b/packages/rax-renderer/demo/wechat-miniprogram/app.js deleted file mode 100644 index 348293551..000000000 --- a/packages/rax-renderer/demo/wechat-miniprogram/app.js +++ /dev/null @@ -1 +0,0 @@ -App({}); diff --git a/packages/rax-renderer/demo/wechat-miniprogram/app.json b/packages/rax-renderer/demo/wechat-miniprogram/app.json deleted file mode 100644 index be00ced60..000000000 --- a/packages/rax-renderer/demo/wechat-miniprogram/app.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "pages": ["pages/index"], - "window": { - "title": "demo" - } -} diff --git a/packages/rax-renderer/demo/wechat-miniprogram/pages/index.js b/packages/rax-renderer/demo/wechat-miniprogram/pages/index.js deleted file mode 100644 index 687d87e19..000000000 --- a/packages/rax-renderer/demo/wechat-miniprogram/pages/index.js +++ /dev/null @@ -1,4 +0,0 @@ -Page({ - onLoad() {}, - onShow() {}, -}); diff --git a/packages/rax-renderer/demo/wechat-miniprogram/pages/index.json b/packages/rax-renderer/demo/wechat-miniprogram/pages/index.json deleted file mode 100644 index 9448c84ea..000000000 --- a/packages/rax-renderer/demo/wechat-miniprogram/pages/index.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "title": "Wechat MiniProgram Rax Text demo", - "usingComponents": { - "my-component": "../components/Target/index" - } -} diff --git a/packages/rax-renderer/demo/wechat-miniprogram/pages/index.wxml b/packages/rax-renderer/demo/wechat-miniprogram/pages/index.wxml deleted file mode 100644 index 41b536b4c..000000000 --- a/packages/rax-renderer/demo/wechat-miniprogram/pages/index.wxml +++ /dev/null @@ -1 +0,0 @@ -<my-component></my-component> diff --git a/packages/rax-renderer/demo/wechat-miniprogram/pages/index.wxss b/packages/rax-renderer/demo/wechat-miniprogram/pages/index.wxss deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/rax-renderer/package.json b/packages/rax-renderer/package.json deleted file mode 100644 index 8173d6e4e..000000000 --- a/packages/rax-renderer/package.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "name": "@alilc/lowcode-rax-renderer", - "version": "1.2.5", - "description": "Rax renderer for Ali lowCode engine", - "main": "lib/index.js", - "module": "es/index.js", - "miniappConfig": { - "main": "lib/miniapp/index", - "main:wechat": "lib/wechat-miniprogram/index" - }, - "files": [ - "dist", - "es", - "lib" - ], - "keywords": [ - "low-code", - "lowcode", - "Rax" - ], - "engines": { - "npm": ">=3.0.0" - }, - "peerDependencies": { - "prop-types": "^15.7.2", - "rax": "^1.1.0" - }, - "scripts": { - "start": "build-scripts start", - "build": "build-scripts build" - }, - "dependencies": { - "@alilc/lowcode-renderer-core": "1.2.5", - "@alilc/lowcode-utils": "1.2.5", - "rax-find-dom-node": "^1.0.1" - }, - "devDependencies": { - "@alib/build-scripts": "^0.1.0", - "build-plugin-rax-component": "^0.2.11", - "driver-universal": "^3.1.3" - }, - "publishConfig": { - "access": "public", - "registry": "https://registry.npmjs.org/" - }, - "repository": { - "type": "http", - "url": "https://github.com/alibaba/lowcode-engine/tree/main/packages/rax-renderer" - }, - "license": "MIT", - "homepage": "https://github.com/alibaba/lowcode-engine/#readme", - "gitHead": "2669f179e6f899d395ce1942d0fe04f9c5ed48a6", - "bugs": "https://github.com/alibaba/lowcode-engine/issues" -} diff --git a/packages/rax-renderer/src/hoc/compFactory.tsx b/packages/rax-renderer/src/hoc/compFactory.tsx deleted file mode 100644 index 1c821ab6d..000000000 --- a/packages/rax-renderer/src/hoc/compFactory.tsx +++ /dev/null @@ -1,82 +0,0 @@ -// @ts-nocheck - -import { Component, forwardRef } from 'rax'; -import PropTypes from 'prop-types'; -import { AppHelper } from '@alilc/lowcode-utils'; -import { utils, contextFactory } from '@alilc/lowcode-renderer-core'; -import componentRendererFactory from '../renderer/component'; -import blockRendererFactory from '../renderer/block'; - -const { forEach, isFileSchema } = utils; - -export default function compFactory(schema, components = {}, componentsMap = {}, config = {}) { - // 自定义组件需要有自己独立的appHelper - const appHelper = new AppHelper(config); - const CompRenderer = componentRendererFactory(); - const BlockRenderer = blockRendererFactory(); - const AppContext = contextFactory(); - - class LNCompView extends Component { - static displayName = 'LceCompFactory'; - - static version = config.version || '0.0.0'; - - static contextType = AppContext; - - static propTypes = { - forwardedRef: PropTypes.func, - }; - - render() { - if (!schema || schema.componentName !== 'Component' || !isFileSchema(schema)) { - console.warn('自定义组件模型结构异常!'); - return null; - } - const { forwardedRef, ...otherProps } = this.props; - // 低代码组件透传应用上下文 - const ctx = ['utils', 'constants', 'history', 'location', 'match']; - ctx.forEach(key => { - if (!appHelper[key] && this.context?.appHelper && this.context?.appHelper[key]) { - appHelper.set(key, this.context.appHelper[key]); - } - }); - // 支持通过context透传国际化配置 - const localeProps = {}; - const { locale, messages } = this.context; - if (locale && messages && messages[schema.fileName]) { - localeProps.locale = locale; - localeProps.messages = messages[schema.fileName]; - } - const props = { - ...schema.defaultProps, - ...localeProps, - ...otherProps, - __schema: schema, - ref: forwardedRef, - }; - - return ( - <AppContext.Consumer> - {context => { - this.context = context; - return ( - <CompRenderer - {...props} - __appHelper={appHelper} - __components={{ ...components, Component: CompRenderer, Block: BlockRenderer }} - __componentsMap={componentsMap} - /> - ); - }} - </AppContext.Consumer> - ); - } - } - - const ResComp = forwardRef((props, ref) => <LNCompView {...props} forwardedRef={ref} />); - forEach(schema.static, (val, key) => { - ResComp[key] = val; - }); - ResComp.version = config.version || '0.0.0'; - return ResComp; -} diff --git a/packages/rax-renderer/src/index.ts b/packages/rax-renderer/src/index.ts deleted file mode 100644 index 1947aa5ae..000000000 --- a/packages/rax-renderer/src/index.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Component, PureComponent, createElement, createContext, forwardRef } from 'rax'; -import findDOMNode from 'rax-find-dom-node'; -import { - adapter, - addonRendererFactory, - tempRendererFactory, - rendererFactory, -} from '@alilc/lowcode-renderer-core'; -import pageRendererFactory from './renderer/page'; -import componentRendererFactory from './renderer/component'; -import blockRendererFactory from './renderer/block'; -import CompFactory from './hoc/compFactory'; - -adapter.setRuntime({ - Component, - PureComponent, - createContext, - createElement, - forwardRef, - findDOMNode, -}); - -adapter.setRenderers({ - PageRenderer: pageRendererFactory(), - ComponentRenderer: componentRendererFactory(), - BlockRenderer: blockRendererFactory(), - AddonRenderer: addonRendererFactory(), - TempRenderer: tempRendererFactory(), -}); - -function factory() { - const Renderer = rendererFactory(); - return class extends Renderer { - constructor(props: any, context: any) { - super(props, context); - } - - isValidComponent(obj: any) { - return obj?.prototype?.setState || obj?.prototype instanceof Component; - } - }; -} - -const RaxRenderer: any = factory(); -const Engine: any = RaxRenderer; - -export { - Engine, - CompFactory, -}; - -export default RaxRenderer; diff --git a/packages/rax-renderer/src/renderer/block.tsx b/packages/rax-renderer/src/renderer/block.tsx deleted file mode 100644 index 8fa4a2731..000000000 --- a/packages/rax-renderer/src/renderer/block.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { blockRendererFactory, types } from '@alilc/lowcode-renderer-core'; - -const raxBlockRendererFactory: () => any = () => { - const OriginBlock = blockRendererFactory(); - return class BlockRenderer extends OriginBlock { - render() { - // @ts-ignore - const that: types.IRenderer = this; - const { __schema, __components } = that.props; - if (that.__checkSchema(__schema)) { - return '区块 schema 结构异常!'; - } - that.__debug(`render - ${__schema.fileName}`); - - const children = ((context) => { - that.context = context; - that.__generateCtx({}); - that.__render(); - return that.__renderComp((__components as any)?.Block, { blockContext: that }); - }); - return that.__renderContextConsumer(children); - } - }; -}; -export default raxBlockRendererFactory; diff --git a/packages/rax-renderer/src/renderer/component.tsx b/packages/rax-renderer/src/renderer/component.tsx deleted file mode 100644 index 9943b3c2f..000000000 --- a/packages/rax-renderer/src/renderer/component.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { componentRendererFactory, types } from '@alilc/lowcode-renderer-core'; - -const raxComponentRendererFactory: () => any = () => { - const OriginComponent = componentRendererFactory(); - return class ComponentRenderer extends OriginComponent { - render() { - // @ts-ignore - const that: types.IRenderer = this; - const { __schema, __components } = that.props; - if (that.__checkSchema(__schema)) { - return '自定义组件 schema 结构异常!'; - } - that.__debug(`render - ${__schema.fileName}`); - - const { noContainer } = that.__parseData(__schema.props); - - const children = ((context) => { - that.context = context; - that.__generateCtx({ component: that }); - that.__render(); - // 传 null,使用内置的 div 来渲染,解决在页面中渲染 vc-component 报错的问题 - return that.__renderComp(null, { - compContext: that, - blockContext: that, - }); - }); - const content = that.__renderContextConsumer(children); - - if (noContainer) { - return content; - } - - return that.__renderContent(content); - } - }; -}; -export default raxComponentRendererFactory; diff --git a/packages/rax-renderer/src/renderer/page.tsx b/packages/rax-renderer/src/renderer/page.tsx deleted file mode 100644 index f6ebd3f7c..000000000 --- a/packages/rax-renderer/src/renderer/page.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { pageRendererFactory, types } from '@alilc/lowcode-renderer-core'; - -const raxPageRendererFactory: () => any = () => { - const OriginPage = pageRendererFactory(); - return class PageRenderer extends OriginPage { - async componentDidUpdate() { - // @ts-ignore - super.componentDidUpdate(...arguments); - } - - render() { - // @ts-ignore - const that: types.IRenderer = this; - const { __schema, __components } = that.props; - if (that.__checkSchema(__schema)) { - return '页面 schema 结构异常!'; - } - that.__debug(`render - ${__schema?.fileName}`); - - const { Page } = __components as any; - if (Page) { - const children = ((context) => { - that.context = context; - that.__render(); - return that.__renderComp(Page, { pageContext: that }); - }); - return that.__renderContextConsumer(children); - } - - return that.__renderContent(that.__renderContextConsumer((context) => { - that.context = context; - return that.__renderContextProvider({ pageContext: that }); - })); - } - }; -}; - -export default raxPageRendererFactory; \ No newline at end of file diff --git a/packages/rax-renderer/tsconfig.json b/packages/rax-renderer/tsconfig.json deleted file mode 100644 index 7e264d1f0..000000000 --- a/packages/rax-renderer/tsconfig.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "compilerOptions": { - "lib": ["es2015", "dom"], - "target": "esnext", - "module": "esnext", - "moduleResolution": "node", - "strict": false, - "strictPropertyInitialization": false, - "allowSyntheticDefaultImports": true, - "esModuleInterop": true, - "jsx": "react", - "jsxFactory": "createElement", - "importHelpers": true, - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "sourceMap": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "outDir": "lib" - }, - "exclude": ["test", "lib", "es", "node_modules"], - "include": [ - "src" - ] -} diff --git a/packages/rax-simulator-renderer/.babelrc b/packages/rax-simulator-renderer/.babelrc deleted file mode 100644 index e0e2e5f34..000000000 --- a/packages/rax-simulator-renderer/.babelrc +++ /dev/null @@ -1,9 +0,0 @@ -{ - "plugins": [ - ["@babel/plugin-transform-react-jsx", { - "pragma": "createElement", // default pragma is React.createElement - "pragmaFrag": "createFragment", // default is React.Fragment - "throwIfNamespace": false // defaults to true - }] - ] -} diff --git a/packages/rax-simulator-renderer/babel.config.js b/packages/rax-simulator-renderer/babel.config.js deleted file mode 100644 index c5986f2bc..000000000 --- a/packages/rax-simulator-renderer/babel.config.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('../../babel.config'); \ No newline at end of file diff --git a/packages/rax-simulator-renderer/build.json b/packages/rax-simulator-renderer/build.json deleted file mode 100644 index e7ae1dcf7..000000000 --- a/packages/rax-simulator-renderer/build.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "plugins": ["@alilc/build-plugin-lce", "./build.plugin.js"] -} diff --git a/packages/rax-simulator-renderer/build.plugin.js b/packages/rax-simulator-renderer/build.plugin.js deleted file mode 100644 index d613f1f56..000000000 --- a/packages/rax-simulator-renderer/build.plugin.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = ({ onGetWebpackConfig }) => { - onGetWebpackConfig((config) => { - config.performance.hints(false); - }); -}; diff --git a/packages/rax-simulator-renderer/build.umd.json b/packages/rax-simulator-renderer/build.umd.json deleted file mode 100644 index 833c92b24..000000000 --- a/packages/rax-simulator-renderer/build.umd.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "entry": { - "rax-simulator-renderer": "src/index" - }, - "sourceMap": true, - "library": "___RaxSimulatorRenderer___", - "libraryTarget": "umd", - "externals": { - "react": "var window.React", - "react-dom": "var window.ReactDOM", - "prop-types": "var window.PropTypes", - "@alifd/next": "var Next", - "@alilc/lowcode-engine-ext": "var window.AliLowCodeEngineExt", - "rax": "var window.Rax", - "moment": "var moment", - "lodash": "var _" - }, - "polyfill": false, - "outputDir": "dist", - "vendor": false, - "ignoreHtmlTemplate": true, - "plugins": [ - "build-plugin-react-app", - [ - "build-plugin-fusion", - { - "externalNext": "umd" - } - ], - [ - "build-plugin-moment-locales", - { - "locales": ["zh-cn"] - } - ], - "./build.plugin.js" - ] -} diff --git a/packages/rax-simulator-renderer/package.json b/packages/rax-simulator-renderer/package.json deleted file mode 100644 index f264380ec..000000000 --- a/packages/rax-simulator-renderer/package.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "name": "@alilc/lowcode-rax-simulator-renderer", - "version": "1.2.5", - "description": "rax simulator renderer for alibaba lowcode designer", - "main": "lib/index.js", - "module": "es/index.js", - "license": "MIT", - "files": [ - "dist" - ], - "scripts": { - "build": "NODE_OPTIONS=--max_old_space_size=8192 build-scripts build", - "build:umd": "build-scripts build --config build.umd.json" - }, - "dependencies": { - "@alilc/lowcode-designer": "1.2.5", - "@alilc/lowcode-rax-renderer": "1.2.5", - "@alilc/lowcode-types": "1.2.5", - "@alilc/lowcode-utils": "1.2.5", - "classnames": "^2.2.6", - "driver-universal": "^3.1.3", - "history": "^5.0.0", - "lodash": "^4.17.19", - "mobx": "^6.3.0", - "mobx-react": "^7.2.0", - "path-to-regexp": "3.2.0", - "rax-find-dom-node": "^1.0.0", - "react": "^16", - "react-dom": "^16.7.0" - }, - "devDependencies": { - "@alib/build-scripts": "^0.1.18", - "@babel/plugin-transform-react-jsx": "^7.10.4", - "@types/classnames": "^2.2.7", - "@types/node": "^13.7.1", - "@types/rax": "^1.0.0", - "@types/react": "^16", - "@types/react-dom": "^16", - "build-plugin-rax-component": "^0.2.11" - }, - "peerDependencies": { - "rax": "^1.1.0" - }, - "publishConfig": { - "access": "public", - "registry": "https://registry.npmjs.org/" - }, - "repository": { - "type": "http", - "url": "https://github.com/alibaba/lowcode-engine/tree/main/packages/rax-simulator-renderer" - }, - "homepage": "https://github.com/alibaba/lowcode-engine/#readme", - "gitHead": "2669f179e6f899d395ce1942d0fe04f9c5ed48a6", - "bugs": "https://github.com/alibaba/lowcode-engine/issues" -} diff --git a/packages/rax-simulator-renderer/src/builtin-components/UnusualComponent/index.less b/packages/rax-simulator-renderer/src/builtin-components/UnusualComponent/index.less deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/rax-simulator-renderer/src/builtin-components/UnusualComponent/index.tsx b/packages/rax-simulator-renderer/src/builtin-components/UnusualComponent/index.tsx deleted file mode 100644 index 6608942c4..000000000 --- a/packages/rax-simulator-renderer/src/builtin-components/UnusualComponent/index.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { Component } from 'rax'; -import lg from '@ali/vu-logger'; - -import './index.less'; - -export class UnknownComponent extends Component { - props: { - _componentName: string; - }; - - render() { - lg.log('ERROR_NO_COMPONENT_VIEW'); - lg.error('Error component information:', this.props); - return <div className="engine-unknow-component">组件 {this.props._componentName} 无视图,请打开控制台排查</div>; - } -} - -export class FaultComponent extends Component { - props: { - _componentName: string; - }; - - render() { - return <div className="engine-fault-component">组件 {this.props._componentName} 渲染错误,请打开控制台排查</div>; - } -} - -export class HiddenComponent extends Component { - render() { - return <div className="engine-hidden-component">在本页面不显示</div>; - } -} - -export default { FaultComponent, HiddenComponent, UnknownComponent }; diff --git a/packages/rax-simulator-renderer/src/builtin-components/leaf.tsx b/packages/rax-simulator-renderer/src/builtin-components/leaf.tsx deleted file mode 100644 index 5276b0018..000000000 --- a/packages/rax-simulator-renderer/src/builtin-components/leaf.tsx +++ /dev/null @@ -1,251 +0,0 @@ -import { Component } from 'rax'; - -class Leaf extends Component { - static displayName = 'Leaf'; - - static componentMetadata = { - componentName: 'Leaf', - configure: { - props: [{ - name: 'children', - setter: 'StringSetter', - }], - // events/className/style/general/directives - supports: false, - }, - }; - - render() { - const { children } = this.props; - return children; - } -} - -export default Leaf; - -// import { Component, createElement } from 'rax'; -// import findDOMNode from 'rax-find-dom-node'; -// import { each, get, omit } from 'lodash'; -// import { getView, setNativeNode, createNodeStyleSheet } from '../renderUtils'; - -// import { FaultComponent, HiddenComponent, UnknownComponent } from '../UnusualComponent'; - -// export interface ILeaf { -// leaf: any; -// } -// export default class Leaf extends Component<ILeaf, {}> { -// static displayName = 'Leaf'; - -// state = { -// hasError: false, -// }; - -// willDetach: any[]; - -// styleSheet: any; - -// context: any; -// refs: any; - -// componentWillMount() { -// const { leaf } = this.props; -// this.willDetach = [ -// leaf.onPropsChange(() => { -// // 强制刷新 -// this.setState(this.state); -// }), -// leaf.onChildrenChange(() => { -// // 强制刷新 -// this.setState(this.state); -// }), -// leaf.onStatusChange((status: { dropping: boolean }, field: string) => { -// // console.log({...status}, field) -// if (status.dropping !== false) { -// // 当 dropping 为 Insertion 对象时,强制渲染会出错,原因待查 -// return; -// } -// if (field === 'dragging' || field === 'dropping' || field === 'pseudo' || field === 'visibility') { -// // 强制刷新 -// this.setState(this.state); -// } -// }), -// ]; - -// /** -// * while props replaced -// * bind the new event on it -// */ -// leaf.onPropsReplace(() => { -// this.willDetach[0](); -// this.willDetach[0] = leaf.onPropsChange(() => { -// // 强制刷新 -// this.setState(this.state); -// }); -// }); -// } - -// componentDidMount() { -// this.modifyDOM(); -// } - -// shouldComponentUpdate() { -// // forceUpdate 的替代方案 -// return true; -// // const pageCanRefresh = this.leaf.getPage().canRefresh(); -// // if (pageCanRefresh) { -// // return pageCanRefresh; -// // } -// // const getExtProps = obj => { -// // const { leaf, ...props } = obj; -// // return props; -// // }; -// // return !shallowEqual(getExtProps(this.props), getExtProps(nextProps)); -// } - -// componentDidUpdate() { -// this.modifyDOM(); -// } - -// componentWillUnmount() { -// if (this.willDetach) { -// this.willDetach.forEach((off) => off()); -// } -// setNativeNode(this.props.leaf, null); -// } - -// componentDidCatch() { -// this.setState({ hasError: true }, () => { -// console.log('error'); -// }); -// } - -// modifyDOM() { -// const shell = findDOMNode(this); -// const { leaf } = this.props; -// // 与 React 不同,rax 的 findDOMNode 找不到节点时, -// // shell 会是 <!-- empty -->,而不是 null, -// // 所以这里进行是否为注释的判断 -// if (shell && shell.nodeType !== window.Node.COMMENT_NODE) { -// setNativeNode(leaf, shell); -// if (leaf.getStatus('dragging')) { -// get(shell, 'classList').add('engine-dragging'); -// } else { -// get(shell, 'classList').remove('engine-dragging'); -// } -// each(get(shell, 'classList'), (cls) => { -// if (cls.substring(0, 8) === '-pseudo-') { -// get(shell, 'classList').remove(cls); -// } -// }); -// const pseudo = leaf.getStatus('pseudo'); -// if (pseudo) { -// get(shell, 'classList').add(`-pseudo-${pseudo}`); -// } -// } else { -// setNativeNode(leaf, null); -// } -// } - -// render() { -// const props = omit(this.props, ['leaf']); -// const { leaf } = this.props; -// const componentName = leaf.getComponentName(); - -// const View = getView(componentName); - -// const newProps = { -// _componentName: componentName, -// }; - -// if (!View) { -// return createElement(UnknownComponent, { -// // _componentName: componentName, -// ...newProps, -// }); -// } - -// let staticProps = { -// ...leaf.getStaticProps(false), -// ...props, -// _componentName: componentName, -// _leaf: leaf, -// componentId: leaf.getId(), -// }; - -// if (!leaf.isVisibleInPane()) { -// return null; -// } - -// if (!leaf.isVisible()) { -// return createElement(HiddenComponent, { -// ...staticProps, -// }); -// } - -// if (this.state.hasError) { -// return createElement(FaultComponent, { -// // _componentName: componentName, -// ...newProps, -// }); -// } - -// if (this.styleSheet) { -// this.styleSheet.parentNode.removeChild(this.styleSheet); -// } - -// this.styleSheet = createNodeStyleSheet(staticProps); - -// if (leaf.ableToModifyChildren()) { -// const children = leaf -// .getChildren() -// .filter((child: any) => child.getComponentName() !== 'Slot') -// .map((child: any) => -// createElement(Leaf, { -// key: child.getId(), -// leaf: child, -// }), -// ); -// // const insertion = leaf.getStatus('dropping'); -// // InsertionGhost 都是React节点,用Rax渲染会报错,后面这些节点需要通过Rax组件来实现 -// // if (children.length < 1 && insertion && insertion.getIndex() !== null) { - -// // //children = []; -// // children = [<InsertionGhost key="insertion" />]; -// // } else if (insertion && insertion.isNearEdge()) { -// // if (insertion.isNearAfter()) { -// // children.push(<InsertionGhost key="insertion" />); -// // } else { -// // children.unshift(<InsertionGhost key="insertion" />); -// // } -// // } -// staticProps = { -// ...staticProps, -// ...this.processSlots(this.props.leaf.getChildren()), -// }; - -// return createElement( -// View, -// { -// ...staticProps, -// }, -// children, -// ); -// } - -// return createElement(View, { -// ...staticProps, -// }); -// } - -// processSlots(children: Rax.RaxNodeArray) { -// const slots: any = {}; -// children && -// children.length && -// children.forEach((child: any) => { -// if (child.getComponentName() === 'Slot') { -// slots[child.getPropValue('slotName')] = <Leaf key={child.getId()} leaf={child} />; -// } -// }); -// return slots; -// } -// } diff --git a/packages/rax-simulator-renderer/src/builtin-components/renderUtils.ts b/packages/rax-simulator-renderer/src/builtin-components/renderUtils.ts deleted file mode 100644 index 10f8438fb..000000000 --- a/packages/rax-simulator-renderer/src/builtin-components/renderUtils.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { isObject } from 'lodash'; -import { css } from '@alilc/lowcode-utils'; - -const { toCss } = css; -const engine = (window as any).VisualEngine; -const { Trunk, Viewport } = engine; - -export const NativeNodeCache: any = {}; - -function ucfirst(s: string) { - return s.charAt(0).toUpperCase() + s.substring(1); -} - -export function shallowEqual(obj: { [key: string]: string }, tObj: { [key: string]: string }) { - for (const i in obj) { - if (Object.prototype.hasOwnProperty.call(obj, i) && obj[i] !== tObj[i]) { - return false; - } - } - return true; -} - -export function createNodeStyleSheet(props: any) { - if (props && props.fieldId) { - let styleProp = props.__style__; - - if (isObject(styleProp)) { - styleProp = toCss(styleProp); - } - - if (typeof styleProp === 'string') { - const s = document.createElement('style'); - const cssId = `_style_pesudo_${ props.fieldId}`; - const cssClass = `_css_pesudo_${ props.fieldId}`; - - props.className = cssClass; - s.setAttribute('type', 'text/css'); - s.setAttribute('id', cssId); - document.getElementsByTagName('head')[0].appendChild(s); - - s.appendChild( - document.createTextNode( - styleProp - .replace(/(\d+)rpx/g, (a, b) => { - return `${b / 2}px`; - }) - .replace(/:root/g, `.${ cssClass}`), - ), - ); - return s; - } - } -} - -export function setNativeNode(leaf: any, node: Rax.RaxNode) { - const id = leaf.getId(); - if (NativeNodeCache[id] === node) { - return; - } - NativeNodeCache[id] = node; - leaf.mountChange(); -} - -export function getView(componentName: string) { - // let view = new Trunk().getPrototypeView(componentName); - let view = Trunk.getPrototypeView(componentName); - if (!view) { - return null; - } - const viewport = Viewport.getViewport(); - if (viewport) { - const [mode, device] = viewport.split('-', 2).map(ucfirst); - if (view.hasOwnProperty(device)) { - view = view[device]; - } - - if (view.hasOwnProperty(mode)) { - view = view[mode]; - } - } - - return view; -} diff --git a/packages/rax-simulator-renderer/src/builtin-components/slot.tsx b/packages/rax-simulator-renderer/src/builtin-components/slot.tsx deleted file mode 100644 index 3a77491bc..000000000 --- a/packages/rax-simulator-renderer/src/builtin-components/slot.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { Component } from 'rax'; - -class Slot extends Component { - static displayName = 'Slot'; - - static componentMetadata = { - componentName: 'Slot', - configure: { - props: [{ - name: '___title', - title: { - type: 'i18n', - 'en-US': 'Slot Title', - 'zh-CN': '插槽标题', - }, - setter: 'StringSetter', - defaultValue: '插槽容器', - }, { - name: '___params', - title: { - type: 'i18n', - 'en-US': 'Slot Params', - 'zh-CN': '插槽入参', - }, - setter: { - componentName: 'ArraySetter', - props: { - itemSetter: { - componentName: 'StringSetter', - props: { - placeholder: { - type: 'i18n', - 'zh-CN': '参数名称', - 'en-US': 'Argument Name', - }, - }, - }, - }, - }, - }], - // events/className/style/general/directives - supports: false, - }, - }; - - render() { - const { children } = this.props; - return ( - <div className="lc-container">{children}</div> - ); - } -} - -export default Slot; diff --git a/packages/rax-simulator-renderer/src/host.ts b/packages/rax-simulator-renderer/src/host.ts deleted file mode 100644 index c5cf2e3e1..000000000 --- a/packages/rax-simulator-renderer/src/host.ts +++ /dev/null @@ -1,4 +0,0 @@ -// NOTE: 仅做类型标注,切勿做其它用途 -import { BuiltinSimulatorHost } from '@alilc/lowcode-designer'; - -export const host: BuiltinSimulatorHost = (window as any).LCSimulatorHost; diff --git a/packages/rax-simulator-renderer/src/image.d.ts b/packages/rax-simulator-renderer/src/image.d.ts deleted file mode 100644 index 7ed4ad925..000000000 --- a/packages/rax-simulator-renderer/src/image.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -declare module 'rax-find-dom-node'; -declare module '@alilc/lowcode-rax-renderer/lib/index'; diff --git a/packages/rax-simulator-renderer/src/index.ts b/packages/rax-simulator-renderer/src/index.ts deleted file mode 100644 index 3a8872665..000000000 --- a/packages/rax-simulator-renderer/src/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import renderer from './renderer'; - -if (typeof window !== 'undefined') { - (window as any).SimulatorRenderer = renderer; -} - -export default renderer; diff --git a/packages/rax-simulator-renderer/src/rax-use-router.js b/packages/rax-simulator-renderer/src/rax-use-router.js deleted file mode 100644 index 9a399a947..000000000 --- a/packages/rax-simulator-renderer/src/rax-use-router.js +++ /dev/null @@ -1,288 +0,0 @@ -// Inspired by react-router and universal-router -import { useState, useEffect, useLayoutEffect, createElement } from 'rax'; -import pathToRegexp from 'path-to-regexp'; - -const cache = {}; -function decodeParam(val) { - try { - return decodeURIComponent(val); - } catch (err) { - return val; - } -} - -function matchPath(route, pathname, parentParams) { - let { path, routes, exact: end = true, strict = false, sensitive = false } = route; - // If not has path or has routes that should do not exact match - if (path == null || routes) { - end = false; - } - - // Default path is empty - path = path || ''; - - const regexpCacheKey = `${path}|${end}|${strict}|${sensitive}`; - const keysCacheKey = `${regexpCacheKey }|`; - - let regexp = cache[regexpCacheKey]; - const keys = cache[keysCacheKey] || []; - - if (!regexp) { - regexp = pathToRegexp(path, keys, { - end, - strict, - sensitive, - }); - cache[regexpCacheKey] = regexp; - cache[keysCacheKey] = keys; - } - - const result = regexp.exec(pathname); - if (!result) { - return null; - } - - const url = result[0]; - const params = { ...parentParams, history: router.history, location: router.history.location }; - - for (let i = 1; i < result.length; i++) { - const key = keys[i - 1]; - const prop = key.name; - const value = result[i]; - if (value !== undefined || !Object.prototype.hasOwnProperty.call(params, prop)) { - if (key.repeat) { - params[prop] = value ? value.split(key.delimiter).map(decodeParam) : []; - } else { - params[prop] = value ? decodeParam(value) : value; - } - } - } - - return { - path: !end && url.charAt(url.length - 1) === '/' ? url.slice(1) : url, - params, - }; -} - -function matchRoute(route, baseUrl, pathname, parentParams) { - let matched; - let childMatches; - let childIndex = 0; - - return { - next() { - if (!matched) { - matched = matchPath(route, pathname, parentParams); - - if (matched) { - return { - done: false, - $: { - route, - baseUrl, - path: matched.path, - params: matched.params, - }, - }; - } - } - - if (matched && route.routes) { - while (childIndex < route.routes.length) { - if (!childMatches) { - const childRoute = route.routes[childIndex]; - childRoute.parent = route; - - childMatches = matchRoute( - childRoute, - baseUrl + matched.path, - pathname.slice(matched.path.length), - matched.params, - ); - } - - const childMatch = childMatches.next(); - if (!childMatch.done) { - return { - done: false, - $: childMatch.$, - }; - } - - childMatches = null; - childIndex++; - } - } - - return { done: true }; - }, - }; -} - -let _initialized = false; -let _routerConfig = null; -const router = { - history: null, - handles: [], - errorHandler() { }, - addHandle(handle) { - return router.handles.push(handle); - }, - removeHandle(handleId) { - router.handles[handleId - 1] = null; - }, - triggerHandles(component) { - router.handles.forEach((handle) => { - handle && handle(component); - }); - }, - match(fullpath) { - if (fullpath == null) return; - - router.fullpath = fullpath; - - const parent = router.root; - const matched = matchRoute( - parent, - parent.path, - fullpath, - ); - - function next(parent) { - const current = matched.next(); - - if (current.done) { - const error = new Error(`No match for ${fullpath}`); - return router.errorHandler(error, router.history.location); - } - - let { component } = current.$.route; - if (typeof component === 'function') { - component = component(current.$.params, router.history.location); - } - if (component instanceof Promise) { - // Lazy loading component by import('./Foo') - return component.then((component) => { - // Check current fullpath avoid router has changed before lazy loading complete - if (fullpath === router.fullpath) { - router.triggerHandles(component); - } - }); - } else if (component != null) { - router.triggerHandles(component); - return component; - } else { - return next(parent); - } - } - - return next(parent); - }, -}; - -function matchLocation({ pathname }) { - router.match(pathname); -} - - -function getInitialComponent(routerConfig) { - let InitialComponent = []; - - if (_routerConfig === null) { - if (process.env.NODE_ENV !== 'production') { - if (!routerConfig) { - throw new Error('Error: useRouter should have routerConfig, see: https://www.npmjs.com/package/rax-use-router.'); - } - if (!routerConfig.history || !routerConfig.routes) { - throw new Error('Error: routerConfig should contain history and routes, see: https://www.npmjs.com/package/rax-use-router.'); - } - } - _routerConfig = routerConfig; - } - if (_routerConfig.InitialComponent) { - InitialComponent = _routerConfig.InitialComponent; - } - router.history = _routerConfig.history; - - return InitialComponent; -} - -let unlisten = null; -let handleId = null; -let pathes = ''; -export function useRouter(routerConfig) { - const [component, setComponent] = useState(getInitialComponent(routerConfig)); - - let newPathes = ''; - if (routerConfig) { - _routerConfig = routerConfig; - const { routes } = _routerConfig; - router.root = Array.isArray(routes) ? { routes } : routes; - if (Array.isArray(routes)) { - newPathes = routes.map(it => it.path).join(','); - } else { - newPathes = routes.path; - } - } - if (_initialized && _routerConfig.history) { - if (newPathes !== pathes) { - matchLocation(_routerConfig.history.location); - pathes = newPathes; - } - } - - useLayoutEffect(() => { - if (unlisten) { - unlisten(); - unlisten = null; - } - - if (handleId) { - router.removeHandle(handleId); - handleId = null; - } - - const { history } = _routerConfig; - const { routes } = _routerConfig; - - router.root = Array.isArray(routes) ? { routes } : routes; - - handleId = router.addHandle((component) => { - setComponent(component); - }); - - // Init path match - if (_initialized || !_routerConfig.InitialComponent) { - matchLocation(history.location); - pathes = newPathes; - } - - unlisten = history.listen(({ location }) => { - matchLocation(location); - pathes = newPathes; - }); - - _initialized = true; - - return () => { - pathes = ''; - router.removeHandle(handleId); - handleId = null; - unlisten(); - unlisten = null; - }; - }, []); - - return { component }; -} - -export function withRouter(Component) { - function Wrapper(props) { - const { history } = router; - return createElement(Component, { ...props, history, location: history.location }); - } - - Wrapper.displayName = `withRouter(${ Component.displayName || Component.name })`; - Wrapper.WrappedComponent = Component; - return Wrapper; -} diff --git a/packages/rax-simulator-renderer/src/renderer-view.tsx b/packages/rax-simulator-renderer/src/renderer-view.tsx deleted file mode 100644 index bc39cddea..000000000 --- a/packages/rax-simulator-renderer/src/renderer-view.tsx +++ /dev/null @@ -1,275 +0,0 @@ -import RaxRenderer from '@alilc/lowcode-rax-renderer'; -import { History } from 'history'; -import { Component, createElement, Fragment } from 'rax'; -import { useRouter } from './rax-use-router'; -import { DocumentInstance, SimulatorRendererContainer } from './renderer'; -import './renderer.less'; -import { uniqueId } from '@alilc/lowcode-utils'; -import { GlobalEvent } from '@alilc/lowcode-types'; -import { host } from './host'; - -// patch cloneElement avoid lost keyProps -const originCloneElement = (window as any).Rax.cloneElement; -(window as any).Rax.cloneElement = (child: any, { _leaf, ...props }: any = {}, ...rest: any[]) => { - if (child.ref && props.ref) { - const dRef = props.ref; - const cRef = child.ref; - props.ref = (x: any) => { - if (cRef) { - if (typeof cRef === 'function') { - cRef(x); - } else { - try { - cRef.current = x; - } catch (e) { - console.error(e); - } - } - } - if (dRef) { - if (typeof dRef === 'function') { - dRef(x); - } else { - try { - dRef.current = x; - } catch (e) { - console.error(e); - } - } - } - }; - } - return originCloneElement(child, props, ...rest); -}; - -export default class SimulatorRendererView extends Component<{ rendererContainer: SimulatorRendererContainer }> { - private unlisten: any; - - componentDidMount() { - const { rendererContainer } = this.props; - this.unlisten = rendererContainer.onLayoutChange(() => { - this.forceUpdate(); - }); - } - - componentWillUnmount() { - if (this.unlisten) { - this.unlisten(); - } - } - - render() { - const { rendererContainer } = this.props; - return ( - <Layout rendererContainer={rendererContainer}> - <Routes rendererContainer={rendererContainer} history={rendererContainer.history} /> - </Layout> - ); - } -} - -export const Routes = (props: { - rendererContainer: SimulatorRendererContainer; - history: History; -}) => { - const { rendererContainer, history } = props; - const { documentInstances } = rendererContainer; - - const routes = { - history, - routes: documentInstances.map(instance => { - return { - path: instance.path, - component: (props: any) => <Renderer key={instance.id} rendererContainer={rendererContainer} documentInstance={instance} {...props} />, - }; - }), - }; - const { component } = useRouter(routes); - return component; -}; - -function ucfirst(s: string) { - return s.charAt(0).toUpperCase() + s.substring(1); -} -function getDeviceView(view: any, device: string, mode: string) { - if (!view || typeof view === 'string') { - return view; - } - - // compatible vision Mobile | Preview - device = ucfirst(device); - if (device === 'Mobile' && view.hasOwnProperty(device)) { - view = view[device]; - } - mode = ucfirst(mode); - if (mode === 'Preview' && view.hasOwnProperty(mode)) { - view = view[mode]; - } - return view; -} - -class Layout extends Component<{ rendererContainer: SimulatorRendererContainer }> { - constructor(props: any) { - super(props); - this.props.rendererContainer.onReRender(() => { - this.forceUpdate(); - }); - } - - render() { - const { rendererContainer, children } = this.props; - const { layout } = rendererContainer; - - if (layout) { - const { Component, props, componentName } = layout; - if (Component) { - return <Component props={props}>{children}</Component>; - } - if (componentName && rendererContainer.getComponent(componentName)) { - return createElement( - rendererContainer.getComponent(componentName), - { - ...props, - rendererContainer, - }, - [children], - ); - } - } - - return <Fragment>{children}</Fragment>; - } -} - -class Renderer extends Component<{ - rendererContainer: SimulatorRendererContainer; - documentInstance: DocumentInstance; -}> { - private unlisten: any; - private key: string; - private startTime: number | null = null; - - componentWillMount() { - this.key = uniqueId('renderer'); - } - - componentDidMount() { - const { documentInstance } = this.props; - this.unlisten = documentInstance.onReRender((params) => { - if (params && params.shouldRemount) { - this.key = uniqueId('renderer'); - } - this.forceUpdate(); - }); - } - - componentWillUnmount() { - if (this.unlisten) { - this.unlisten(); - } - } - shouldComponentUpdate() { - return false; - } - - componentDidUpdate() { - if (this.startTime) { - const time = Date.now() - this.startTime; - const nodeCount = host.designer.currentDocument?.getNodeCount?.(); - host.designer.editor?.eventBus.emit(GlobalEvent.Node.Rerender, { - componentName: 'Renderer', - type: 'All', - time, - nodeCount, - }); - } - } - - schemaChangedSymbol = false; - - getSchemaChangedSymbol = () => { - return this.schemaChangedSymbol; - }; - - setSchemaChangedSymbol = (symbol: boolean) => { - this.schemaChangedSymbol = symbol; - }; - - render() { - const { documentInstance } = this.props; - const { container, document } = documentInstance; - const { designMode, device } = container; - const { rendererContainer: renderer } = this.props; - this.startTime = Date.now(); - this.schemaChangedSymbol = false; - - return ( - <RaxRenderer - schema={documentInstance.schema} - components={renderer.components} - appHelper={renderer.context} - context={renderer.context} - device={device} - designMode={renderer.designMode} - key={this.key} - __host={host} - __container={container} - suspended={documentInstance.suspended} - self={documentInstance.scope} - onCompGetRef={(schema: any, ref: any) => { - documentInstance.mountInstance(schema.id, ref); - }} - thisRequiredInJSE={host.thisRequiredInJSE} - documentId={document.id} - getNode={(id: string) => documentInstance.getNode(id) as any} - rendererName="PageRenderer" - customCreateElement={(Component: any, props: any, children: any) => { - const { __id, ...viewProps } = props; - viewProps.componentId = __id; - const leaf = documentInstance.getNode(__id); - viewProps._leaf = leaf; - viewProps._componentName = leaf?.componentName; - // 如果是容器 && 无children && 高宽为空 增加一个占位容器,方便拖动 - if ( - !viewProps.dataSource && - leaf?.isContainer() && - (children == null || (Array.isArray(children) && !children.length)) && - (!viewProps.style || Object.keys(viewProps.style).length === 0) - ) { - children = ( - <div className="lc-container-placeholder" style={viewProps.placeholderStyle}> - {viewProps.placeholder || '拖拽组件或模板到这里'} - </div> - ); - } - - // if (viewProps._componentName === 'Menu') { - // Object.assign(viewProps, { - // _componentName: 'Menu', - // className: '_css_pesudo_menu_kbrzyh0f', - // context: { VE: (window as any).VisualLowCodeRenderer }, - // direction: undefined, - // events: { ignored: true }, - // fieldId: 'menu_kbrzyh0f', - // footer: '', - // header: '', - // mode: 'inline', - // onItemClick: { ignored: true }, - // onSelect: { ignored: true }, - // popupAlign: 'follow', - // selectMode: false, - // triggerType: 'click', - // }); - // console.info('menuprops', viewProps); - // } - - return createElement( - getDeviceView(Component, device, designMode), - viewProps, - leaf?.isContainer() ? (children == null ? [] : Array.isArray(children) ? children : [children]) : children, - ); - }} - /> - ); - } -} diff --git a/packages/rax-simulator-renderer/src/renderer.less b/packages/rax-simulator-renderer/src/renderer.less deleted file mode 100644 index b71dde989..000000000 --- a/packages/rax-simulator-renderer/src/renderer.less +++ /dev/null @@ -1,125 +0,0 @@ -body, html { - display: block; - background: white; - padding: 0; - margin: 0; -} - -html.engine-cursor-move, html.engine-cursor-move * { - cursor: grabbing !important; -} - -html.engine-cursor-copy, html.engine-cursor-copy * { - cursor: copy !important; -} - -html.engine-cursor-ew-resize, html.engine-cursor-ew-resize * { - cursor: ew-resize !important; -} - -::-webkit-scrollbar { - display: none; -} - -.lc-container { - &:empty { - background: #f2f3f5; - color: #a7b1bd; - outline: 1px dashed rgba(31, 56, 88, 0.2); - outline-offset: -1px !important; - height: 66px; - max-height: 100%; - min-width: 140px; - text-align: center; - overflow: hidden; - display: flex; - align-items: center; - &:before { - content: '\62D6\62FD\7EC4\4EF6\6216\6A21\677F\5230\8FD9\91CC'; - font-size: 14px; - z-index: 1; - width: 100%; - white-space: nowrap; - height: 100%; - display: flex; - align-items: center; - justify-content: center; - } - } -} - -.engine-empty { - background: #f2f3f5; - color: #a7b1bd; - outline: 1px dashed rgba(31, 56, 88, 0.2); - outline-offset: -1px !important; - height: 66px; - max-height: 100%; - min-width: 140px; - text-align: center; - overflow: hidden; - display: flex; - align-items: center; -} - -.engine-empty:before { - content: '\62D6\62FD\7EC4\4EF6\6216\6A21\677F\5230\8FD9\91CC'; - font-size: 14px; - z-index: 1; - width: 100%; - white-space: nowrap; - height: 100%; - display: flex; - align-items: center; - justify-content: center; -} - -.lc-container-placeholder { - min-height: 60px; - height: 100%; - width: 100%; - background-color: rgb(240, 240, 240); - border: 1px dotted; - color: rgb(167, 177, 189); - display: flex; - align-items: center; - justify-content: center; - font-size: 14px; -} - -body.engine-document { - &:after, &:before { - content: ""; - display: table; - } - &:after { - clear: both; - } - - /* - .next-input-group, - .next-checkbox-group,.next-date-picker,.next-input,.next-month-picker, - .next-number-picker,.next-radio-group,.next-range,.next-range-picker, - .next-rating,.next-select,.next-switch,.next-time-picker,.next-upload, - .next-year-picker, - .next-breadcrumb-item,.next-calendar-header,.next-calendar-table { - pointer-events: none !important; - } */ -} - -.engine-live-editing { - cursor: text; - outline: none; - box-shadow: 0 0 0 2px rgb(102, 188, 92); - user-select: text; -} - -/* stylelint-disable-next-line selector-max-id */ -#app { - height: 100vh; -} - - -.luna-page { - height: 100%; -} diff --git a/packages/rax-simulator-renderer/src/renderer.ts b/packages/rax-simulator-renderer/src/renderer.ts deleted file mode 100644 index 64ec2d2c6..000000000 --- a/packages/rax-simulator-renderer/src/renderer.ts +++ /dev/null @@ -1,690 +0,0 @@ -import { BuiltinSimulatorRenderer, Component, IBaseNode, IDocumentModel } from '@alilc/lowcode-designer'; -import { IPublicTypeComponentSchema, IPublicTypeNodeSchema, IPublicTypeNpmInfo, IPublicEnumTransformStage, IPublicTypeNodeInstance, IPublicTypeProjectSchema } from '@alilc/lowcode-types'; -import { Asset, compatibleLegaoSchema, cursor, isElement, isESModule, isLowcodeProjectSchema, isComponentSchema, isPlainObject, isReactComponent, setNativeSelection } from '@alilc/lowcode-utils'; -import LowCodeRenderer from '@alilc/lowcode-rax-renderer'; -import { computed, observable as obx, makeObservable, configure } from 'mobx'; -import DriverUniversal from 'driver-universal'; -import { createMemoryHistory, MemoryHistory } from 'history'; -// @ts-ignore -import Rax, { ComponentType, createElement, render as raxRender, shared } from 'rax'; -import Leaf from './builtin-components/leaf'; -import Slot from './builtin-components/slot'; -import { host } from './host'; -import SimulatorRendererView from './renderer-view'; -import { raxFindDOMNodes } from './utils/find-dom-nodes'; -import { getClientRects } from './utils/get-client-rects'; -import loader from './utils/loader'; -import { parseQuery, withQueryParams } from './utils/url'; -import { IEventBus, createModuleEventBus } from '@alilc/lowcode-editor-core'; - -configure({ enforceActions: 'never' }); -const { Instance } = shared; - -export interface LibraryMap { - [key: string]: string; -} - -const SYMBOL_VNID = Symbol('_LCNodeId'); -const SYMBOL_VDID = Symbol('_LCDocId'); - -const INTERNAL = '_internal'; - -function accessLibrary(library: string | object) { - if (typeof library !== 'string') { - return library; - } - - return (window as any)[library]; -} - -// Slot/Leaf and Fragment|FunctionComponent polyfill(ref) - -const builtinComponents = { - Slot, - Leaf, -}; - -function buildComponents( - libraryMap: LibraryMap, - componentsMap: { [componentName: string]: IPublicTypeNpmInfo | ComponentType<any> | IPublicTypeComponentSchema }, - createComponent: (schema: IPublicTypeProjectSchema<IPublicTypeComponentSchema>) => Component | null, -) { - const components: any = { - ...builtinComponents, - }; - Object.keys(componentsMap).forEach((componentName) => { - let component = componentsMap[componentName]; - if (component && (isLowcodeProjectSchema(component) || isComponentSchema(component))) { - if (isComponentSchema(component)) { - components[componentName] = createComponent({ - version: '', - componentsMap: [], - componentsTree: [component], - }); - } else { - components[componentName] = createComponent(component); - } - } else if (isReactComponent(component)) { - components[componentName] = component; - } else { - component = findComponent(libraryMap, componentName, component as IPublicTypeNpmInfo); - if (component) { - components[componentName] = component; - } - } - }); - return components; -} - -let REACT_KEY = ''; -function cacheReactKey(el: Element): Element { - if (REACT_KEY !== '') { - return el; - } - // react17 采用 __reactFiber 开头 - REACT_KEY = Object.keys(el).find( - (key) => key.startsWith('__reactInternalInstance$') || key.startsWith('__reactFiber$'), - ) || ''; - if (!REACT_KEY && (el as HTMLElement).parentElement) { - return cacheReactKey((el as HTMLElement).parentElement!); - } - return el; -} - -function checkInstanceMounted(instance: any): boolean { - if (isElement(instance)) { - return instance.parentElement != null; - } - return true; -} - -function isValidDesignModeRaxComponentInstance( - raxComponentInst: any, -): raxComponentInst is { - props: { - _leaf: Exclude<IPublicTypeNodeInstance<any>['node'], null | undefined>; - }; -} { - const leaf = raxComponentInst?.props?._leaf; - return leaf && typeof leaf === 'object' && leaf.isNode; -} - -export class DocumentInstance { - private instancesMap = new Map<string, any[]>(); - - private emitter: IEventBus = createModuleEventBus('DocumentInstance'); - - get schema(): any { - return this.document.export(IPublicEnumTransformStage.Render); - } - - constructor(readonly container: SimulatorRendererContainer, readonly document: IDocumentModel) { - makeObservable(this); - } - - @computed get suspended(): any { - return false; - } - - @computed get scope(): any { - return null; - } - - get path(): string { - return `/${ this.document.fileName}`; - } - - get id() { - return this.document.id; - } - - private unmountIntance(id: string, instance: any) { - const instances = this.instancesMap.get(id); - if (instances) { - const i = instances.indexOf(instance); - if (i > -1) { - instances.splice(i, 1); - host.setInstance(this.document.id, id, instances); - } - } - } - - refresh() { - this.emitter.emit('rerender', { shouldRemount: true }); - } - - onReRender(fn: () => void) { - this.emitter.on('rerender', fn); - return () => { - this.emitter.removeListener('renderer', fn); - }; - } - - mountInstance(id: string, instance: any) { - const docId = this.document.id; - const { instancesMap } = this; - if (instance == null) { - let instances = this.instancesMap.get(id); - if (instances) { - instances = instances.filter(checkInstanceMounted); - if (instances.length > 0) { - instancesMap.set(id, instances); - host.setInstance(this.document.id, id, instances); - } else { - instancesMap.delete(id); - host.setInstance(this.document.id, id, null); - } - } - return; - } - const unmountIntance = this.unmountIntance.bind(this); - const origId = (instance as any)[SYMBOL_VNID]; - if (origId && origId !== id) { - // 另外一个节点的 instance 在此被复用了,需要从原来地方卸载 - unmountIntance(origId, instance); - } - if (isElement(instance)) { - cacheReactKey(instance); - } else if (origId !== id) { - // 涵盖 origId == null || origId !== id 的情况 - let origUnmount: any = instance.componentWillUnmount; - if (origUnmount && origUnmount.origUnmount) { - origUnmount = origUnmount.origUnmount; - } - // hack! delete instance from map - const newUnmount = function (this: any) { - unmountIntance(id, instance); - origUnmount && origUnmount.call(this); - }; - (newUnmount as any).origUnmount = origUnmount; - instance.componentWillUnmount = newUnmount; - } - - (instance as any)[SYMBOL_VNID] = id; - (instance as any)[SYMBOL_VDID] = docId; - let instances = this.instancesMap.get(id); - if (instances) { - const l = instances.length; - instances = instances.filter(checkInstanceMounted); - let updated = instances.length !== l; - if (!instances.includes(instance)) { - instances.push(instance); - updated = true; - } - if (!updated) { - return; - } - } else { - instances = [instance]; - } - instancesMap.set(id, instances); - host.setInstance(this.document.id, id, instances); - } - - mountContext(docId: string, id: string, ctx: object) { - // this.ctxMap.set(id, ctx); - } - - getComponentInstances(id: string): any[] | null { - return this.instancesMap.get(id) || null; - } - - getNode(id: string): IBaseNode<IPublicTypeNodeSchema> | null { - return this.document.getNode(id); - } -} - -export class SimulatorRendererContainer implements BuiltinSimulatorRenderer { - readonly isSimulatorRenderer = true; - private dispose?: () => void; - readonly history: MemoryHistory; - - private emitter: IEventBus = createModuleEventBus('SimulatorRendererContainer'); - - @obx.ref private _documentInstances: DocumentInstance[] = []; - get documentInstances() { - return this._documentInstances; - } - - get currentDocumentInstance() { - return this._documentInstances.find((item) => item.id === host.project.currentDocument?.id); - } - - constructor() { - this.dispose = host.connect(this, () => { - // sync layout config - this._layout = host.project.get('config').layout; - // todo: split with others, not all should recompute - if (this._libraryMap !== host.libraryMap || this._componentsMap !== host.designer.componentsMap) { - this._libraryMap = host.libraryMap || {}; - this._componentsMap = host.designer.componentsMap; - this.buildComponents(); - } - - // sync designMode - this._designMode = host.designMode; - - this._locale = host.locale; - - // sync requestHandlersMap - this._requestHandlersMap = host.requestHandlersMap; - - // sync device - this._device = host.device; - - this.emitter.emit('layoutChange'); - }); - const documentInstanceMap = new Map<string, DocumentInstance>(); - let initialEntry = '/'; - let firstRun = true; - host.autorun(() => { - this._documentInstances = host.project.documents.map((doc) => { - let inst = documentInstanceMap.get(doc.id); - if (!inst) { - inst = new DocumentInstance(this, doc); - documentInstanceMap.set(doc.id, inst); - } - return inst; - }); - - const path = host.project.currentDocument ? documentInstanceMap.get(host.project.currentDocument.id)!.path : '/'; - if (firstRun) { - initialEntry = path; - firstRun = false; - } else { - if (this.history.location.pathname !== path) { - this.history.replace(path); - } - this.emitter.emit('layoutChange'); - } - }); - const history = createMemoryHistory({ - initialEntries: [initialEntry], - }); - this.history = history; - history.listen(({ location }) => { - host.project.open(location.pathname.slice(1)); - }); - host.componentsConsumer.consume(async (componentsAsset) => { - if (componentsAsset) { - await this.load(componentsAsset); - this.buildComponents(); - } - }); - this._appContext = { - utils: { - router: { - push(path: string, params?: object) { - history.push(withQueryParams(path, params)); - }, - replace(path: string, params?: object) { - history.replace(withQueryParams(path, params)); - }, - back() { - history.back(); - }, - }, - legaoBuiltins: { - getUrlParams() { - const { search } = history.location; - return parseQuery(search); - }, - }, - }, - constants: {}, - requestHandlersMap: this._requestHandlersMap, - }; - host.injectionConsumer.consume((data) => { - // sync utils, i18n, contants,... config - }); - } - - @obx private _layout: any = null; - @computed get layout(): any { - // TODO: parse layout Component - return this._layout; - } - set layout(value: any) { - this._layout = value; - } - - private _libraryMap: { [key: string]: string } = {}; - private buildComponents() { - // TODO: remove this.createComponent - this._components = buildComponents(this._libraryMap, this._componentsMap, this.createComponent.bind(this)); - } - @obx.ref private _components: Record<string, React.FC | React.ComponentClass> | null = {}; - @computed get components(): Record<string, React.FC | React.ComponentClass> { - // 根据 device 选择不同组件,进行响应式 - // 更好的做法是,根据 device 选择加载不同的组件资源,甚至是 simulatorUrl - return this._components || {}; - } - // context from: utils、constants、history、location、match - @obx.ref private _appContext = {}; - @computed get context(): any { - return this._appContext; - } - @obx.ref private _designMode: string = 'design'; - @computed get designMode(): any { - return this._designMode; - } - @obx.ref private _device: string = 'default'; - @computed get device() { - return this._device; - } - @obx.ref private _locale: string | undefined = undefined; - @computed get locale() { - return this._locale; - } - @obx.ref private _requestHandlersMap = null; - @computed get requestHandlersMap(): any { - return this._requestHandlersMap; - } - @obx.ref private _componentsMap = {}; - @computed get componentsMap(): any { - return this._componentsMap; - } - - /** - * 加载资源 - */ - load(asset: Asset): Promise<any> { - return loader.load(asset); - } - - async loadAsyncLibrary(asyncLibraryMap: Record<string, any>) { - } - - getComponent(componentName: string) { - const paths = componentName.split('.'); - const subs: string[] = []; - - while (true) { - const component = this._components?.[componentName]; - if (component) { - return getSubComponent(component, subs); - } - - const sub = paths.pop(); - if (!sub) { - return null; - } - subs.unshift(sub); - componentName = paths.join('.'); - } - } - - getNodeInstance(dom: HTMLElement): IPublicTypeNodeInstance<any> | null { - const INTERNAL = '_internal'; - let instance: any = dom; - if (!isElement(instance)) { - return { - docId: instance.props._leaf.document.id, - nodeId: instance.props._leaf.getId(), - instance, - node: instance.props._leaf, - }; - } - instance = Instance.get(dom); - - let loopNum = 0; // 防止由于某种意外而导致死循环 - while (instance && instance[INTERNAL] && loopNum < 1000) { - if (isValidDesignModeRaxComponentInstance(instance)) { - // if (instance && SYMBOL_VNID in instance) { - // const docId = (instance.props as any).schema.docId; - return { - docId: instance.props._leaf.document?.id || '', - nodeId: instance.props._leaf.getId(), - instance, - node: instance.props._leaf, - }; - } - - instance = getRaxVDomParentInstance(instance); - loopNum += 1; - } - - return null; - } - - getClosestNodeInstance(from: any, nodeId?: string): IPublicTypeNodeInstance<any> | null { - const el: any = from; - if (el) { - // if (isElement(el)) { - // el = cacheReactKey(el); - // } else { - // return getNodeInstance(el, specId); - // } - return this.getNodeInstance(el); - } - return null; - } - - findDOMNodes(instance: any, selector?: string): Array<Element | Text> | null { - let el = instance; - if (selector) { - el = document.querySelector(selector); - } - try { - return raxFindDOMNodes(el); - } catch (e) { - // ignore - } - if (el && el.type && el.props && el.props.componentId) { - el = document.querySelector(`${el.type}[componentid=${el.props.componentId}]`); - } else { - console.error(instance); - throw new Error('This instance may not a valid element'); - } - return raxFindDOMNodes(el); - } - - getClientRects(element: Element | Text) { - return getClientRects(element); - } - - setNativeSelection(enableFlag: boolean) { - setNativeSelection(enableFlag); - } - setDraggingState(state: boolean) { - cursor.setDragging(state); - } - setCopyState(state: boolean) { - cursor.setCopy(state); - } - clearState() { - cursor.release(); - } - - onLayoutChange(cb: () => void) { - this.emitter.on('layoutChange', cb); - return () => { - this.emitter.removeListener('layoutChange', cb); - }; - } - - onReRender(fn: () => void) { - this.emitter.on('rerender', fn); - return () => { - this.emitter.removeListener('renderer', fn); - }; - } - - rerender() { - this.currentDocumentInstance?.refresh(); - } - - stopAutoRepaintNode() { - } - - enableAutoRepaintNode() { - } - - createComponent(schema: IPublicTypeProjectSchema<IPublicTypeComponentSchema>): Component | null { - const _schema: IPublicTypeProjectSchema<IPublicTypeComponentSchema> = { - ...schema, - componentsTree: schema.componentsTree.map(compatibleLegaoSchema), - }; - - const componentsTreeSchema = _schema.componentsTree[0]; - - if (componentsTreeSchema.componentName === 'Component' && componentsTreeSchema.css) { - const doc = window.document; - const s = doc.createElement('style'); - s.setAttribute('type', 'text/css'); - s.setAttribute('id', `Component-${componentsTreeSchema.id || ''}`); - s.appendChild(doc.createTextNode(componentsTreeSchema.css || '')); - doc.getElementsByTagName('head')[0].appendChild(s); - } - - const renderer = this; - const { componentsMap: components } = renderer; - - class LowCodeComp extends Rax.Component { - render() { - const extraProps = getLowCodeComponentProps(this.props); - // @ts-ignore - return createElement(LowCodeRenderer, { - ...extraProps, - schema: componentsTreeSchema, - components, - designMode: '', - locale: renderer.locale, - messages: _schema.i18n || {}, - device: renderer.device, - appHelper: renderer.context, - rendererName: 'LowCodeRenderer', - thisRequiredInJSE: host.thisRequiredInJSE, - customCreateElement: (Comp: any, props: any, children: any) => { - const componentMeta = host.currentDocument?.getComponentMeta(Comp.displayName); - if (componentMeta?.isModal) { - return null; - } - - const { __id, __designMode, ...viewProps } = props; - // mock _leaf,减少性能开销 - const _leaf = { - isEmpty: () => false, - isMock: true, - }; - viewProps._leaf = _leaf; - return createElement(Comp, viewProps, children); - }, - }); - } - } - - return LowCodeComp; - } - - private _running = false; - run() { - if (this._running) { - return; - } - this._running = true; - const containerId = 'app'; - let container = document.getElementById(containerId); - if (!container) { - container = document.createElement('div'); - document.body.appendChild(container); - container.id = containerId; - } - - // ==== compatiable vision - document.documentElement.classList.add('engine-page'); - document.body.classList.add('engine-document'); // important! Stylesheet.invoke depends - - raxRender(createElement(SimulatorRendererView, { - rendererContainer: this, - }), container, { - driver: DriverUniversal, - }); - host.project.setRendererReady(this); - } -} - -function getSubComponent(library: any, paths: string[]) { - const l = paths.length; - if (l < 1 || !library) { - return library; - } - let i = 0; - let component: any; - while (i < l) { - const key = paths[i]!; - let ex: any; - try { - component = library[key]; - } catch (e) { - ex = e; - component = null; - } - if (i === 0 && component == null && key === 'default') { - if (ex) { - return l === 1 ? library : null; - } - component = library; - } else if (component == null) { - return null; - } - library = component; - i++; - } - return component; -} - -function findComponent(libraryMap: LibraryMap, componentName: string, npm?: IPublicTypeNpmInfo) { - if (!npm) { - return accessLibrary(componentName); - } - // libraryName the key access to global - // export { exportName } from xxx exportName === global.libraryName.exportName - // export exportName from xxx exportName === global.libraryName.default || global.libraryName - // export { exportName as componentName } from package - // if exportName == null exportName === componentName; - // const componentName = exportName.subName, if exportName empty subName donot use - const exportName = npm.exportName || npm.componentName || componentName; - const libraryName = libraryMap[npm.package] || exportName; - const library = accessLibrary(libraryName); - const paths = npm.exportName && npm.subName ? npm.subName.split('.') : []; - if (npm.destructuring) { - paths.unshift(exportName); - } else if (isESModule(library)) { - paths.unshift('default'); - } - return getSubComponent(library, paths); -} - -function getLowCodeComponentProps(props: any) { - if (!props || !isPlainObject(props)) { - return props; - } - const newProps: any = {}; - Object.keys(props).forEach(k => { - if (['children', 'componentId', '__designMode', '_componentName', '_leaf'].includes(k)) { - return; - } - newProps[k] = props[k]; - }); - return newProps; -} - -/** - * 获取 Rax 里面 VDOM 的上一级的实例 - * 注意:Rax 的 development 的包是带有 __parentInstance, - * 但是 production 的包 __parentInstance 会被压缩掉, - * 所以这里遍历下其中的所有值,尝试找到有 _internal 的那个(别的值不会带有这个属性的) - */ -function getRaxVDomParentInstance(instance: { _internal: any }) { - const internalInstance = instance._internal; - return internalInstance.__parentInstance || - Object.values(internalInstance).find(v => ( - v !== null && - v !== instance && - typeof v === 'object' && - typeof (v as {_internal: unknown})._internal === 'object' - )); -} - -export default new SimulatorRendererContainer(); diff --git a/packages/rax-simulator-renderer/src/utils/create-defer.ts b/packages/rax-simulator-renderer/src/utils/create-defer.ts deleted file mode 100644 index e7997365a..000000000 --- a/packages/rax-simulator-renderer/src/utils/create-defer.ts +++ /dev/null @@ -1,17 +0,0 @@ -export interface Defer<T = any> { - resolve(value?: T | PromiseLike<T>): void; - reject(reason?: any): void; - promise(): Promise<T>; -} - -export function createDefer<T = any>(): Defer<T> { - const r: any = {}; - const promise = new Promise<T>((resolve, reject) => { - r.resolve = resolve; - r.reject = reject; - }); - - r.promise = () => promise; - - return r; -} diff --git a/packages/rax-simulator-renderer/src/utils/find-dom-nodes.ts b/packages/rax-simulator-renderer/src/utils/find-dom-nodes.ts deleted file mode 100644 index 106af2efa..000000000 --- a/packages/rax-simulator-renderer/src/utils/find-dom-nodes.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { isElement } from '@alilc/lowcode-utils'; -import findDOMNode from 'rax-find-dom-node'; -// import { isDOMNode } from './is-dom-node'; - -export function raxFindDOMNodes(instance: any): Array<Element | Text> | null { - if (!instance) { - return null; - } - if (isElement(instance)) { - return [instance]; - } - // eslint-disable-next-line react/no-find-dom-node - const result = findDOMNode(instance); - if (Array.isArray(result)) { - return result; - } - return [result]; -} diff --git a/packages/rax-simulator-renderer/src/utils/get-client-rects.ts b/packages/rax-simulator-renderer/src/utils/get-client-rects.ts deleted file mode 100644 index dd13aba81..000000000 --- a/packages/rax-simulator-renderer/src/utils/get-client-rects.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { isElement } from '@alilc/lowcode-utils'; - -// a range for test TextNode clientRect -const cycleRange = document.createRange(); - -export function getClientRects(node: Element | Text) { - if (isElement(node)) { - return [node.getBoundingClientRect()]; - } - - cycleRange.selectNode(node); - return Array.from(cycleRange.getClientRects()); -} diff --git a/packages/rax-simulator-renderer/src/utils/get-closest-node-instance.ts b/packages/rax-simulator-renderer/src/utils/get-closest-node-instance.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/rax-simulator-renderer/src/utils/get-device-view.ts b/packages/rax-simulator-renderer/src/utils/get-device-view.ts deleted file mode 100644 index 900556259..000000000 --- a/packages/rax-simulator-renderer/src/utils/get-device-view.ts +++ /dev/null @@ -1,23 +0,0 @@ -function ucfirst(s: string) { - return s.charAt(0).toUpperCase() + s.substring(1); -} -function getDeviceView(view: any, device: string, mode: string) { - if (!view || typeof view === 'string') { - return view; - } - - // compatible vision Mobile | Preview - device = ucfirst(device); - if (device === 'Mobile' && view.hasOwnProperty(device)) { - view = view[device]; - } - mode = ucfirst(mode); - if (mode === 'Preview' && view.hasOwnProperty(mode)) { - view = view[mode]; - } - return view; -} - -export default { - getDeviceView, -}; diff --git a/packages/rax-simulator-renderer/src/utils/is-dom-node.ts b/packages/rax-simulator-renderer/src/utils/is-dom-node.ts deleted file mode 100644 index bfbeb79c1..000000000 --- a/packages/rax-simulator-renderer/src/utils/is-dom-node.ts +++ /dev/null @@ -1,4 +0,0 @@ -export function isDOMNode(node: any): node is Element | Text { - if (!node) return false; - return node.nodeType && (node.nodeType === Node.ELEMENT_NODE || node.nodeType === Node.TEXT_NODE); -} diff --git a/packages/rax-simulator-renderer/src/utils/loader.ts b/packages/rax-simulator-renderer/src/utils/loader.ts deleted file mode 100644 index 436e51c44..000000000 --- a/packages/rax-simulator-renderer/src/utils/loader.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { load, evaluate } from './script'; -import StylePoint from './style'; -import { - Asset, - AssetLevel, - AssetLevels, - AssetType, - AssetList, - isAssetBundle, - isAssetItem, - assetItem, - AssetItem, - isCSSUrl, -} from '@alilc/lowcode-utils'; - -function parseAssetList(scripts: any, styles: any, assets: AssetList, level?: AssetLevel) { - for (const asset of assets) { - parseAsset(scripts, styles, asset, level); - } -} - -function parseAsset(scripts: any, styles: any, asset: Asset | undefined | null, level?: AssetLevel) { - if (!asset) { - return; - } - if (Array.isArray(asset)) { - return parseAssetList(scripts, styles, asset, level); - } - - if (isAssetBundle(asset)) { - if (asset.assets) { - if (Array.isArray(asset.assets)) { - parseAssetList(scripts, styles, asset.assets, asset.level || level); - } else { - parseAsset(scripts, styles, asset.assets, asset.level || level); - } - return; - } - return; - } - - if (!isAssetItem(asset)) { - asset = assetItem(isCSSUrl(asset) ? AssetType.CSSUrl : AssetType.JSUrl, asset, level)!; - } - - let lv = asset.level || level; - - if (!lv || AssetLevel[lv] == null) { - lv = AssetLevel.App; - } - - asset.level = lv; - if (asset.type === AssetType.CSSUrl || asset.type == AssetType.CSSText) { - styles[lv].push(asset); - } else { - scripts[lv].push(asset); - } -} - -export class AssetLoader { - async load(asset: Asset) { - const styles: any = {}; - const scripts: any = {}; - AssetLevels.forEach(lv => { - styles[lv] = []; - scripts[lv] = []; - }); - parseAsset(scripts, styles, asset); - const styleQueue: AssetItem[] = styles[AssetLevel.Environment].concat( - styles[AssetLevel.Library], - styles[AssetLevel.Theme], - styles[AssetLevel.Runtime], - styles[AssetLevel.App], - ); - const scriptQueue: AssetItem[] = scripts[AssetLevel.Environment].concat( - scripts[AssetLevel.Library], - scripts[AssetLevel.Theme], - scripts[AssetLevel.Runtime], - scripts[AssetLevel.App], - ); - await Promise.all( - styleQueue.map(({ content, level, type, id }) => this.loadStyle(content, level!, type === AssetType.CSSUrl, id)), - ); - await Promise.all(scriptQueue.map(({ content, type }) => this.loadScript(content, type === AssetType.JSUrl))); - } - - private stylePoints = new Map<string, StylePoint>(); - - private loadStyle(content: string | undefined | null, level: AssetLevel, isUrl?: boolean, id?: string) { - if (!content) { - return; - } - let point: StylePoint | undefined; - if (id) { - point = this.stylePoints.get(id); - if (!point) { - point = new StylePoint(level, id); - this.stylePoints.set(id, point); - } - } else { - point = new StylePoint(level); - } - return isUrl ? point.applyUrl(content) : point.applyText(content); - } - - private loadScript(content: string | undefined | null, isUrl?: boolean) { - if (!content) { - return; - } - return isUrl ? load(content) : evaluate(content); - } -} - -export default new AssetLoader(); diff --git a/packages/rax-simulator-renderer/src/utils/script.ts b/packages/rax-simulator-renderer/src/utils/script.ts deleted file mode 100644 index 81841ff6d..000000000 --- a/packages/rax-simulator-renderer/src/utils/script.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { createDefer } from './create-defer'; - -export function evaluate(script: string) { - const scriptEl = document.createElement('script'); - scriptEl.text = script; - document.head.appendChild(scriptEl); - document.head.removeChild(scriptEl); -} - -export function load(url: string) { - const node: any = document.createElement('script'); - - // node.setAttribute('crossorigin', 'anonymous'); - - node.onload = onload; - node.onerror = onload; - - const i = createDefer(); - - function onload(e: any) { - node.onload = null; - node.onerror = null; - if (e.type === 'load') { - i.resolve(); - } else { - i.reject(); - } - // document.head.removeChild(node); - // node = null; - } - - // node.async = true; - node.src = url; - - document.head.appendChild(node); - - return i.promise(); -} - -export function evaluateExpression(expr: string) { - // eslint-disable-next-line no-new-func - const fn = new Function(expr); - return fn(); -} - -export function newFunction(args: string, code: string) { - try { - // eslint-disable-next-line no-new-func - return new Function(args, code); - } catch (e) { - console.warn('Caught error, Cant init func'); - return null; - } -} diff --git a/packages/rax-simulator-renderer/src/utils/style.ts b/packages/rax-simulator-renderer/src/utils/style.ts deleted file mode 100644 index 91dbbc634..000000000 --- a/packages/rax-simulator-renderer/src/utils/style.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { createDefer } from './create-defer'; - -export default class StylePoint { - private lastContent: string | undefined; - - private lastUrl: string | undefined; - - private placeholder: Element | Text; - - constructor(readonly level: number, readonly id?: string) { - let placeholder: any; - if (id) { - placeholder = document.head.querySelector(`style[data-id="${id}"]`); - } - if (!placeholder) { - placeholder = document.createTextNode(''); - const meta = document.head.querySelector(`meta[level="${level}"]`); - if (meta) { - document.head.insertBefore(placeholder, meta); - } else { - document.head.appendChild(placeholder); - } - } - this.placeholder = placeholder; - } - - applyText(content: string) { - if (this.lastContent === content) { - return; - } - this.lastContent = content; - this.lastUrl = undefined; - const element = document.createElement('style'); - element.setAttribute('type', 'text/css'); - if (this.id) { - element.setAttribute('data-id', this.id); - } - element.appendChild(document.createTextNode(content)); - document.head.insertBefore(element, this.placeholder.parentNode === document.head ? this.placeholder.nextSibling : null); - document.head.removeChild(this.placeholder); - this.placeholder = element; - } - - applyUrl(url: string) { - if (this.lastUrl === url) { - return; - } - this.lastContent = undefined; - this.lastUrl = url; - const element = document.createElement('link'); - element.onload = onload; - element.onerror = onload; - - const i = createDefer(); - function onload(e: any) { - element.onload = null; - element.onerror = null; - if (e.type === 'load') { - i.resolve(); - } else { - i.reject(); - } - } - - element.href = url; - element.rel = 'stylesheet'; - if (this.id) { - element.setAttribute('data-id', this.id); - } - document.head.insertBefore(element, this.placeholder.parentNode === document.head ? this.placeholder.nextSibling : null); - document.head.removeChild(this.placeholder); - this.placeholder = element; - return i.promise(); - } -} diff --git a/packages/rax-simulator-renderer/src/utils/url.ts b/packages/rax-simulator-renderer/src/utils/url.ts deleted file mode 100644 index d720323b3..000000000 --- a/packages/rax-simulator-renderer/src/utils/url.ts +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Parse queryString - * @param {String} str '?q=query&b=test' - * @return {Object} - */ -export function parseQuery(str: string): object { - const ret: any = {}; - - if (typeof str !== 'string') { - return ret; - } - - const s = str.trim().replace(/^(\?|#|&)/, ''); - - if (!s) { - return ret; - } - - s.split('&').forEach((param) => { - const parts = param.replace(/\+/g, ' ').split('='); - let key = parts.shift()!; - let val: any = parts.length > 0 ? parts.join('=') : undefined; - - key = decodeURIComponent(key); - - val = val === undefined ? null : decodeURIComponent(val); - - if (ret[key] === undefined) { - ret[key] = val; - } else if (Array.isArray(ret[key])) { - ret[key].push(val); - } else { - ret[key] = [ret[key], val]; - } - }); - - return ret; -} - -/** - * Stringify object to query parammeters - * @param {Object} obj - * @return {String} - */ -export function stringifyQuery(obj: any): string { - const param: string[] = []; - Object.keys(obj).forEach((key) => { - let value = obj[key]; - if (value && typeof value === 'object') { - value = JSON.stringify(value); - } - param.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`); - }); - return param.join('&'); -} - -export function uriEncode(uri: string) { - return encodeURIComponent(uri); -} - -export function uriDecode(uri: string) { - return decodeURIComponent(uri); -} - -export function withQueryParams(url: string, params?: object) { - const queryStr = params ? stringifyQuery(params) : ''; - if (queryStr === '') { - return url; - } - const urlSplit = url.split('#'); - const hash = urlSplit[1] ? `#${urlSplit[1]}` : ''; - const urlWithoutHash = urlSplit[0]; - return `${urlWithoutHash}${~urlWithoutHash.indexOf('?') ? '&' : '?'}${queryStr}${hash}`; -} diff --git a/packages/react-renderer/package.json b/packages/react-renderer/package.json index 2970e9341..b1b22f8f4 100644 --- a/packages/react-renderer/package.json +++ b/packages/react-renderer/package.json @@ -1,6 +1,6 @@ { "name": "@alilc/lowcode-react-renderer", - "version": "1.2.5", + "version": "1.3.0", "description": "react renderer for ali lowcode engine", "main": "lib/index.js", "module": "es/index.js", @@ -22,7 +22,7 @@ ], "dependencies": { "@alifd/next": "^1.21.16", - "@alilc/lowcode-renderer-core": "1.2.5" + "@alilc/lowcode-renderer-core": "1.3.0" }, "devDependencies": { "@alib/build-scripts": "^0.1.18", diff --git a/packages/react-simulator-renderer/package.json b/packages/react-simulator-renderer/package.json index 030a19708..49778b5df 100644 --- a/packages/react-simulator-renderer/package.json +++ b/packages/react-simulator-renderer/package.json @@ -1,6 +1,6 @@ { "name": "@alilc/lowcode-react-simulator-renderer", - "version": "1.2.5", + "version": "1.3.0", "description": "react simulator renderer for alibaba lowcode designer", "main": "lib/index.js", "module": "es/index.js", @@ -17,10 +17,10 @@ "test:cov": "build-scripts test --config build.test.json --jest-coverage" }, "dependencies": { - "@alilc/lowcode-designer": "1.2.5", - "@alilc/lowcode-react-renderer": "1.2.5", - "@alilc/lowcode-types": "1.2.5", - "@alilc/lowcode-utils": "1.2.5", + "@alilc/lowcode-designer": "1.3.0", + "@alilc/lowcode-react-renderer": "1.3.0", + "@alilc/lowcode-types": "1.3.0", + "@alilc/lowcode-utils": "1.3.0", "classnames": "^2.2.6", "mobx": "^6.3.0", "mobx-react": "^7.2.0", diff --git a/packages/renderer-core/package.json b/packages/renderer-core/package.json index fab0d3f13..bf2afdea1 100644 --- a/packages/renderer-core/package.json +++ b/packages/renderer-core/package.json @@ -1,6 +1,6 @@ { "name": "@alilc/lowcode-renderer-core", - "version": "1.2.5", + "version": "1.3.0", "description": "renderer core", "license": "MIT", "main": "lib/index.js", @@ -16,8 +16,8 @@ }, "dependencies": { "@alilc/lowcode-datasource-engine": "^1.0.0", - "@alilc/lowcode-types": "1.2.5", - "@alilc/lowcode-utils": "1.2.5", + "@alilc/lowcode-types": "1.3.0", + "@alilc/lowcode-utils": "1.3.0", "classnames": "^2.2.6", "debug": "^4.1.1", "fetch-jsonp": "^1.1.3", @@ -32,7 +32,7 @@ "devDependencies": { "@alib/build-scripts": "^0.1.18", "@alifd/next": "^1.26.0", - "@alilc/lowcode-designer": "1.2.5", + "@alilc/lowcode-designer": "1.3.0", "@babel/plugin-transform-typescript": "^7.16.8", "@testing-library/react": "^11.2.2", "@types/classnames": "^2.2.11", diff --git a/packages/renderer-core/src/adapter/index.ts b/packages/renderer-core/src/adapter/index.ts index 12896b137..7a56bc039 100644 --- a/packages/renderer-core/src/adapter/index.ts +++ b/packages/renderer-core/src/adapter/index.ts @@ -2,7 +2,6 @@ import { IRuntime, IRendererModules, IGeneralConstructor } from '../types'; export enum Env { React = 'react', - Rax = 'rax', } class Adapter { @@ -22,22 +21,22 @@ class Adapter { initRuntime() { const Component: IGeneralConstructor = class <T = any, S = any> { - setState() {} - forceUpdate() {} - render() {} state: Readonly<S>; props: Readonly<T> & Readonly<{ children?: any | undefined }>; refs: Record<string, unknown>; context: Record<string, unknown>; + setState() {} + forceUpdate() {} + render() {} }; const PureComponent = class <T = any, S = any> { - setState() {} - forceUpdate() {} - render() {} state: Readonly<S>; props: Readonly<T> & Readonly<{ children?: any | undefined }>; refs: Record<string, unknown>; context: Record<string, unknown>; + setState() {} + forceUpdate() {} + render() {} }; const createElement = () => {}; const createContext = () => {}; @@ -85,10 +84,6 @@ class Adapter { return this.env === Env.React; } - isRax() { - return this.env === Env.Rax; - } - setRenderers(renderers: IRendererModules) { this.renderers = renderers; } diff --git a/packages/renderer-core/tests/adapter/adapter.test.ts b/packages/renderer-core/tests/adapter/adapter.test.ts index a838602fb..57d92d1d4 100644 --- a/packages/renderer-core/tests/adapter/adapter.test.ts +++ b/packages/renderer-core/tests/adapter/adapter.test.ts @@ -79,15 +79,10 @@ describe('test src/adapter ', () => { }); - it('setEnv/.env/isReact/isRax works', () => { + it('setEnv/.env/isReact works', () => { adapter.setEnv(Env.React); expect(adapter.env).toBe(Env.React); expect(adapter.isReact()).toBeTruthy(); - expect(adapter.isRax()).toBeFalsy(); - adapter.setEnv(Env.Rax); - expect(adapter.env).toBe(Env.Rax); - expect(adapter.isRax()).toBeTruthy(); - expect(adapter.isReact()).toBeFalsy(); }); it('setRenderers/getRenderers works', () => { diff --git a/packages/shell/package.json b/packages/shell/package.json index 86c4c7d11..fba14598e 100644 --- a/packages/shell/package.json +++ b/packages/shell/package.json @@ -1,6 +1,6 @@ { "name": "@alilc/lowcode-shell", - "version": "1.2.5", + "version": "1.3.0", "description": "Shell Layer for AliLowCodeEngine", "main": "lib/index.js", "module": "es/index.js", @@ -13,12 +13,12 @@ }, "license": "MIT", "dependencies": { - "@alilc/lowcode-designer": "1.2.5", - "@alilc/lowcode-editor-core": "1.2.5", - "@alilc/lowcode-editor-skeleton": "1.2.5", - "@alilc/lowcode-types": "1.2.5", - "@alilc/lowcode-utils": "1.2.5", - "@alilc/lowcode-workspace": "1.2.5", + "@alilc/lowcode-designer": "1.3.0", + "@alilc/lowcode-editor-core": "1.3.0", + "@alilc/lowcode-editor-skeleton": "1.3.0", + "@alilc/lowcode-types": "1.3.0", + "@alilc/lowcode-utils": "1.3.0", + "@alilc/lowcode-workspace": "1.3.0", "classnames": "^2.2.6", "enzyme": "^3.11.0", "enzyme-adapter-react-16": "^1.15.5", diff --git a/packages/shell/src/api/common.tsx b/packages/shell/src/api/common.tsx index a2e4d8c4f..8ce07153a 100644 --- a/packages/shell/src/api/common.tsx +++ b/packages/shell/src/api/common.tsx @@ -26,6 +26,7 @@ import { IPublicApiCommonEditorCabin, IPublicModelDragon, IPublicModelSettingField, + IPublicTypeI18nData, } from '@alilc/lowcode-types'; import { SettingField as InnerSettingField, @@ -261,6 +262,10 @@ class Utils implements IPublicApiCommonUtils { } { return innerCreateIntl(instance); } + + intl(data: IPublicTypeI18nData | string, params?: object): any { + return innerIntl(data, params); + } } class EditorCabin implements IPublicApiCommonEditorCabin { diff --git a/packages/shell/src/api/commonUI.tsx b/packages/shell/src/api/commonUI.tsx new file mode 100644 index 000000000..0a9021fe6 --- /dev/null +++ b/packages/shell/src/api/commonUI.tsx @@ -0,0 +1,73 @@ +import { IPublicApiCommonUI, IPublicModelPluginContext, IPublicTypeContextMenuAction } from '@alilc/lowcode-types'; +import { + HelpTip, + IEditor, + Tip as InnerTip, + Title as InnerTitle, + } from '@alilc/lowcode-editor-core'; +import { Balloon, Breadcrumb, Button, Card, Checkbox, DatePicker, Dialog, Dropdown, Form, Icon, Input, Loading, Message, Overlay, Pagination, Radio, Search, Select, SplitButton, Step, Switch, Tab, Table, Tree, TreeSelect, Upload, Divider } from '@alifd/next'; +import { ContextMenu } from '../components/context-menu'; +import { editorSymbol } from '../symbols'; + +export class CommonUI implements IPublicApiCommonUI { + [editorSymbol]: IEditor; + + Balloon = Balloon; + Breadcrumb = Breadcrumb; + Button = Button; + Card = Card; + Checkbox = Checkbox; + DatePicker = DatePicker; + Dialog = Dialog; + Dropdown = Dropdown; + Form = Form; + Icon = Icon; + Input = Input; + Loading = Loading; + Message = Message; + Overlay = Overlay; + Pagination = Pagination; + Radio = Radio; + Search = Search; + Select = Select; + SplitButton = SplitButton; + Step = Step; + Switch = Switch; + Tab = Tab; + Table = Table; + Tree = Tree; + TreeSelect = TreeSelect; + Upload = Upload; + Divider = Divider; + + constructor(editor: IEditor) { + this[editorSymbol] = editor; + } + + get Tip() { + return InnerTip; + } + + get HelpTip() { + return HelpTip; + } + + get Title() { + return InnerTitle; + } + + get ContextMenu() { + const editor = this[editorSymbol]; + const innerContextMenu = (props: any) => { + const pluginContext: IPublicModelPluginContext = editor.get('pluginContext') as IPublicModelPluginContext; + return <ContextMenu {...props} pluginContext={pluginContext} />; + }; + + innerContextMenu.create = (menus: IPublicTypeContextMenuAction[], event: MouseEvent) => { + const pluginContext: IPublicModelPluginContext = editor.get('pluginContext') as IPublicModelPluginContext; + return ContextMenu.create(pluginContext, menus, event); + }; + + return innerContextMenu; + } +} diff --git a/packages/shell/src/api/index.ts b/packages/shell/src/api/index.ts index 4114926e1..3726020de 100644 --- a/packages/shell/src/api/index.ts +++ b/packages/shell/src/api/index.ts @@ -10,4 +10,5 @@ export * from './simulator-host'; export * from './skeleton'; export * from './canvas'; export * from './workspace'; -export * from './config'; \ No newline at end of file +export * from './config'; +export * from './commonUI'; \ No newline at end of file diff --git a/packages/shell/src/api/material.ts b/packages/shell/src/api/material.ts index 39b21848e..f0c37d8a4 100644 --- a/packages/shell/src/api/material.ts +++ b/packages/shell/src/api/material.ts @@ -13,6 +13,8 @@ import { IPublicTypeNpmInfo, IPublicModelEditor, IPublicTypeDisposable, + IPublicTypeContextMenuAction, + IPublicTypeContextMenuItem, } from '@alilc/lowcode-types'; import { Workspace as InnerWorkspace } from '@alilc/lowcode-workspace'; import { editorSymbol, designerSymbol } from '../symbols'; @@ -190,4 +192,16 @@ export class Material implements IPublicApiMaterial { dispose.forEach(d => d && d()); }; } + + addContextMenuOption(option: IPublicTypeContextMenuAction) { + this[designerSymbol].contextMenuActions.addMenuAction(option); + } + + removeContextMenuOption(name: string) { + this[designerSymbol].contextMenuActions.removeMenuAction(name); + } + + adjustContextMenuLayout(fn: (actions: IPublicTypeContextMenuItem[]) => IPublicTypeContextMenuItem[]) { + this[designerSymbol].contextMenuActions.adjustMenuLayout(fn); + } } diff --git a/packages/shell/src/components/context-menu.tsx b/packages/shell/src/components/context-menu.tsx new file mode 100644 index 000000000..c752e7990 --- /dev/null +++ b/packages/shell/src/components/context-menu.tsx @@ -0,0 +1,63 @@ +import { createContextMenu, parseContextMenuAsReactNode, parseContextMenuProperties } from '@alilc/lowcode-utils'; +import { engineConfig } from '@alilc/lowcode-editor-core'; +import { IPublicModelPluginContext, IPublicTypeContextMenuAction } from '@alilc/lowcode-types'; +import React from 'react'; + +export function ContextMenu({ children, menus, pluginContext }: { + menus: IPublicTypeContextMenuAction[]; + children: React.ReactElement[] | React.ReactElement; + pluginContext: IPublicModelPluginContext; +}): React.ReactElement<any, string | React.JSXElementConstructor<any>> { + if (!engineConfig.get('enableContextMenu')) { + return ( + <>{ children }</> + ); + } + + const handleContextMenu = (event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + + let destroyFn: Function | undefined; + const destroy = () => { + destroyFn?.(); + }; + const children: React.ReactNode[] = parseContextMenuAsReactNode(parseContextMenuProperties(menus, { + destroy, + pluginContext, + }), { pluginContext }); + + if (!children?.length) { + return; + } + + destroyFn = createContextMenu(children, { event }); + }; + + // 克隆 children 并添加 onContextMenu 事件处理器 + const childrenWithContextMenu = React.Children.map(children, (child) => + React.cloneElement( + child, + { onContextMenu: handleContextMenu }, + )); + + return ( + <>{childrenWithContextMenu}</> + ); +} + +ContextMenu.create = (pluginContext: IPublicModelPluginContext, menus: IPublicTypeContextMenuAction[], event: MouseEvent) => { + const children: React.ReactNode[] = parseContextMenuAsReactNode(parseContextMenuProperties(menus, { + pluginContext, + }), { + pluginContext, + }); + + if (!children?.length) { + return; + } + + return createContextMenu(children, { + event, + }); +}; \ No newline at end of file diff --git a/packages/shell/src/index.ts b/packages/shell/src/index.ts index 6f79c78bc..ce09ccaaa 100644 --- a/packages/shell/src/index.ts +++ b/packages/shell/src/index.ts @@ -28,6 +28,7 @@ import { Workspace, SimulatorHost, Config, + CommonUI, } from './api'; export * from './symbols'; @@ -68,4 +69,5 @@ export { Config, SettingField, SkeletonItem, + CommonUI, }; diff --git a/packages/types/package.json b/packages/types/package.json index 52a5e03ae..8d056e4ee 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@alilc/lowcode-types", - "version": "1.2.5", + "version": "1.3.0", "description": "Types for Ali lowCode engine", "files": [ "es", diff --git a/packages/types/src/shell/api/common.ts b/packages/types/src/shell/api/common.ts index 60fac1606..05ef0da17 100644 --- a/packages/types/src/shell/api/common.ts +++ b/packages/types/src/shell/api/common.ts @@ -1,6 +1,6 @@ import { Component, ReactNode } from 'react'; -import { IPublicTypeNodeSchema, IPublicTypeTitleContent } from '../type'; +import { IPublicTypeI18nData, IPublicTypeNodeSchema, IPublicTypeTitleContent } from '../type'; import { IPublicEnumTransitionType } from '../enum'; export interface IPublicApiCommonUtils { @@ -69,6 +69,11 @@ export interface IPublicApiCommonUtils { getLocale(): string; setLocale(locale: string): void; }; + + /** + * i18n 转换方法 + */ + intl(data: IPublicTypeI18nData | string, params?: object): string; } export interface IPublicApiCommonSkeletonCabin { diff --git a/packages/types/src/shell/api/commonUI.ts b/packages/types/src/shell/api/commonUI.ts new file mode 100644 index 000000000..5ac025fcd --- /dev/null +++ b/packages/types/src/shell/api/commonUI.ts @@ -0,0 +1,74 @@ +import React, { ReactElement } from 'react'; +import { IPublicTypeContextMenuAction, IPublicTypeHelpTipConfig, IPublicTypeTipConfig, IPublicTypeTitleContent } from '../type'; +import { Balloon, Breadcrumb, Button, Card, Checkbox, DatePicker, Dialog, Dropdown, Form, Icon, Input, Loading, Message, Overlay, Pagination, Radio, Search, Select, SplitButton, Step, Switch, Tab, Table, Tree, TreeSelect, Upload, Divider } from '@alifd/next'; +import { IconProps } from '@alifd/next/types/icon'; + +export interface IPublicApiCommonUI { + Balloon: typeof Balloon; + Breadcrumb: typeof Breadcrumb; + Button: typeof Button; + Card: typeof Card; + Checkbox: typeof Checkbox; + DatePicker: typeof DatePicker; + Dialog: typeof Dialog; + Dropdown: typeof Dropdown; + Form: typeof Form; + Icon: typeof Icon; + Input: typeof Input; + Loading: typeof Loading; + Message: typeof Message; + Overlay: typeof Overlay; + Pagination: typeof Pagination; + Radio: typeof Radio; + Search: typeof Search; + Select: typeof Select; + SplitButton: typeof SplitButton; + Step: typeof Step; + Switch: typeof Switch; + Tab: typeof Tab; + Table: typeof Table; + Tree: typeof Tree; + TreeSelect: typeof TreeSelect; + Upload: typeof Upload; + Divider: typeof Divider; + + /** + * Title 组件 + */ + get Tip(): React.ComponentClass<IPublicTypeTipConfig>; + + /** + * HelpTip 组件 + */ + get HelpTip(): React.VFC<{ + help: IPublicTypeHelpTipConfig; + + /** + * 方向 + * @default 'top' + */ + direction: IPublicTypeTipConfig['direction']; + + /** + * 大小 + * @default 'small' + */ + size: IconProps['size']; + }>; + + /** + * Tip 组件 + */ + get Title(): React.ComponentClass<{ + title: IPublicTypeTitleContent | undefined; + match?: boolean; + keywords?: string | null; + }>; + + get ContextMenu(): ((props: { + menus: IPublicTypeContextMenuAction[]; + children: React.ReactElement[] | React.ReactElement; + }) => ReactElement) & { + create(menus: IPublicTypeContextMenuAction[], event: MouseEvent | React.MouseEvent): void; + }; +} diff --git a/packages/types/src/shell/api/index.ts b/packages/types/src/shell/api/index.ts index 3d7d76559..79f1b0dc7 100644 --- a/packages/types/src/shell/api/index.ts +++ b/packages/types/src/shell/api/index.ts @@ -10,3 +10,4 @@ export * from './plugins'; export * from './logger'; export * from './canvas'; export * from './workspace'; +export * from './commonUI'; diff --git a/packages/types/src/shell/api/material.ts b/packages/types/src/shell/api/material.ts index d64455edd..89b2b39ad 100644 --- a/packages/types/src/shell/api/material.ts +++ b/packages/types/src/shell/api/material.ts @@ -1,4 +1,4 @@ -import { IPublicTypeAssetsJson, IPublicTypeMetadataTransducer, IPublicTypeComponentAction, IPublicTypeNpmInfo, IPublicTypeDisposable } from '../type'; +import { IPublicTypeAssetsJson, IPublicTypeMetadataTransducer, IPublicTypeComponentAction, IPublicTypeNpmInfo, IPublicTypeDisposable, IPublicTypeContextMenuAction, IPublicTypeContextMenuItem } from '../type'; import { IPublicModelComponentMeta } from '../model'; import { ComponentType } from 'react'; @@ -15,7 +15,7 @@ export interface IPublicApiMaterial { * set data for Assets * @returns void */ - setAssets(assets: IPublicTypeAssetsJson): void; + setAssets(assets: IPublicTypeAssetsJson): Promise<void>; /** * 获取「资产包」结构 @@ -128,4 +128,22 @@ export interface IPublicApiMaterial { * @since v1.1.7 */ refreshComponentMetasMap(): void; + + /** + * 添加右键菜单项 + * @param action + */ + addContextMenuOption(action: IPublicTypeContextMenuAction): void; + + /** + * 删除特定右键菜单项 + * @param name + */ + removeContextMenuOption(name: string): void; + + /** + * 调整右键菜单项布局 + * @param actions + */ + adjustContextMenuLayout(fn: (actions: IPublicTypeContextMenuItem[]) => IPublicTypeContextMenuItem[]): void; } diff --git a/packages/types/src/shell/enum/context-menu.ts b/packages/types/src/shell/enum/context-menu.ts new file mode 100644 index 000000000..fd209b197 --- /dev/null +++ b/packages/types/src/shell/enum/context-menu.ts @@ -0,0 +1,7 @@ +export enum IPublicEnumContextMenuType { + SEPARATOR = 'separator', + // 'menuItem' + MENU_ITEM = 'menuItem', + // 'nodeTree' + NODE_TREE = 'nodeTree', +} \ No newline at end of file diff --git a/packages/types/src/shell/enum/index.ts b/packages/types/src/shell/enum/index.ts index f3d558011..13282d0f2 100644 --- a/packages/types/src/shell/enum/index.ts +++ b/packages/types/src/shell/enum/index.ts @@ -3,4 +3,5 @@ export * from './transition-type'; export * from './transform-stage'; export * from './drag-object-type'; export * from './prop-value-changed-type'; -export * from './plugin-register-level'; \ No newline at end of file +export * from './plugin-register-level'; +export * from './context-menu'; \ No newline at end of file diff --git a/packages/types/src/shell/model/plugin-context.ts b/packages/types/src/shell/model/plugin-context.ts index 35e7e38af..45568424d 100644 --- a/packages/types/src/shell/model/plugin-context.ts +++ b/packages/types/src/shell/model/plugin-context.ts @@ -11,6 +11,7 @@ import { IPluginPreferenceMananger, IPublicApiPlugins, IPublicApiWorkspace, + IPublicApiCommonUI, } from '../api'; import { IPublicEnumPluginRegisterLevel } from '../enum'; import { IPublicModelEngineConfig, IPublicModelWindow } from './'; @@ -102,6 +103,12 @@ export interface IPublicModelPluginContext { */ get workspace(): IPublicApiWorkspace; + /** + * commonUI API + * @tutorial https://lowcode-engine.cn/site/docs/api/commonUI + */ + get commonUI(): IPublicApiCommonUI; + /** * 插件注册层级 * @since v1.1.7 diff --git a/packages/types/src/shell/type/context-menu.ts b/packages/types/src/shell/type/context-menu.ts new file mode 100644 index 000000000..dd6d583c2 --- /dev/null +++ b/packages/types/src/shell/type/context-menu.ts @@ -0,0 +1,63 @@ +import { IPublicEnumContextMenuType } from '../enum'; +import { IPublicModelNode } from '../model'; +import { IPublicTypeI18nData } from './i8n-data'; +import { IPublicTypeHelpTipConfig } from './widget-base-config'; + +export interface IPublicTypeContextMenuItem extends Omit<IPublicTypeContextMenuAction, 'condition' | 'disabled' | 'items'> { + disabled?: boolean; + + items?: Omit<IPublicTypeContextMenuItem, 'items'>[]; +} + +export interface IPublicTypeContextMenuAction { + + /** + * 动作的唯一标识符 + * Unique identifier for the action + */ + name: string; + + /** + * 显示的标题,可以是字符串或国际化数据 + * Display title, can be a string or internationalized data + */ + title?: string | IPublicTypeI18nData; + + /** + * 菜单项类型 + * Menu item type + * @see IPublicEnumContextMenuType + * @default IPublicEnumContextMenuType.MENU_ITEM + */ + type?: IPublicEnumContextMenuType; + + /** + * 点击时执行的动作,可选 + * Action to execute on click, optional + */ + action?: (nodes?: IPublicModelNode[], event?: MouseEvent) => void; + + /** + * 子菜单项或生成子节点的函数,可选,仅支持两级 + * Sub-menu items or function to generate child node, optional + */ + items?: Omit<IPublicTypeContextMenuAction, 'items'>[] | ((nodes?: IPublicModelNode[]) => Omit<IPublicTypeContextMenuAction, 'items'>[]); + + /** + * 显示条件函数 + * Function to determine display condition + */ + condition?: (nodes?: IPublicModelNode[]) => boolean; + + /** + * 禁用条件函数,可选 + * Function to determine disabled condition, optional + */ + disabled?: (nodes?: IPublicModelNode[]) => boolean; + + /** + * 帮助提示,可选 + */ + help?: IPublicTypeHelpTipConfig; +} + diff --git a/packages/types/src/shell/type/engine-options.ts b/packages/types/src/shell/type/engine-options.ts index f17716653..8221c4089 100644 --- a/packages/types/src/shell/type/engine-options.ts +++ b/packages/types/src/shell/type/engine-options.ts @@ -41,7 +41,7 @@ export interface IPublicTypeEngineOptions { /** * 渲染器类型,默认值:'react' */ - renderEnv?: 'react' | 'rax' | string; + renderEnv?: 'react' | string; /** * 设备类型映射器,处理设计器与渲染器中 device 的映射 @@ -177,6 +177,18 @@ export interface IPublicTypeEngineOptions { * 应用级设计模式下,自动打开第一个窗口 */ enableAutoOpenFirstWindow?: boolean; + + /** + * @default false + * 开启右键菜单能力 + */ + enableContextMenu?: boolean; + + /** + * @default false + * 隐藏设计器辅助层 + */ + hideComponentAction?: boolean; } /** diff --git a/packages/types/src/shell/type/index.ts b/packages/types/src/shell/type/index.ts index b2fd3313f..b1c7779d0 100644 --- a/packages/types/src/shell/type/index.ts +++ b/packages/types/src/shell/type/index.ts @@ -91,4 +91,5 @@ export * from './hotkey-callback-config'; export * from './hotkey-callbacks'; export * from './scrollable'; export * from './simulator-renderer'; -export * from './config-transducer'; \ No newline at end of file +export * from './config-transducer'; +export * from './context-menu'; \ No newline at end of file diff --git a/packages/types/src/shell/type/tip-config.ts b/packages/types/src/shell/type/tip-config.ts index fa82ab96f..f8b271c90 100644 --- a/packages/types/src/shell/type/tip-config.ts +++ b/packages/types/src/shell/type/tip-config.ts @@ -2,8 +2,20 @@ import { IPublicTypeI18nData } from '..'; import { ReactNode } from 'react'; export interface IPublicTypeTipConfig { + + /** + * className + */ className?: string; + + /** + * tip 的内容 + */ children?: IPublicTypeI18nData | ReactNode; theme?: string; + + /** + * tip 的方向 + */ direction?: 'top' | 'bottom' | 'left' | 'right'; } diff --git a/packages/types/src/shell/type/title-config.ts b/packages/types/src/shell/type/title-config.ts index 571502bc5..f8de28759 100644 --- a/packages/types/src/shell/type/title-config.ts +++ b/packages/types/src/shell/type/title-config.ts @@ -1,26 +1,51 @@ import { ReactNode } from 'react'; -import { IPublicTypeI18nData, IPublicTypeIconType, TipContent } from './'; +import { IPublicTypeI18nData, IPublicTypeIconType, IPublicTypeTitleContent, TipContent } from './'; + +export interface IPublicTypeTitleProps { + + /** + * 标题内容 + */ + title: IPublicTypeTitleContent; + + /** + * className + */ + className?: string; + + /** + * 点击事件 + */ + onClick?: () => void; + match?: boolean; + keywords?: string; +} /** * 描述 props 的 setter title */ export interface IPublicTypeTitleConfig { + /** * 文字描述 */ label?: IPublicTypeI18nData | ReactNode; + /** * hover 后的展现内容 */ tip?: TipContent; + /** * 文档链接,暂未实现 */ docUrl?: string; + /** * 图标 */ icon?: IPublicTypeIconType; + /** * CSS 类 */ diff --git a/packages/utils/package.json b/packages/utils/package.json index 976b76341..217204132 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,6 +1,6 @@ { "name": "@alilc/lowcode-utils", - "version": "1.2.5", + "version": "1.3.0", "description": "Utils for Ali lowCode engine", "files": [ "lib", @@ -14,7 +14,7 @@ }, "dependencies": { "@alifd/next": "^1.19.16", - "@alilc/lowcode-types": "1.2.5", + "@alilc/lowcode-types": "1.3.0", "lodash": "^4.17.21", "mobx": "^6.3.0", "react": "^16" diff --git a/packages/utils/src/context-menu.scss b/packages/utils/src/context-menu.scss new file mode 100644 index 000000000..0b75ca3ec --- /dev/null +++ b/packages/utils/src/context-menu.scss @@ -0,0 +1,50 @@ +.engine-context-menu-tree-wrap { + position: relative; + padding: 4px 10px 4px 32px; +} + +.engine-context-menu-tree-children { + margin-left: 8px; + line-height: 24px; +} + +.engine-context-menu-item { + .engine-context-menu-text { + color: var(--color-context-menu-text, var(--color-text)); + display: flex; + align-items: center; + + .lc-help-tip { + margin-left: 4px; + opacity: 0.8; + } + } + + &.disabled { + &:hover .engine-context-menu-text, .engine-context-menu-text { + color: var(--color-context-menu-text-disabled, var(--color-text-disabled)); + } + } + + &:hover { + .engine-context-menu-text { + color: var(--color-context-menu-text-hover, var(--color-title)); + } + } +} + +.engine-context-menu-title { + color: var(--color-context-menu-text, var(--color-text)); + cursor: pointer; + + &:hover { + background-color: var(--color-block-background-light); + color: var(--color-title); + } +} + +.engine-context-menu-tree-selecte-icon { + position: absolute; + left: 10px; + color: var(--color-icon-active); +} \ No newline at end of file diff --git a/packages/utils/src/context-menu.tsx b/packages/utils/src/context-menu.tsx new file mode 100644 index 000000000..d783a9643 --- /dev/null +++ b/packages/utils/src/context-menu.tsx @@ -0,0 +1,230 @@ +import { Menu, Icon } from '@alifd/next'; +import { IPublicEnumContextMenuType, IPublicModelNode, IPublicModelPluginContext, IPublicTypeContextMenuAction, IPublicTypeContextMenuItem } from '@alilc/lowcode-types'; +import { Logger } from '@alilc/lowcode-utils'; +import classNames from 'classnames'; +import React from 'react'; +import './context-menu.scss'; + +const logger = new Logger({ level: 'warn', bizName: 'designer' }); +const { Item, Divider, PopupItem } = Menu; + +const MAX_LEVEL = 2; + +interface IOptions { + nodes?: IPublicModelNode[] | null; + destroy?: Function; + pluginContext: IPublicModelPluginContext; +} + +const Tree = (props: { + node?: IPublicModelNode | null; + children?: React.ReactNode; + options: IOptions; +}) => { + const { node } = props; + + if (!node) { + return ( + <div className="engine-context-menu-tree-wrap">{ props.children }</div> + ); + } + + const { common } = props.options.pluginContext || {}; + const { intl } = common?.utils || {}; + const indent = node.zLevel * 8 + 32; + const style = { + paddingLeft: indent, + marginLeft: -indent, + marginRight: -10, + paddingRight: 10, + }; + + return ( + <Tree {...props} node={node.parent} > + <div + className="engine-context-menu-title" + onClick={() => { + props.options.destroy?.(); + node.select(); + }} + style={style} + > + {props.options.nodes?.[0].id === node.id ? (<Icon className="engine-context-menu-tree-selecte-icon" size="small" type="success" />) : null} + {intl(node.title)} + </div> + <div + className="engine-context-menu-tree-children" + > + { props.children } + </div> + </Tree> + ); +}; + +let destroyFn: Function | undefined; + +export function parseContextMenuAsReactNode(menus: IPublicTypeContextMenuItem[], options: IOptions): React.ReactNode[] { + const { common, commonUI } = options.pluginContext || {}; + const { intl = (title: any) => title } = common?.utils || {}; + const { HelpTip } = commonUI || {}; + + const children: React.ReactNode[] = []; + menus.forEach((menu, index) => { + if (menu.type === IPublicEnumContextMenuType.SEPARATOR) { + children.push(<Divider key={menu.name || index} />); + return; + } + + if (menu.type === IPublicEnumContextMenuType.MENU_ITEM) { + if (menu.items && menu.items.length) { + children.push(( + <PopupItem + className={classNames('engine-context-menu-item', { + disabled: menu.disabled, + })} + key={menu.name} + label={<div className="engine-context-menu-text">{intl(menu.title)}</div>} + > + <Menu className="next-context engine-context-menu"> + { parseContextMenuAsReactNode(menu.items, options) } + </Menu> + </PopupItem> + )); + } else { + children.push(( + <Item + className={classNames('engine-context-menu-item', { + disabled: menu.disabled, + })} + disabled={menu.disabled} + onClick={() => { + menu.action?.(); + }} + key={menu.name} + > + <div className="engine-context-menu-text"> + { menu.title ? intl(menu.title) : null } + { menu.help ? <HelpTip size="xs" help={menu.help} direction="right" /> : null } + </div> + </Item> + )); + } + } + + if (menu.type === IPublicEnumContextMenuType.NODE_TREE) { + children.push(( + <Tree node={options.nodes?.[0]} options={options} /> + )); + } + }); + + return children; +} + +export function parseContextMenuProperties(menus: (IPublicTypeContextMenuAction | Omit<IPublicTypeContextMenuAction, 'items'>)[], options: IOptions & { + event?: MouseEvent; +}, level = 1): IPublicTypeContextMenuItem[] { + destroyFn?.(); + + const { nodes, destroy } = options; + if (level > MAX_LEVEL) { + logger.warn('context menu level is too deep, please check your context menu config'); + return []; + } + + return menus + .filter(menu => !menu.condition || (menu.condition && menu.condition(nodes || []))) + .map((menu) => { + const { + name, + title, + type = IPublicEnumContextMenuType.MENU_ITEM, + help, + } = menu; + + const result: IPublicTypeContextMenuItem = { + name, + title, + type, + help, + action: () => { + destroy?.(); + menu.action?.(nodes || [], options.event); + }, + disabled: menu.disabled && menu.disabled(nodes || []) || false, + }; + + if ('items' in menu && menu.items) { + result.items = parseContextMenuProperties( + typeof menu.items === 'function' ? menu.items(nodes || []) : menu.items, + options, + level + 1, + ); + } + + return result; + }) + .reduce((menus: IPublicTypeContextMenuItem[], currentMenu: IPublicTypeContextMenuItem) => { + if (!currentMenu.name) { + return menus.concat([currentMenu]); + } + + const index = menus.find(item => item.name === currentMenu.name); + if (!index) { + return menus.concat([currentMenu]); + } else { + return menus; + } + }, []); +} + +let cachedMenuItemHeight: string | undefined; + +function getMenuItemHeight() { + if (cachedMenuItemHeight) { + return cachedMenuItemHeight; + } + const root = document.documentElement; + const styles = getComputedStyle(root); + const menuItemHeight = styles.getPropertyValue('--context-menu-item-height').trim(); + cachedMenuItemHeight = menuItemHeight; + + return menuItemHeight; +} + +export function createContextMenu(children: React.ReactNode[], { + event, + offset = [0, 0], +}: { + event: MouseEvent | React.MouseEvent; + offset?: [number, number]; +}) { + event.preventDefault(); + event.stopPropagation(); + + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + const dividerCount = React.Children.count(children.filter(child => React.isValidElement(child) && child.type === Divider)); + const popupItemCount = React.Children.count(children.filter(child => React.isValidElement(child) && (child.type === PopupItem || child.type === Item))); + const menuHeight = popupItemCount * parseInt(getMenuItemHeight(), 10) + dividerCount * 8 + 16; + const menuWidthLimit = 200; + let x = event.clientX + offset[0]; + let y = event.clientY + offset[1]; + if (x + menuWidthLimit > viewportWidth) { + x = x - menuWidthLimit; + } + if (y + menuHeight > viewportHeight) { + y = y - menuHeight; + } + + const menuInstance = Menu.create({ + target: document.body, + offset: [x, y], + children, + className: 'engine-context-menu', + }); + + destroyFn = (menuInstance as any).destroy; + + return destroyFn; +} \ No newline at end of file diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 3e37f46ba..b99ab0207 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -31,3 +31,4 @@ export * as css from './css-helper'; export { transactionManager } from './transaction-manager'; export * from './check-types'; export * from './workspace'; +export * from './context-menu'; diff --git a/packages/workspace/package.json b/packages/workspace/package.json index cbc31f379..3c58c040e 100644 --- a/packages/workspace/package.json +++ b/packages/workspace/package.json @@ -1,6 +1,6 @@ { "name": "@alilc/lowcode-workspace", - "version": "1.2.5", + "version": "1.3.0", "description": "Shell Layer for AliLowCodeEngine", "main": "lib/index.js", "module": "es/index.js", @@ -15,11 +15,11 @@ }, "license": "MIT", "dependencies": { - "@alilc/lowcode-designer": "1.2.5", - "@alilc/lowcode-editor-core": "1.2.5", - "@alilc/lowcode-editor-skeleton": "1.2.5", - "@alilc/lowcode-types": "1.2.5", - "@alilc/lowcode-utils": "1.2.5", + "@alilc/lowcode-designer": "1.3.0", + "@alilc/lowcode-editor-core": "1.3.0", + "@alilc/lowcode-editor-skeleton": "1.3.0", + "@alilc/lowcode-types": "1.3.0", + "@alilc/lowcode-utils": "1.3.0", "classnames": "^2.2.6", "enzyme": "^3.11.0", "enzyme-adapter-react-16": "^1.15.5", diff --git a/packages/workspace/src/context/base-context.ts b/packages/workspace/src/context/base-context.ts index 78f32079f..2f6154788 100644 --- a/packages/workspace/src/context/base-context.ts +++ b/packages/workspace/src/context/base-context.ts @@ -32,6 +32,7 @@ import { Workspace, Window, Canvas, + CommonUI, } from '@alilc/lowcode-shell'; import { IPluginPreferenceMananger, @@ -127,6 +128,7 @@ export class BasicContext implements IBasicContext { const logger = getLogger({ level: 'warn', bizName: 'common' }); const skeleton = new Skeleton(innerSkeleton, 'any', true); const canvas = new Canvas(editor, true); + const commonUI = new CommonUI(editor); editor.set('setters', setters); editor.set('project', project); editor.set('material', material); @@ -166,11 +168,13 @@ export class BasicContext implements IBasicContext { context.plugins = plugins; context.logger = new Logger({ level: 'warn', bizName: `plugin:${pluginName}` }); context.canvas = canvas; + context.commonUI = commonUI; if (editorWindow) { context.editorWindow = new Window(editorWindow); } context.registerLevel = registerLevel; context.isPluginRegisteredInWorkspace = registerLevel === IPublicEnumPluginRegisterLevel.Workspace; + editor.set('pluginContext', context); }, }; diff --git a/scripts/build.sh b/scripts/build.sh index 92b0a99c2..0d96598e6 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -9,8 +9,6 @@ lerna run build \ --scope @alilc/lowcode-designer \ --scope @alilc/lowcode-plugin-designer \ --scope @alilc/lowcode-plugin-outline-pane \ - --scope @alilc/lowcode-rax-renderer \ - --scope @alilc/lowcode-rax-simulator-renderer \ --scope @alilc/lowcode-react-renderer \ --scope @alilc/lowcode-react-simulator-renderer \ --scope @alilc/lowcode-renderer-core \ @@ -20,13 +18,9 @@ lerna run build \ lerna run build:umd \ --scope @alilc/lowcode-engine \ - --scope @alilc/lowcode-rax-simulator-renderer \ --scope @alilc/lowcode-react-simulator-renderer \ --scope @alilc/lowcode-react-renderer \ --stream cp ./packages/react-simulator-renderer/dist/js/* ./packages/engine/dist/js/ cp ./packages/react-simulator-renderer/dist/css/* ./packages/engine/dist/css/ - -cp ./packages/rax-simulator-renderer/dist/js/* ./packages/engine/dist/js/ -cp ./packages/rax-simulator-renderer/dist/css/* ./packages/engine/dist/css/ \ No newline at end of file diff --git a/scripts/sync.sh b/scripts/sync.sh index 98895c723..e9840eeca 100755 --- a/scripts/sync.sh +++ b/scripts/sync.sh @@ -10,8 +10,6 @@ tnpm sync @alilc/lowcode-designer tnpm sync @alilc/lowcode-plugin-designer tnpm sync @alilc/lowcode-plugin-outline-pane tnpm sync @alilc/lowcode-renderer-core -tnpm sync @alilc/lowcode-rax-renderer -tnpm sync @alilc/lowcode-rax-simulator-renderer tnpm sync @alilc/lowcode-react-renderer tnpm sync @alilc/lowcode-react-simulator-renderer tnpm sync @alilc/lowcode-engine