diff --git a/packages/react-renderer/.babelrc b/packages/react-renderer/.babelrc new file mode 100644 index 000000000..6c1eff28e --- /dev/null +++ b/packages/react-renderer/.babelrc @@ -0,0 +1,11 @@ +{ + "presets": [ + "@babel/preset-env", + "@babel/preset-react" + ], + "plugins": [ + "@babel/plugin-syntax-dynamic-import", + "@babel/plugin-proposal-class-properties", + "@babel/plugin-transform-runtime" + ] +} diff --git a/packages/react-renderer/.eslintignore b/packages/react-renderer/.eslintignore new file mode 100644 index 000000000..3b437e614 --- /dev/null +++ b/packages/react-renderer/.eslintignore @@ -0,0 +1,11 @@ +# 忽略目录 +build/ +tests/ +demo/ + +# node 覆盖率文件 +coverage/ + +# 忽略文件 +**/*-min.js +**/*.min.js diff --git a/packages/react-renderer/.eslintrc.js b/packages/react-renderer/.eslintrc.js new file mode 100644 index 000000000..68e745d60 --- /dev/null +++ b/packages/react-renderer/.eslintrc.js @@ -0,0 +1,77 @@ +module.exports = { + "root": true, + "extends": [ + "eslint:recommended", + "plugin:react/recommended" + ], + "plugins": [ + "react" + ], + "env": { + "browser": true, + "node": true, + "es6":true + }, + "parser": "babel-eslint", + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "module", + "ecmaFeatures": { + "jsx": true, + "experimentalObjectRestSpread": true + } + }, + "rules": { + "react/no-deprecated": 0, // react15.x关闭deprated警告 + "constructor-super": 2,//要求在构造函数中有 super() 的调用 + "no-case-declarations": 2,//不允许在 case 子句中使用词法声明 + "no-class-assign": 2,//禁止修改类声明的变量 + "no-compare-neg-zero": 2,//禁止负0比较 + "no-cond-assign": 2,//禁止条件表达式中出现赋值操作符 + "no-console": [2, { + "allow": ["info", "warn", "error"] + }],//禁止console + "no-const-assign": 2,//禁止修改 const 声明的变量 + "no-constant-condition": 2,//禁止在条件中使用常量表达式 + "no-control-regex": 2,//禁止在正则表达式中使用控制字符 + "no-debugger": 2,//禁止debugger + "no-delete-var": 2,//禁止删除变量 + "no-dupe-args": 2,//禁止重复的参数 + "no-dupe-class-members": 2,//禁止类成员中出现重复的名称 + "no-dupe-keys": 2,//禁止重复的键值 + "no-duplicate-case": 2,//禁止重复的case条件 + "no-empty-character-class": 2,//禁止在正则表达式中使用空字符集 + "no-empty-pattern": 2,//禁止使用空解构模式 + "no-empty": 2,//禁止出现空语句块 + "no-ex-assign": 2,//禁止对 catch 子句的参数重新赋值 + "no-extra-boolean-cast": 2,//禁止不必要的布尔转换 + "no-extra-semi": 2,//禁止多余的分号 + "no-fallthrough": 2,//禁止 case 语句落空 + "no-func-assign": 2,//禁止对 function 声明重新赋值 + "no-global-assign": 2,//禁止对全局对象重新赋值 + "no-inner-declarations": 2,//禁止在嵌套的块中出现变量声明或 function 声明 + "no-invalid-regexp": 2,//禁止 RegExp 构造函数中存在无效的正则表达式字符串 + "no-irregular-whitespace": 2,//禁止在字符串和注释之外不规则的空白 + "no-mixed-spaces-and-tabs": 2,//禁止空格和 tab 的混合缩进 + "no-new-symbol": 2,//禁止对Symbol使用new关键字 + "no-obj-calls": 2,//禁止把全局对象作为函数调用 + "no-octal": 2,//禁止8进制的字面量 + "no-redeclare": 2,//禁止多次声明同一变量 + "no-regex-spaces": 2,//禁止正则表达式字面量中出现多个空格 + "no-self-assign": 2,//禁止自我赋值 + "no-sparse-arrays": 2,//禁用稀疏数组 + "no-this-before-super": 2,//禁止在构造函数中,在调用 super() 之前使用 this 或 super + "no-undef": 2,//禁用未声明的变量,除非它们在 /*global */ 注释中被提到 + "no-unexpected-multiline": 2,//禁止出现令人困惑的多行表达式 + "no-unreachable": 2,//禁止在return、throw、continue 和 break 语句之后出现不可达代码 + "no-unsafe-finally": 2,//禁止在 finally 语句块中出现控制流语句 + "no-unsafe-negation": 2,//禁止在表达式左侧使用关系表达式 + "no-unused-labels": 2,//禁用出现未使用过的标 + "no-unused-vars": 2,//禁止出现未使用过的变量 + "no-useless-escape": 2,//禁用不必要的转义字符 + "require-yield": 2,//要求 generator 函数内有 yield + "use-isnan": 2,//使用isNan() 判断isNaN + "valid-typeof": 2//强制 typeof 表达式与有效的字符串进行比较 + }, + "settings": {} +} \ No newline at end of file diff --git a/packages/react-renderer/.gitignore b/packages/react-renderer/.gitignore new file mode 100644 index 000000000..96d21acc5 --- /dev/null +++ b/packages/react-renderer/.gitignore @@ -0,0 +1,18 @@ +# See https://help.github.com/ignore-files/ for more about ignoring files. + +# dependencies +/node_modules + +# production +/build +/dist +/lib + +# misc +.idea/ +.happypack +.DS_Store + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/packages/react-renderer/.npmignore b/packages/react-renderer/.npmignore new file mode 100644 index 000000000..dbc869080 --- /dev/null +++ b/packages/react-renderer/.npmignore @@ -0,0 +1 @@ +/src \ No newline at end of file diff --git a/packages/react-renderer/.prettierrc b/packages/react-renderer/.prettierrc new file mode 100644 index 000000000..d3c963559 --- /dev/null +++ b/packages/react-renderer/.prettierrc @@ -0,0 +1,4 @@ +{ + "printWidth": 120, + "singleQuote": true +} \ No newline at end of file diff --git a/packages/react-renderer/CHANGELOG.md b/packages/react-renderer/CHANGELOG.md new file mode 100644 index 000000000..b7e062e68 --- /dev/null +++ b/packages/react-renderer/CHANGELOG.md @@ -0,0 +1,84 @@ +# 更新日志 + +## [1.0.0] - 2019-11-13 +### 新增 +* iceluna-sdk基本功能; + +## [1.0.1] - 2019-11-26 +### 新增 +* 粘贴schema时判断如果为非法schema触发illegalSchema.paste消息; +* 增加schema.paste 和 schema.copy消息; +* schema中支持__ignoreParse,标记不需解析成ReactDom的schema结构; +* 复制组件后,高亮选中新增的组件; + +### 优化 +* 修改sdk入口文件; +* engine的hidden属性改为suspended; +* websocket重连delay时间改为3s; + +### 修复 +* 当异步请求返回数据中success为false的时候,依然走请求成功回调,由用户自己判断错误处理方式; +* 修复预发发布以后异步请求配置变量不解析问题; +* 修复初始数据重新获取导致调试字段t不更新问题; +* 修复异步请求配置参数变量解析时机不及时导致参数解析出错问题; + +## [1.0.2] - 2019-12-16 +### 新增 +* 画布支持缩放以及placeholder设置; +* window上挂载react和reactDOM; +* 支持国际化; +* 画布支持layout设置; + +### 优化 +* 若schema中的loop或者loopArgs变成默认值时删除该属性字段; +* 扩展模式时组件最小高度调整为10px; +* 采用react.forward透传compFactory和addonFactory高阶组件ref; +* 插件通过context透传国际化配置; +* 画布最外层组件支持设置固定宽高; + +### 修复 +* 修发布后的页面被嵌入到iframe中变量解析时的跨域问题; +* 修复form循环问题; +* 修复模型结构中null及undefined被转成空对象问题; +* 修复fetch请求类型为PUT和DELETE时参数序列化问题; +* 修复form自定义设置key导致的scopeprops不传递问题; +* 修复beforeRequest和afterRequest中this上下文丢失问题; + +## [1.0.3] - 2019-12-24 +### 新增 +* compFactory和addonFactory增加version static字段; +* 接入小二工作台fetch库; +* 增加标准搭建协议转换API; +* 上下文增加React Router的match属性; + +### 优化 +* 错误码非200时依旧解析请求返回结果; +* 增加编辑器环境判断并兼容vscode复制粘贴操作; +* 国际化语言判断的边界校验; + +### 修复 +* 修复当children设置为变量时且condition为false时报错问题; +* 修复未支持国际化的插件报错问题; +* 修复canvas获取viewport不存在时的报错问题; + +## [1.0.4] - 2020-01-13 +### 新增 +* 增加黄金令箭埋点API; +* 新增ReadMe; +* 渲染引擎支持Flagment组件; +* 容器类组件支持自动loading及占位高度属性配置; + +### 优化 +* schema序列化增加unsafe选项设置; +* 优化自定义组件预览逻辑; +* 渲染引擎scope、ctx和self上下文构造重构; +* 异步数据请求API兼容callback写法; +* bzb增加根据url参数控制环境; + +### 修复 +* 修复无内容状态容器展示异常的问题; +* 修复自定义组件children属性中添加Block导致Block内部组件无法进行依赖分析,从而无法展示问题; +* 修复componentsMap获取失败问题; +* 升级bzb-request; +* 修复初始数据源无内容返回导致容器组件不重新渲染问题; +* 修复单独使用this时,变量解析过程中this转义问题; \ No newline at end of file diff --git a/packages/react-renderer/CONTRIBUTING.md b/packages/react-renderer/CONTRIBUTING.md new file mode 100644 index 000000000..515ead055 --- /dev/null +++ b/packages/react-renderer/CONTRIBUTING.md @@ -0,0 +1,2 @@ +# Contributing Guidelines +TODO \ No newline at end of file diff --git a/packages/react-renderer/README.md b/packages/react-renderer/README.md index 486fc0f53..2778b215e 100644 --- a/packages/react-renderer/README.md +++ b/packages/react-renderer/README.md @@ -1 +1,62 @@ -react 渲染模块 +# iceluna-sdk + +> iceluna 底层引擎渲染能力。 + +## Table of Contents + +- [Background](#background) +- [Install](#install) +- [Develop](#Develop) +- [Structure](#Structure) +- [Publish](#Publish) +- [Maintainers](#maintainers) +- [Contributing](#contributing) +- [License](#license) + +## Background +iceluna 是由淘系技术部研发,面向中后台应用低代码搭建的通用解决方案,集前端应用工程创建、开发、调试及发布的全链路一体化的低代码搭建平台。同时,基于集团中后台 《搭建基础协议》 和 《物料协议》 标准之上,生产低代码搭建物料,沉淀搭建基础设施,助力上层不同业务领域 孵化低代码搭建产品,目标 打造成为基于集团标准的低代码搭建中台。 + +iceluna 代码整个项目结构如下图。 +![iceluna代码仓库结构](https://img.alicdn.com/tfs/TB1KJThsHr1gK0jSZR0XXbP8XXa-660-322.png) + +该项目 iceluna-SDK 为 iceluna 通用解决方案的提供最底层,最核心的渲染引擎能力。为 iceluna 体系上层可视化编辑器提供基础引擎能力。 + +## Install + +```sh +# 前提:tnpm install @ali/iceluna-cli -g +tnpm i +``` + +## Develop +1. 执行 `tnpm link`, 在全局建立符号链接。 +2. 到[IDE项目](http://gitlab.alibaba-inc.com/iceluna/iceluna-IDE)启动前,执行 `tnpm link @ali/iceluna-sdk`,建立连接。 +3. 按照 `iceluna-IDE` 方式调试 + +## Structure +``` +. +└── src + ├── comp + ├── context + ├── engine + ├── hoc + ├── utils +``` + +## Publish + +## Maintainers + +[@月飞](dingtalk://dingtalkclient/action/sendmsg?dingtalk_id=qhkv9cp), [@下羊](dingtalk://dingtalkclient/action/sendmsg?dingtalk_id=r93lhx4) + +## Contributing + +See [the contributing file](CONTRIBUTING.md)! + +PRs accepted. + + +## License + +MIT © 2019 diff --git a/packages/react-renderer/demo/example/index.jsx b/packages/react-renderer/demo/example/index.jsx new file mode 100644 index 000000000..bad8f02c0 --- /dev/null +++ b/packages/react-renderer/demo/example/index.jsx @@ -0,0 +1,21 @@ +import React, { PureComponent } from 'react'; + +import Widget from '_'; + +import './index.less'; + +export default class Example extends PureComponent { + static displayName = 'example'; + constructor(props) { + super(props); + } + + render() { + return ( +
+ This is an example + +
+ ); + } +} diff --git a/packages/react-renderer/demo/example/index.less b/packages/react-renderer/demo/example/index.less new file mode 100644 index 000000000..b534fcd55 --- /dev/null +++ b/packages/react-renderer/demo/example/index.less @@ -0,0 +1,2 @@ +.example { +} diff --git a/packages/react-renderer/demo/index.json b/packages/react-renderer/demo/index.json new file mode 100644 index 000000000..4046bd727 --- /dev/null +++ b/packages/react-renderer/demo/index.json @@ -0,0 +1,3 @@ +{ + "example": true +} diff --git a/packages/react-renderer/package-lock.json b/packages/react-renderer/package-lock.json new file mode 100644 index 000000000..f5f8796eb --- /dev/null +++ b/packages/react-renderer/package-lock.json @@ -0,0 +1,11 @@ +{ + "requires": true, + "lockfileVersion": 1, + "dependencies": { + "prettier": { + "version": "1.19.1", + "resolved": "https://registry.npm.alibaba-inc.com/prettier/download/prettier-1.19.1.tgz", + "integrity": "sha1-99f1/4qc2HKnvkyhQglZVqYHl8s=" + } + } +} diff --git a/packages/react-renderer/package.json b/packages/react-renderer/package.json new file mode 100644 index 000000000..fd8e46e29 --- /dev/null +++ b/packages/react-renderer/package.json @@ -0,0 +1,71 @@ +{ + "name": "@ali/lowcode-engine-react-renderer", + "version": "0.0.1", + "description": "lowcode engine react renderer", + "main": "lib/index.js", + "scripts": { + "babel": "rm -rf lib && babel src -d lib --copy-files", + "prettier": "prettier --write \"./src/**/*.{js,jsx,ejs,less,css,scss,json}\" \"./demo/**/*.{js,jsx,ejs,less,css,scss,json}\"", + "build": "npm run babel", + "prepublish": "npm run prettier && npm run babel" + }, + "repository": { + "type": "git", + "url": "git@gitlab.alibaba-inc.com:ali-lowcode/ali-lowcode-engine.git" + }, + "keywords": [ + "lowcode", + "engine", + "react" + ], + "author": "xiayang.xy", + "license": "ISC", + "dependencies": { + "@ali/b3-one": "^0.0.17", + "@ali/bzb-request": "^2.6.0-beta.13", + "@ali/iceluna-comp-div": "^0.0.5", + "@ali/iceluna-rax": "0.0.5", + "@ali/lib-mtop": "^2.5.1", + "@alifd/next": "^1.18.17", + "debug": "^4.1.1", + "driver-universal": "^3.1.2", + "events": "^3.0.0", + "fetch-jsonp": "^1.1.3", + "intl-messageformat": "^7.7.2", + "jsonuri": "^2.1.2", + "keymaster": "^1.6.2", + "localforage": "^1.7.3", + "lodash": "^4.17.11", + "moment": "^2.24.0", + "rax": "^1.1.1", + "rax-find-dom-node": "^1.0.1", + "react-is": "^16.10.1", + "serialize-javascript": "^1.7.0", + "socket.io-client": "^2.2.0", + "whatwg-fetch": "^3.0.0" + }, + "peerDependencies": { + "react": "^16.8.6", + "react-dom": "^16.8.6", + "prop-types": "^15.7.2" + }, + "devDependencies": { + "@babel/cli": "^7.4.4", + "@babel/core": "^7.4.4", + "@babel/plugin-proposal-class-properties": "^7.0.0", + "@babel/plugin-proposal-decorators": "^7.0.0", + "@babel/plugin-syntax-dynamic-import": "^7.0.0", + "@babel/plugin-transform-runtime": "^7.4.4", + "@babel/preset-env": "^7.4.4", + "@babel/preset-react": "^7.0.0", + "babel-eslint": "^9.0.0", + "eslint": "^4.15.0", + "eslint-config-airbnb": "^17.1.0", + "eslint-loader": "^1.9.0", + "eslint-plugin-react": "^7.12.4", + "prettier": "^1.12.1" + }, + "publishConfig": { + "registry": "http://registry.npm.alibaba-inc.com" + } +} diff --git a/packages/react-renderer/src/adapter/rax.jsx b/packages/react-renderer/src/adapter/rax.jsx new file mode 100644 index 000000000..ceef88556 --- /dev/null +++ b/packages/react-renderer/src/adapter/rax.jsx @@ -0,0 +1,33 @@ +import { createElement, render, useState } from 'rax'; +import React, { PureComponent } from 'react'; +import DriverUniversal from 'driver-universal'; +import { Engine } from '@ali/iceluna-rax'; +import findDOMNode from 'rax-find-dom-node'; + +let updateRax = () => {}; + +export default class Rax extends PureComponent { + constructor(props) { + super(props); + } + + componentDidMount() { + const RaxEngine = () => { + const [config, setConfig] = useState(this.props); + updateRax = setConfig; + return createElement(Engine, { + ...config + }); + }; + render(createElement(RaxEngine), document.getElementById('luna-rax-container'), { driver: DriverUniversal }); + } + componentDidUpdate() { + updateRax(this.props); + } + + render() { + return
; + } +} + +Rax.findDOMNode = findDOMNode; diff --git a/packages/react-renderer/src/comp/addon/index.jsx b/packages/react-renderer/src/comp/addon/index.jsx new file mode 100644 index 000000000..256fe6263 --- /dev/null +++ b/packages/react-renderer/src/comp/addon/index.jsx @@ -0,0 +1,87 @@ +import { PureComponent } from 'react'; +import PropTypes from 'prop-types'; + +import AppContext from '../../context/appContext'; +import { isEmpty, generateI18n, goldlog } from '../../utils'; + +export default class Addon extends PureComponent { + static displayName = 'lunaAddon'; + static propTypes = { + config: PropTypes.object, + locale: PropTypes.string, + messages: PropTypes.object + }; + static defaultProps = { + config: {} + }; + static contextType = AppContext; + constructor(props, context) { + super(props, context); + if (isEmpty(props.config) || !props.config.addonKey) { + console.warn('luna addon has wrong config'); + return; + } + // 插件上下文中的appHelper使用IDE的appHelper + context.appHelper = (window.__ctx && window.__ctx.appHelper) || context.appHelper; + context.locale = props.locale; + context.messages = props.messages; + // 注册插件 + this.appHelper = context.appHelper; + let { locale, messages } = props; + this.i18n = generateI18n(locale, messages); + this.addonKey = props.config.addonKey; + this.appHelper.addons = this.appHelper.addons || {}; + this.appHelper.addons[this.addonKey] = this; + } + + async componentWillUnmount() { + // 销毁插件 + const config = this.props.config || {}; + if (config && this.appHelper.addons) { + delete this.appHelper.addons[config.addonKey]; + } + } + + open = () => { + return true; + }; + + close = () => { + return true; + }; + + goldlog = (goKey, params) => { + const { addonKey, addonConfig = {} } = this.props.config || {}; + goldlog( + goKey, + { + addonKey, + package: addonConfig.package, + version: addonConfig.version, + ...this.appHelper.logParams, + ...params + }, + 'addon' + ); + }; + + get utils() { + return this.appHelper.utils; + } + + get constants() { + return this.appHelper.constants; + } + + get history() { + return this.appHelper.history; + } + + get location() { + return this.appHelper.location; + } + + render() { + return null; + } +} diff --git a/packages/react-renderer/src/comp/canvas/index.jsx b/packages/react-renderer/src/comp/canvas/index.jsx new file mode 100644 index 000000000..f112a3d0f --- /dev/null +++ b/packages/react-renderer/src/comp/canvas/index.jsx @@ -0,0 +1,729 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { on, off } from '@ali/b3-one/lib/event'; +import AppHelper from '../../utils/appHelper'; +import SchemaHelper from '../../utils/schemaHelper'; +import DndHelper from '../../utils/dndHelper'; +import Engine from '../../engine'; + +import CompFactory from '../../hoc/compFactory'; +import { + isSchema, + isFileSchema, + isEmpty, + isJSSlot, + jsonuri, + registShortCuts, + unRegistShortCuts, + generateUtils, + parseObj, + shallowEqual, + addCssTag, + transformSchemaToPure, + goldlog +} from '../../utils'; +import './index.scss'; + +const DESIGN_MODE = { + EXTEND: 'extend', + BORDER: 'border', + PREVIEW: 'preview' +}; + +const DEFAULT_PLACEHOLDER = { + emptyImage: '//img.alicdn.com/tfs/TB1zpkUoUT1gK0jSZFhXXaAtVXa-620-430.png', + emptyText: '当前页面为空~\n请拖拽组件放入页面容器内吧!', + nullImage: '//img.alicdn.com/tfs/TB1m_oSoND1gK0jSZFsXXbldVXa-620-430.png', + nullText: '编辑内容不存在~!' +}; + +export default class Canvas extends PureComponent { + static displayName = 'Canvas'; + static propTypes = { + appHelper: PropTypes.object, + components: PropTypes.object, + engine: PropTypes.element, + onCreate: PropTypes.func, + initSchema: PropTypes.object, + shortCuts: PropTypes.array, + utils: PropTypes.object + }; + static defaultProps = { + components: {}, + engine: Engine, + onCreate: () => {}, + initSchema: {}, + shortCuts: [], + utils: {} + }; + constructor(props) { + super(props); + this.appHelper = props.appHelper || new AppHelper(); + if (!this.appHelper.schemaHelper) { + this.appHelper.set('schemaHelper', new SchemaHelper(props.initSchema || {}, this.appHelper)); + } + this.appHelper.set('basicSchemaHelper', this.appHelper.schemaHelper); + if (!this.appHelper.dndHelper) { + this.appHelper.set('dndHelper', new DndHelper(this.appHelper)); + } + this.appHelper.dndHelper.setCanvasWin(window); + if (this.appHelper.designMode === undefined) { + this.appHelper.designMode = 'extend'; + } + + this.canvasAppHelper = new AppHelper({ + history: this.appHelper.history, + location: this.appHelper.location, + match: this.appHelper.match + }); + + this.updateCanvasAppHelper(props); + this.appHelper.once('ide.ready', () => { + this.updateCanvasAppHelper(props); + }); + + window.__ctx = { + appHelper: this.appHelper, + canvasAppHelper: this.canvasAppHelper, + components: this.props.components + }; + + window.goldlog = window.goldlog || window.parent.goldlog; + + this.state = { + canvasStack: [ + { + lunaKey: 'root', + lunaPath: '', + name: 'root', + schemaHelper: this.appHelper.schemaHelper, + schema: this.appHelper.schemaHelper.get('schema') + } + ] + }; + this.appHelper.set('canvasStack', this.state.canvasStack); + } + + componentDidMount() { + const appHelper = this.appHelper; + appHelper.batchOn(['behavior.undo', 'behavior.redo'], this.handleUndoRedo); + appHelper.on('schema.reset', this.handleSchemaReset); + appHelper.on('material.move', this.handleMaterialMove); + appHelper.on('material.add', this.handleMaterialAdd); + appHelper.on('material.remove', this.handleMaterialRemove); + appHelper.on('material.up', this.handleMaterialMoveUp); + appHelper.on('material.down', this.handleMaterialMoveDown); + appHelper.on('material.copy', this.handleMaterialCopy); + appHelper.on('material.update', this.handleMaterialUpdate); + appHelper.on('material.select', this.handleMaterialSelect); + appHelper.on('schemaHelper.schema.afterUpdate', this.handleReset); + appHelper.on('designMode.change', this.handleDesignModeChange); + appHelper.on('preview.change', this.handlePreviewChange); + appHelper.on('canvas.stack.push', this.handleCanvasPush); + appHelper.on('canvas.stack.pop', this.handleCanvasPop); + appHelper.on('canvas.stack.jump', this.handleCanvasJump); + appHelper.on('style.update', this.updateStyle); + appHelper.batchOn(['utils.update', 'constants.update', 'componentsMap.update'], this.handleCanvasAppHelperUpdate); + appHelper.on('viewPort.update', this.handleForceUpdate); + + registShortCuts(this.props.shortCuts, this.appHelper); + this.appHelper.set('canvas', this); + this.props.onCreate(this.appHelper); + appHelper.emit('canvas.ready', this); + goldlog( + 'EXP', + { + action: 'appear' + }, + 'canvas' + ); + } + + componentWillUnmount() { + const appHelper = this.appHelper; + appHelper.batchOff(['behavior.undo', 'behavior.redo'], this.handleUndoRedo); + appHelper.on('schema.reset', this.handleSchemaReset); + appHelper.off('material.move', this.handleMaterialMove); + appHelper.off('material.add', this.handleMaterialAdd); + appHelper.off('material.remove', this.handleMaterialRemove); + appHelper.off('material.up', this.handleMaterialMoveUp); + appHelper.off('material.down', this.handleMaterialMoveDown); + appHelper.off('material.copy', this.handleMaterialCopy); + appHelper.off('material.update', this.handleMaterialUpdate); + appHelper.off('material.select', this.handleMaterialSelect); + appHelper.off('schemaHelper.schema.afterUpdate', this.handleReset); + appHelper.off('designMode.change', this.handleDesignModeChange); + appHelper.off('preview.change', this.handlePreviewChange); + appHelper.off('canvas.stack.push', this.handleCanvasPush); + appHelper.off('canvas.stack.pop', this.handleCanvasPop); + appHelper.off('canvas.stack.jump', this.handleCanvasJump); + appHelper.off('style.update', this.updateStyle); + appHelper.batchOff(['utils.update', 'constants.update', 'componentsMap.update'], this.handleCanvasAppHelperUpdate); + appHelper.off('viewPort.update', this.handleForceUpdate); + unRegistShortCuts(this.props.shortCuts); + } + + // 消息处理 + + handleMaterialMove = ({ lunaKey, targetKey, direction }) => { + const appHelper = this.appHelper; + appHelper.schemaHelper.move(lunaKey, targetKey, direction); + appHelper.emit('behavior.record'); + }; + + handleMaterialAdd = ({ schema, targetKey, direction }) => { + if (!isSchema(schema)) { + throw new Error('物料schema结构异常,无法添加!'); + } + const appHelper = this.appHelper; + const addSchema = Array.isArray(schema) ? schema[0] : schema; + // 对于没有设置文件名的容器组件,交给画布外层处理 + if (isFileSchema(addSchema) && !addSchema.fileName) { + return appHelper.emit('onFileNameMaterial.add', { schema: addSchema, targetKey, direction }); + } + + const addKey = appHelper.schemaHelper.add(schema, targetKey, direction); + appHelper.emit('behavior.record'); + this.autoSelectComponent(addKey); + }; + + handleMaterialRemove = lunaKey => { + const appHelper = this.appHelper; + const schemaHelper = appHelper.schemaHelper; + const currCompSchema = schemaHelper.schemaMap[lunaKey]; + // 获取当前删除物料的相邻物料 + const nextCompSchema = jsonuri.get( + schemaHelper.schema, + currCompSchema.__ctx.lunaPath.replace(/\/(\d+)$/, (res, idx) => `/${parseInt(idx) + 1}`) + ); + const activeKey = (nextCompSchema && nextCompSchema.__ctx.lunaKey) || currCompSchema.__ctx.parentKey; + appHelper.schemaHelper.remove(lunaKey); + appHelper.emit('behavior.record'); + this.autoSelectComponent(activeKey); + }; + + handleMaterialMoveUp = lunaKey => { + const appHelper = this.appHelper; + appHelper.schemaHelper && appHelper.schemaHelper.slide(lunaKey, 'up'); + appHelper.emit('behavior.record'); + }; + + handleMaterialMoveDown = lunaKey => { + const appHelper = this.appHelper; + appHelper.schemaHelper && appHelper.schemaHelper.slide(lunaKey, 'down'); + appHelper.emit('behavior.record'); + }; + + handleMaterialCopy = lunaKey => { + const appHelper = this.appHelper; + const addKey = appHelper.schemaHelper.copy(lunaKey); + + appHelper.emit('behavior.record'); + this.autoSelectComponent(addKey); + }; + + handleMaterialUpdate = ({ lunaKey, props, propsKey }) => { + const appHelper = this.appHelper; + appHelper.schemaHelper.update(lunaKey, props); + appHelper.emit('behavior.record', { lunaKey, propsKey }); + }; + + handleMaterialSelect = (lunaKey, options) => { + const appHelper = this.appHelper; + if (appHelper.activeKey === lunaKey) return; + appHelper.set('activeKey', lunaKey); + appHelper.emit('material.select.change', lunaKey, options); + const preNode = document.querySelectorAll('[data-active=true]'); + if (preNode[0] && preNode[0].dataset.lunaKey === lunaKey) return; + (preNode || []).forEach(item => { + item.removeAttribute('data-active'); + item.removeAttribute('data-nochild'); + }); + //判断是否容器组件且没有子元素 + if (!lunaKey) { + window.parent.t = window.t = null; + return; + } + let schema = appHelper.schemaHelper.schemaMap[lunaKey]; + if (!schema) return; + let componentInfo = appHelper.componentsMap[schema.componentName]; + const currentNode = document.querySelectorAll(`[data-luna-key=${lunaKey}]`); + (currentNode || []).forEach(item => { + item.setAttribute('data-active', 'true'); + if (componentInfo && componentInfo.isContainer && schema && (!schema.children || !schema.children.length)) { + item.setAttribute('data-nochild', 'true'); + } + }); + // for debug + let ctx = this.appHelper.schemaHelper.compCtxMap[lunaKey]; + let ref = this.appHelper.schemaHelper.compThisMap[lunaKey]; + let t = { + ctx, + schema, + ref + }; + t.__proto__ = ctx; + window.parent.t = window.t = t; + }; + + handleDesignModeChange = designMode => { + this.appHelper.set('designMode', designMode); + this.forceUpdate(); + }; + + handlePreviewChange = isPreview => { + this.appHelper.set('isPreview', isPreview); + this.forceUpdate(); + }; + + handleUndoRedo = schema => { + this.appHelper.schemaHelper.reset(schema); + this.autoSelectComponent(); + }; + + handleSchemaReset = schema => { + this.appHelper.schemaHelper.reset(schema); + this.appHelper.emit('behavior.record'); + this.autoSelectComponent(); + }; + + handleReset = () => { + this.updateCanvasStack(); + this.forceUpdate(); + this.updateStyle(); + }; + + handleCanvasAppHelperUpdate = () => { + this.updateCanvasAppHelper(); + this.forceUpdate(); + }; + + handleForceUpdate = () => { + this.forceUpdate(); + }; + + handleCanvasPush = ({ schema, lunaKey, name }) => { + const appHelper = this.appHelper; + appHelper.emit('canvas.stack.beforePush'); + const { canvasStack } = this.state; + const tempSchema = { + componentName: 'Temp', + fileName: 'temp', + props: {}, + children: isJSSlot(schema) ? schema.value : schema //兼容slot + }; + const schemaHelper = new SchemaHelper(transformSchemaToPure(tempSchema), this.appHelper); + const schemaMap = this.appHelper.schemaHelper.schemaMap || {}; + const compCtxMap = this.appHelper.schemaHelper.compCtxMap || {}; + const currentComp = schemaMap[lunaKey]; + const undoRedoKey = `${lunaKey}_${canvasStack.length}`; + //若是第一层下钻需要先给最上层加上从appHelper中获取的undoRedoKey + if (canvasStack.length === 1) { + canvasStack[0].undoRedoKey = this.appHelper.undoRedoKey; + } + const currentData = { + lunaKey, + lunaPath: currentComp.__ctx.lunaPath, + name, + schema, + schemaHelper, + ctx: compCtxMap[lunaKey], + undoRedoKey, + componentName: currentComp.componentName + }; + appHelper.set('schemaHelper', schemaHelper); + appHelper.undoRedoHelper && appHelper.undoRedoHelper.create(undoRedoKey, tempSchema); + appHelper.set('undoRedoKey', undoRedoKey); + appHelper.set('activeKey', null); + this.setState( + { + canvasStack: [...this.state.canvasStack, currentData] + }, + () => { + this.appHelper.set('canvasStack', this.state.canvasStack); + this.appHelper.emit('canvas.stack.afterPush', currentData, this.state.canvasStack); + this.autoSelectComponent(); + } + ); + }; + + handleCanvasPop = () => { + const { canvasStack } = this.state; + if (canvasStack.length > 1) { + this.handleCanvasJump(null, true); + } + }; + + handleCanvasJump = (idx, isPop) => { + const { canvasStack } = this.state; + const appHelper = this.appHelper; + let preIdx = idx + 1; + if (isPop) { + appHelper.emit('canvas.stack.beforePop'); + preIdx = canvasStack.length - 1; + idx = preIdx - 1; + } else { + appHelper.emit('canvas.stack.beforeJump'); + } + if (idx < 0 || idx > canvasStack.length - 1) return; + const preData = canvasStack[preIdx]; + const currentData = canvasStack[idx]; + appHelper.set('schemaHelper', currentData.schemaHelper); + appHelper.set('undoRedoKey', currentData.undoRedoKey); + appHelper.undoRedoHelper && appHelper.undoRedoHelper.delete(preData.undoRedoKey); + this.setState( + { + canvasStack: canvasStack.slice(0, idx + 1) + }, + () => { + appHelper.set('canvasStack', this.state.canvasStack); + appHelper.schemaHelper.reset(appHelper.schemaHelper.schema); + appHelper.emit('behavior.record'); + appHelper.emit(`canvas.stack.${isPop ? 'afterPop' : 'afterJump'}`, preData, this.state.canvasStack); + this.autoSelectComponent(preData.lunaKey); + } + ); + }; + + // 引擎处理函数 + + handleCompGetCtx = (schema, ctx) => { + const lunaKey = schema && schema.__ctx && schema.__ctx.lunaKey; + if (!lunaKey) return; + // console.log('+++++ getCtx', lunaKey, ctx); + this.appHelper.schemaHelper.compCtxMap[lunaKey] = ctx; + // for debug + if (this.appHelper.activeKey && lunaKey === this.appHelper.activeKey) { + let ref = this.appHelper.schemaHelper.compThisMap[lunaKey]; + let t = { + ctx, + schema, + ref + }; + t.__proto__ = ctx; + window.parent.t = window.t = t; + } + }; + + handleCompGetRef = (schema, ref, topLevel) => { + const lunaKey = schema && schema.__ctx && schema.__ctx.lunaKey; + if (!lunaKey) return; + // console.log('----- getRef', lunaKey, ref); + const schemaHelper = this.appHelper.schemaHelper; + schemaHelper.compThisMap[lunaKey] = ref; + if (ref && !ref.__design) { + this.updateDesignMode(ref, schema, topLevel); + const didUpdate = ref.componentDidUpdate; + ref.componentDidUpdate = (...args) => { + didUpdate && didUpdate.apply(ref, args); + this.updateDesignMode(ref, schema, topLevel); + }; + const willUnmount = ref.componentWillUnmount; + ref.componentWillUnmount = (...args) => { + willUnmount && willUnmount.apply(ref, args); + // console.log('----- destroy', lunaKey, ref); + delete schemaHelper.compThisMap[lunaKey]; + delete schemaHelper.compCtxMap[lunaKey]; + }; + ref.__design = true; + } + }; + + handleDnd = (type, ev, schema) => { + const lunaKey = schema && schema.__ctx && schema.__ctx.lunaKey; + const designMode = this.appHelper.designMode; + if (!lunaKey || ![DESIGN_MODE.EXTEND, DESIGN_MODE.BORDER].includes(designMode)) return; + const dndHelper = this.appHelper && this.appHelper.dndHelper; + if (dndHelper) { + dndHelper[`handle${type}`](ev, lunaKey); + } + }; + + //自动选中组件 + autoSelectComponent = lunaKey => { + const appHelper = this.appHelper; + // 若未指定需要选中的组件,且当前有选中的组件不做处理 + if (appHelper.activeKey && !lunaKey) return; + if (!lunaKey) { + // 若没有指定的组件,且当前又没有选中组件,默认选中顶部组件,如果是下钻编辑则默认选中第一个子组件 + const schema = appHelper.schemaHelper.schema; + if (schema) { + if (schema.componentName === 'Temp') { + lunaKey = schema.children && schema.children[0] && schema.children[0].__ctx.lunaKey; + } else { + lunaKey = schema.__ctx.lunaKey; + } + } + } + appHelper.emit('material.select', lunaKey); + }; + + // 构造低代码组件 + generateLowComps = (props = this.props) => { + const { components } = props; + const { utils, constants } = this.canvasAppHelper || {}; + const componentsMap = this.appHelper.componentsMap || {}; + Object.keys(componentsMap).forEach(key => { + const comp = componentsMap[key]; + // 对自定义组件做特殊处理 + if (comp.version === '0.0.0' && comp.code) { + let schema = parseObj(comp.code); + if (isFileSchema(schema) && schema.componentName === 'Component') { + components[comp.name] = CompFactory(schema, components, componentsMap, { + utils, + constants + }); + } + } + }); + return components; + }; + + updateCanvasAppHelper = (props = this.props) => { + const { utils } = props; + const { entityInfo = {}, componentsMap } = this.appHelper; + this.canvasAppHelper.set({ + componentsMap, + utils: entityInfo.utils ? generateUtils(utils, parseObj(entityInfo.utils)) : utils, + constants: parseObj((entityInfo && entityInfo.constants) || {}) + }); + this.canvasAppHelper.set('components', this.generateLowComps(props)); + }; + + updateStyle = () => { + const entityInfo = this.appHelper.entityInfo || {}; + const blockSchemaMap = (this.appHelper.schemaHelper && this.appHelper.schemaHelper.blockSchemaMap) || {}; + const componentsMap = this.appHelper.componentsMap || {}; + const cssMap = {}; + // 区块中的样式 + Object.keys(blockSchemaMap).forEach(item => { + const schema = blockSchemaMap[item]; + cssMap[schema.fileName] = schema.css || (schema.__ctx && schema.__ctx.css) || ''; + }); + // 低代码自定义组件中的样式 + Object.keys(componentsMap).forEach(item => { + const comp = componentsMap[item]; + // 对自定义组件做特殊处理 + if (comp.version === '0.0.0' && comp.code && comp.css) { + cssMap[comp.name] = comp.css; + } + }); + cssMap.__global = entityInfo.css || ''; + if (shallowEqual(this.cacheCssMap, cssMap)) return; + this.cacheCssMap = cssMap; + const { __global, ...other } = cssMap; + addCssTag( + 'luna-canvas-style', + `${__global}\n${Object.keys(other || {}) + .map(item => cssMap[item]) + .join('\n')}` + ); + }; + + updateCanvasStack = () => { + const { canvasStack } = this.state; + if (canvasStack.length < 2) return; + for (let idx = canvasStack.length - 1; idx > 0; idx--) { + const currentData = canvasStack[idx]; + const { lunaPath, name, schemaHelper, schema } = currentData; + const preData = canvasStack[idx - 1]; + let data = schemaHelper.getPureSchema().children; + // 如果情况内容则删除属性 + if (isEmpty(data)) { + jsonuri.rm( + preData.schemaHelper.schema, + name === 'children' ? `${lunaPath}/children` : `${lunaPath}/props/${name.replace('.', '/')}` + ); + continue; + } + if (isJSSlot(schema)) { + data = { + ...schema, + value: data + }; + } else if (name !== 'children') { + data = { + type: 'JSSlot', + value: data + }; + } + jsonuri.set( + preData.schemaHelper.schema, + name === 'children' ? `${lunaPath}/children` : `${lunaPath}/props/${name.replace('.', '/')}`, + data + ); + } + }; + + updateDesignMode = (ref, schema, topLevel) => { + if (!ref || (ref && ref.current === null) || !schema.__ctx) return; + const { engine } = this.props; + const appHelper = this.appHelper; + const { activeKey, isPreview, viewPortConfig } = appHelper; + const designMode = isPreview ? 'preview' : appHelper.designMode; + const node = engine.findDOMNode(ref.current || ref); + + if (!node || !node.getAttribute) return; + // 渲染引擎可以通过设置__disableDesignMode属性的方式阻止组件的可视模式; + const hasDesignMode = + [DESIGN_MODE.EXTEND, DESIGN_MODE.BORDER].includes(designMode) && !ref.props.__disableDesignMode; + node.setAttribute('data-design-mode', designMode && hasDesignMode ? `luna-design-${designMode}` : ''); + if (topLevel) { + node.setAttribute('top-container', true); + } + const lunaKey = schema.__ctx.lunaKey; + let instanceName = schema.componentName + (window.parent.__isDebug ? (lunaKey || '_').split('_')[1] : ''); + switch (schema.componentName) { + case 'Page': + instanceName = '页面容器 ' + instanceName; + break; + case 'Block': + instanceName = '区块容器 ' + instanceName; + break; + case 'Component': + instanceName = '低代码组件容器 ' + instanceName; + break; + case 'Addon': + instanceName = '插件容器 ' + instanceName; + break; + case 'Temp': + instanceName = '下钻编辑器容器'; + } + + if (topLevel && viewPortConfig) { + node.style.transform = `scale(${viewPortConfig.scale ? viewPortConfig.scale / 100 : 1})`; + } + node.setAttribute('data-instance-name', instanceName); + node.setAttribute('data-luna-key', lunaKey); + node.setAttribute('data-luna-path', schema.__ctx.lunaPath); + + if (hasDesignMode) { + if (activeKey && activeKey === lunaKey) { + node.setAttribute('data-active', true); + } else { + node.removeAttribute('data-active'); + } + // 点击 + if (!node.compEvent && schema.componentName !== 'Temp') { + node.compEvent = ev => { + ev.stopPropagation(); + appHelper.emit('material.select', lunaKey, { isFromCanvas: true }); + }; + on(node, 'mousedown', node.compEvent, false); + } + + // drag and drop + if (!node.draggableFlag) { + if (topLevel) { + node.ondragleave = ev => this.handleDnd('DragLeave', ev, schema); + node.ondrop = ev => this.handleDnd('Drop', ev, schema); + } else { + node.setAttribute('draggable', 'true'); + node.ondragstart = ev => this.handleDnd('DragStart', ev, schema); + node.ondragend = ev => this.handleDnd('DragEnd', ev, schema); + } + node.ondragover = ev => this.handleDnd('DragOver', ev, schema); + node.draggableFlag = true; + } + } else { + //点击 + if (node.compEvent) { + off(node, 'mousedown', node.compEvent, false); + delete node.compEvent; + } + //drag and drop + if (node.draggableFlag) { + node.removeAttribute('draggable'); + delete node.ondragstart; + delete node.ondragover; + delete node.ondragend; + delete node.ondragleave; + delete node.ondrop; + delete node.draggableFlag; + } + } + }; + + renderCanvasStack = () => { + const { canvasStack } = this.state; + const lastIndex = canvasStack.length - 1; + const appHelper = this.appHelper; + const canvasAppHelper = this.canvasAppHelper; + const designMode = appHelper.isPreview ? 'preview' : appHelper.designMode; + const Engine = this.props.engine; + + return (canvasStack || []).map((item, idx) => ( +
+ +
+ )); + }; + + render() { + const { canvasStack } = this.state; + const lastIndex = canvasStack.length - 1; + const schema = canvasStack[lastIndex] && canvasStack[lastIndex].schemaHelper.schema; + + const appHelper = this.appHelper; + const { entityInfo = {}, viewPortConfig = {}, canvasPlaceholder = {} } = appHelper; + const components = this.canvasAppHelper.components || {}; + + const placeholder = { ...DEFAULT_PLACEHOLDER, ...canvasPlaceholder }; + const layoutComp = entityInfo.layoutInfo && entityInfo.layoutInfo.name; + const layoutProps = (entityInfo.layoutInfo && entityInfo.layoutInfo.realProps) || {}; + const Layout = layoutComp && components[layoutComp]; + const { hideLayout } = viewPortConfig; + const isDrillDown = canvasStack && canvasStack.length > 1; + const isSchemaEmpty = isSchema(schema) && isEmpty(schema.children); + const isSchemaNull = schema === null; + const canvasStyle = {}; + if (!isDrillDown) { + if (isSchemaEmpty) { + canvasStyle.backgroundImage = `url(${placeholder.emptyImage})`; + } else if (isSchemaNull) { + canvasStyle.backgroundImage = `url(${placeholder.nullImage})`; + } + } + return ( +
+ {Layout && !hideLayout ? ( + + {this.renderCanvasStack()} + + ) : ( + this.renderCanvasStack() + )} +
+ ); + } +} diff --git a/packages/react-renderer/src/comp/canvas/index.scss b/packages/react-renderer/src/comp/canvas/index.scss new file mode 100644 index 000000000..3961561d4 --- /dev/null +++ b/packages/react-renderer/src/comp/canvas/index.scss @@ -0,0 +1,361 @@ +/*增加标签函数*/ +@mixin labelFun($type: before) { + &:#{$type} { + content: attr(data-instance-name) !important; + position: absolute; + left: 0; + top: 0; + right: unset; + bottom: unset; + color: #666 !important; + font-size: 12px !important; + float: left; + padding: 0 5px !important; + line-height: 12px !important; + height: 12px; + overflow: hidden; + background: rgba(222, 222, 222, 0.7); + z-index: 2; + border-left: 3px solid transparent; + transform-origin: 0 0; + transform: scale(0.8); + transition: all 0.3s ease; + } + + &.luna-block, + &.luna-page, + &.luna-comp { + &:#{$type} { + border-color: #2077ff; + } + } + + &[data-active='true'] { + &:#{$type} { + color: #fff !important; + background: #1861d5 !important; + } + } + + &[data-design-mode='luna-design-border'] { + &:#{$type} { + display: none; + } + + &[data-active='true'] { + &:#{$type} { + display: block; + } + } + } +} + +.luna-canvas-inner { + height: 100%; + + &.empty, + &.null { + position: relative; + background-repeat: no-repeat; + background-position: calc(50% - 180px) 50%; + background-size: 310px 215px; + + &:after { + content: attr(data-placeholder-text); + position: absolute; + pointer-events: none; + top: 50%; + left: 50%; + margin: -40px 0 0 20px; + height: 80px; + line-height: 40px; + color: #aaa; + font-size: 24px; + white-space: pre; + } + } + + &.empty { + &.drill-down { + &:before { + display: none; + } + + &:after { + content: '请拖入组件'; + text-align: center; + left: 0; + right: 0; + margin-left: 0; + font-size: 14px; + } + } + } + + .engine-wrapper { + height: 100%; + display: none; + overflow: auto; + + &.extend, + &.border { + padding: 1px; + + > div { + padding: 1px; + } + } + + &:last-child { + display: block; + } + + &.fixed-width > div { + min-width: unset; + width: auto; + } + + &.fixed-height > div { + min-height: unset; + } + + > div { + transform-origin: left top; + min-width: 100%; + width: fit-content; + min-height: 100%; + } + } +} + +a, +span:not(.next-input-group):not(.next-input) { + &[data-design-mode*='luna-design-'] { + display: inline-block; + min-height: 16px; + } +} + +[data-luna-key] { + transition: all 0.3s ease; +} + +[data-design-mode='luna-design-border'] { + min-height: 10px; +} + +[data-design-mode='luna-design-extend'] { + min-height: 20px; +} + +[data-design-mode*='luna-design-'] { + position: relative; + outline: 1px dotted #d9d9d9; + zoom: 1; + + &:hover { + outline: 1px dotted #2077ff; + } + + [draggable='true'] { + position: relative; + } + + .next-card-body { + overflow: inherit !important; + } + + &.next-loading { + pointer-events: all !important; + } + + &[data-active='true'] { + outline: 1px solid #1861d5 !important; + + &[data-nochild='true']:not(.next-step-item):not([top-container='true']) { + min-height: 60px; + min-width: 200px; + + &:after { + content: '请拖入组件'; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + margin: auto; + opacity: 1 !important; + visibility: visible !important; + line-height: 30px; + height: 30px; + font-size: 13px; + color: #ccc; + text-align: center; + } + } + } + + &:not(.next-tag):not(.next-icon):not(.anticon):not(.icon) { + @include labelFun(before); + } + + &.next-tag, + &.next-icon, + &.anticon, + &.icon { + @include labelFun(after); + } + + &.next-tabs-tabpane.hidden { + min-height: 0; + } + + &.ant-loop:after { + content: ''; + display: block; + clear: both; + line-height: 0; + } + + .ant-tabs-tabpane { + padding: 1px; + } + + .ide-design-placeholder { + position: relative; + text-align: center; + border: 1px dashed #d9d9d9; + outline: none; + padding: 0 !important; + min-height: 20px; + min-width: 80px; + + &:after { + content: attr(data-prop); + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + text-align: center; + line-height: 20px; + color: #e9e9e9; + } + } + + &[data-instance-name='TableGroupHeaderF'] { + clear: both; + + &:after { + content: ''; + width: 100%; + height: 0; + clear: both; + overflow: hidden; + } + } + + &[data-design-mode='luna-design-extend'] { + &[data-luna-key*='luna_'] { + &:not([class*='-input']):not([class*='-picker']):not([class*='-table']):not([class*='-switch']):not([class*='-select']):not(img):not([class*='-btn']):not(.next-tag):not(input):not([class*='-rating']):not([class*='next-menu']) { + padding: 10px; + } + + &.ant-loop { + padding: 10px 0 0; + } + } + + [class*='-form-item-control'] { + & > [data-design-mode*='luna-design-'] { + &:not(button):not(input):not([class*='-input']):not([class*='luna-comp-']) { + padding: 0 !important; + } + } + } + } +} + +#luna-canvas-effect { + position: fixed; + background: #1aab11; + z-index: 10000000; + text-align: center; + pointer-events: none; + + &:before, + &:after { + content: ''; + position: absolute; + width: 2px; + height: 2px; + border: 4px solid #1aab11; + pointer-events: none; + } + + &.left, + &.right { + width: 2px; + + &:before, + &:after { + left: -4px; + border-left-color: transparent; + border-right-color: transparent; + } + + &:before { + top: 0; + border-bottom-width: 0; + } + + &:after { + bottom: 0; + border-top-width: 0; + } + } + + &.top, + &.bottom, + &.in { + height: 2px; + + &:before, + &:after { + top: -4px; + border-top-color: transparent; + border-bottom-color: transparent; + } + + &:before { + left: 0; + border-right-width: 0; + } + + &:after { + right: 0; + border-left-width: 0; + } + } + + &.in { + b { + display: inline-block; + } + } + + b { + display: inline-block; + position: relative; + top: -12px; + margin: 0 auto; + padding: 0 10px; + color: #fff; + background: #1aab11; + height: 16px !important; + line-height: 16px !important; + font-weight: normal; + font-size: 11px; + display: none; + } +} diff --git a/packages/react-renderer/src/comp/visualDom/index.jsx b/packages/react-renderer/src/comp/visualDom/index.jsx new file mode 100644 index 000000000..3ac87cb1a --- /dev/null +++ b/packages/react-renderer/src/comp/visualDom/index.jsx @@ -0,0 +1,27 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import './index.scss'; +export default class VisualDom extends PureComponent { + static displayName = 'VisualDom'; + static propTypes = { + children: PropTypes.oneOfType([PropTypes.element, PropTypes.arrayOf(PropTypes.element)]) + }; + static defaultProps = { + children: null + }; + render() { + const { children, cell, title, label, text, __componentName } = this.props; + let mainContent = children; + if (cell && typeof cell === 'function') { + mainContent = cell(); + } + return ( +
+
+ {title || label || text || __componentName} +
{mainContent}
+
+
+ ); + } +} diff --git a/packages/react-renderer/src/comp/visualDom/index.scss b/packages/react-renderer/src/comp/visualDom/index.scss new file mode 100644 index 000000000..074b9dad8 --- /dev/null +++ b/packages/react-renderer/src/comp/visualDom/index.scss @@ -0,0 +1,19 @@ +.visual-dom { + .panel-container { + box-sizing: border-box; + border: 1px solid #e9e9e9; + .title { + display: block; + font-size: 12px; + color: #333; + background-color: #ebecf0; + line-height: 28px; + padding: 0 12px; + border-bottom: 1px solid #e9e9e9; + } + .content { + min-height: 20px; + padding: 5px; + } + } +} diff --git a/packages/react-renderer/src/context/appContext.js b/packages/react-renderer/src/context/appContext.js new file mode 100644 index 000000000..b7fbccbdb --- /dev/null +++ b/packages/react-renderer/src/context/appContext.js @@ -0,0 +1,3 @@ +import { createContext } from 'react'; +const context = (window.__appContext = createContext({})); +export default context; diff --git a/packages/react-renderer/src/engine/addonEngine.jsx b/packages/react-renderer/src/engine/addonEngine.jsx new file mode 100644 index 000000000..a66289c96 --- /dev/null +++ b/packages/react-renderer/src/engine/addonEngine.jsx @@ -0,0 +1,131 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Debug from 'debug'; +import AppContext from '../context/appContext'; +import classnames from 'classnames'; +import BaseEngine from './base'; +import { isSchema, getFileCssName, isEmpty, goldlog } from '../utils'; +const debug = Debug('engine:addon'); +export default class AddonEngine extends BaseEngine { + static dislayName = 'addon-engine'; + static propTypes = { + config: PropTypes.object, + __schema: PropTypes.object + }; + static defaultProps = { + config: {}, + __schema: {} + }; + + static getDerivedStateFromProps(props, state) { + debug(`comp.getDerivedStateFromProps`); + const func = props.__schema.lifeCycles && props.__schema.lifeCycles.getDerivedStateFromProps; + if (func) { + return func(props, state); + } + return null; + } + + constructor(props, context) { + super(props, context); + this.__generateCtx({ + component: this + }); + const schema = props.__schema || {}; + this.state = this.__parseData(schema.state || {}); + if (isEmpty(props.config) || !props.config.addonKey) { + console.warn('luna addon has wrong config'); + this.state.__hasError = true; + return; + } + // 注册插件 + this.addonKey = props.config.addonKey; + this.appHelper.addons = this.appHelper.addons || {}; + this.appHelper.addons[this.addonKey] = this; + this.__initDataSource(props); + this.open = this.open || (() => {}); + this.close = this.close || (() => {}); + this.__setLifeCycleMethods('constructor', arguments); + debug(`addon.constructor - ${schema.fileName}`); + } + + async getSnapshotBeforeUpdate() { + super.getSnapshotBeforeUpdate(...arguments); + debug(`addon.getSnapshotBeforeUpdate - ${this.props.__schema.fileName}`); + } + async componentDidMount() { + super.componentDidMount(...arguments); + debug(`addon.componentDidMount - ${this.props.__schema.fileName}`); + } + async componentDidUpdate() { + super.componentDidUpdate(...arguments); + debug(`addon.componentDidUpdate - ${this.props.__schema.fileName}`); + } + async componentWillUnmount() { + super.componentWillUnmount(...arguments); + // 注销插件 + const config = this.props.config || {}; + if (config && this.appHelper.addons) { + delete this.appHelper.addons[config.addonKey]; + } + debug(`addon.componentWillUnmount - ${this.props.__schema.fileName}`); + } + async componentDidCatch(e) { + super.componentDidCatch(...arguments); + debug(`addon.componentDidCatch - ${this.props.__schema.fileName}`); + } + + goldlog = (goKey, params) => { + const { addonKey, addonConfig = {} } = this.props.config || {}; + goldlog( + goKey, + { + addonKey, + package: addonConfig.package, + version: addonConfig.version, + ...this.appHelper.logParams, + ...params + }, + 'addon' + ); + }; + + get utils() { + const { utils = {} } = this.context.config || {}; + return { ...this.appHelper.utils, ...utils }; + } + + render() { + const { __schema } = this.props; + + if (!isSchema(__schema, true) || __schema.componentName !== 'Addon') { + return '插件schema结构异常!'; + } + + debug(`addon.render - ${__schema.fileName}`); + this.__generateCtx({ + component: this + }); + this.__render(); + + const { id, className, style } = this.__parseData(__schema.props); + return ( +
+ + {this.__createDom()} + +
+ ); + } +} diff --git a/packages/react-renderer/src/engine/base.jsx b/packages/react-renderer/src/engine/base.jsx new file mode 100644 index 000000000..99a8ae0ed --- /dev/null +++ b/packages/react-renderer/src/engine/base.jsx @@ -0,0 +1,506 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import Debug from 'debug'; +import Div from '@ali/iceluna-comp-div'; +import VisualDom from '../comp/visualDom'; + +import AppContext from '../context/appContext'; +import DataHelper from '../utils/dataHelper'; + +import { + forEach, + getValue, + parseData, + parseExpression, + isEmpty, + isSchema, + isFileSchema, + isJSExpression, + isJSSlot, + isJSFunction, + transformArrayToMap, + transformStringToFunction, + checkPropTypes, + generateI18n, + acceptsRef +} from '../utils'; + +const debug = Debug('engine:base'); +const DESIGN_MODE = { + EXTEND: 'extend', + BORDER: 'border', + PREVIEW: 'preview' +}; +const OVERLAY_LIST = ['Dialog', 'Overlay', 'Animate', 'ConfigProvider']; +let scopeIdx = 0; + +export default class BaseEngine extends PureComponent { + static dislayName = 'base-engine'; + static propTypes = { + locale: PropTypes.string, + messages: PropTypes.object, + __appHelper: PropTypes.object, + __components: PropTypes.object, + __componentsMap: PropTypes.object, + __ctx: PropTypes.object, + __schema: PropTypes.object + }; + static defaultProps = { + __schema: {} + }; + static contextType = AppContext; + + constructor(props, context) { + super(props, context); + this.appHelper = props.__appHelper; + this.__compScopes = {}; + const { locale, messages } = props; + this.i18n = generateI18n(locale, messages); + this.__bindCustomMethods(props); + } + + async getSnapshotBeforeUpdate() { + this.__setLifeCycleMethods('getSnapshotBeforeUpdate', arguments); + } + + async componentDidMount() { + this.reloadDataSource(); + this.__setLifeCycleMethods('componentDidMount', arguments); + } + + async componentDidUpdate() { + this.__setLifeCycleMethods('componentDidUpdate', arguments); + } + + async componentWillUnmount() { + this.__setLifeCycleMethods('componentWillUnmount', arguments); + } + + async componentDidCatch(e) { + this.__setLifeCycleMethods('componentDidCatch', arguments); + console.warn(e); + } + + reloadDataSource = () => { + return new Promise((resolve, reject) => { + debug('reload data source'); + if (!this.__dataHelper) { + this.__showPlaceholder = false; + return resolve(); + } + this.__dataHelper + .getInitData() + .then(res => { + this.__showPlaceholder = false; + if (isEmpty(res)) { + this.forceUpdate(); + return resolve(); + } + this.setState(res, resolve); + }) + .catch(err => { + if (this.__showPlaceholder) { + this.__showPlaceholder = false; + this.forceUpdate(); + } + reject(err); + }); + }); + }; + + __setLifeCycleMethods = (method, args) => { + const lifeCycleMethods = getValue(this.props.__schema, 'lifeCycles', {}); + if (lifeCycleMethods[method]) { + try { + return lifeCycleMethods[method].apply(this, args); + } catch (e) { + console.error(`[${this.props.__schema.componentName}]生命周期${method}出错`, e); + } + } + }; + + __bindCustomMethods = (props = this.props) => { + const { __schema } = props; + const customMethodsList = Object.keys(__schema.methods || {}) || []; + this.__customMethodsList && + this.__customMethodsList.forEach(item => { + if (!customMethodsList.includes(item)) { + delete this[item]; + } + }); + this.__customMethodsList = customMethodsList; + forEach(__schema.methods, (val, key) => { + this[key] = val.bind(this); + }); + }; + + __generateCtx = ctx => { + const { pageContext, compContext } = this.context; + const obj = { + page: pageContext, + component: compContext, + ...ctx + }; + forEach(obj, (val, key) => { + this[key] = val; + }); + }; + + __parseData = (data, ctx) => { + const { __ctx } = this.props; + return parseData(data, ctx || __ctx || this); + }; + + __initDataSource = (props = this.props) => { + const schema = props.__schema || {}; + const appHelper = props.__appHelper; + const dataSource = (schema && schema.dataSource) || {}; + this.__dataHelper = new DataHelper(this, dataSource, appHelper, config => this.__parseData(config)); + this.dataSourceMap = this.__dataHelper.dataSourceMap; + // 设置容器组件占位,若设置占位则在初始异步请求完成之前用loading占位且不渲染容器组件内部内容 + this.__showPlaceholder = + this.__parseData(schema.props && schema.props.autoLoading) && + (dataSource.list || []).some(item => !!this.__parseData(item.isInit)); + }; + + __render = () => { + const schema = this.props.__schema; + this.__setLifeCycleMethods('render'); + + const engine = this.context.engine; + if (engine) { + engine.props.onCompGetCtx(schema, this); + // 画布场景才需要每次渲染bind自定义方法 + if (engine.props.designMode) { + this.__bindCustomMethods(); + this.dataSourceMap = this.__dataHelper && this.__dataHelper.updateConfig(schema.dataSource); + } + } + }; + + __getRef = ref => { + this.__ref = ref; + }; + + __createDom = () => { + const { __schema, __ctx, __components = {} } = this.props; + const self = {}; + self.__proto__ = __ctx || this; + return this.__createVirtualDom(__schema.children, self, { + schema: __schema, + Comp: __components[__schema.componentName] + }); + }; + + // 将模型结构转换成react Element + // schema 模型结构 + // self 为每个渲染组件构造的上下文,self是自上而下继承的 + // parentInfo 父组件的信息,包含schema和Comp + // idx 若为循环渲染的循环Index + __createVirtualDom = (schema, self, parentInfo, idx) => { + if (!schema) return null; + const { __appHelper: appHelper, __components: components = {}, __componentsMap: componentsMap = {} } = + this.props || {}; + const { engine } = this.context || {}; + if (isJSExpression(schema)) { + return parseExpression(schema, self); + } + if (typeof schema === 'string') return schema; + if (typeof schema === 'number' || typeof schema === 'boolean') { + return schema.toString(); + } + if (Array.isArray(schema)) { + if (schema.length === 1) return this.__createVirtualDom(schema[0], self, parentInfo); + return schema.map((item, idx) => + this.__createVirtualDom(item, self, parentInfo, item && item.__ctx && item.__ctx.lunaKey ? '' : idx) + ); + } + + //解析占位组件 + if (schema.componentName === 'Flagment' && schema.children) { + let tarChildren = isJSExpression(schema.children) ? parseExpression(schema.children, self) : schema.children; + return this.__createVirtualDom(tarChildren, self, parentInfo); + } + + if (schema.$$typeof) { + return schema; + } + if (!isSchema(schema)) return null; + + let Comp = components[schema.componentName] || Div; + + if (schema.loop !== undefined) { + return this.__createLoopVirtualDom( + { + ...schema, + loop: parseData(schema.loop, self) + }, + self, + parentInfo, + idx + ); + } + const condition = schema.condition === undefined ? true : parseData(schema.condition, self); + if (!condition) return null; + + let scopeKey = ''; + // 判断组件是否需要生成scope,且只生成一次,挂在this.__compScopes上 + if (Comp.generateScope) { + const key = parseExpression(schema.props.key, self); + if (key) { + // 如果组件自己设置key则使用组件自己的key + scopeKey = key; + } else if (!schema.__ctx) { + // 在生产环境schema没有__ctx上下文,需要手动生成一个lunaKey + schema.__ctx = { + lunaKey: `luna${++scopeIdx}` + }; + scopeKey = schema.__ctx.lunaKey; + } else { + // 需要判断循环的情况 + scopeKey = schema.__ctx.lunaKey + (idx !== undefined ? `_${idx}` : ''); + } + if (!this.__compScopes[scopeKey]) { + this.__compScopes[scopeKey] = Comp.generateScope(this, schema); + } + } + // 如果组件有设置scope,需要为组件生成一个新的scope上下文 + if (scopeKey && this.__compScopes[scopeKey]) { + const compSelf = { ...this.__compScopes[scopeKey] }; + compSelf.__proto__ = self; + self = compSelf; + } + + // 容器类组件的上下文通过props传递,避免context传递带来的嵌套问题 + const otherProps = isFileSchema(schema) + ? { + __schema: schema, + __appHelper: appHelper, + __components: components, + __componentsMap: componentsMap + } + : {}; + if (engine && engine.props.designMode) { + otherProps.__designMode = engine.props.designMode; + } + const componentInfo = componentsMap[schema.componentName] || {}; + const props = this.__parseProps(schema.props, self, '', { + schema, + Comp, + componentInfo: { + ...componentInfo, + props: transformArrayToMap(componentInfo.props, 'name') + } + }); + // 对于可以获取到ref的组件做特殊处理 + if (acceptsRef(Comp)) { + otherProps.ref = ref => { + const refProps = props.ref; + if (refProps && typeof refProps === 'string') { + this[refProps] = ref; + } + engine && engine.props.onCompGetRef(schema, ref); + }; + } + // scope需要传入到组件上 + if (scopeKey && this.__compScopes[scopeKey]) { + props.__scope = this.__compScopes[scopeKey]; + } + if (schema.__ctx && schema.__ctx.lunaKey) { + if (!isFileSchema(schema)) { + engine && engine.props.onCompGetCtx(schema, self); + } + props.key = props.key || `${schema.__ctx.lunaKey}_${schema.__ctx.idx || 0}_${idx !== undefined ? idx : ''}`; + } else if (typeof idx === 'number' && !props.key) { + props.key = idx; + } + const renderComp = props => ( + + {(!isFileSchema(schema) && + !!schema.children && + this.__createVirtualDom( + isJSExpression(schema.children) ? parseExpression(schema.children, self) : schema.children, + self, + { + schema, + Comp + } + )) || + null} + + ); + //设计模式下的特殊处理 + if (engine && [DESIGN_MODE.EXTEND, DESIGN_MODE.BORDER].includes(engine.props.designMode)) { + //对于overlay,dialog等组件为了使其在设计模式下显示,外层需要增加一个div容器 + if (OVERLAY_LIST.includes(schema.componentName)) { + const { ref, ...overlayProps } = otherProps; + return ( +
+ {renderComp({ ...props, ...overlayProps })} +
+ ); + } + // 虚拟dom显示 + if (componentInfo && componentInfo.parentRule) { + const parentList = componentInfo.parentRule.split(','); + const { schema: parentSchema, Comp: parentComp } = parentInfo; + if (!parentList.includes(parentSchema.componentName) || parentComp !== components[parentSchema.componentName]) { + props.__componentName = schema.componentName; + Comp = VisualDom; + } else { + // 若虚拟dom在正常的渲染上下文中,就不显示设计模式了 + props.__disableDesignMode = true; + } + } + } + return renderComp({ ...props, ...otherProps }); + }; + + __createLoopVirtualDom = (schema, self, parentInfo, idx) => { + if (isFileSchema(schema)) { + console.warn('file type not support Loop'); + return null; + } + if (!Array.isArray(schema.loop)) return null; + const itemArg = (schema.loopArgs && schema.loopArgs[0]) || 'item'; + const indexArg = (schema.loopArgs && schema.loopArgs[1]) || 'index'; + return schema.loop.map((item, i) => { + const loopSelf = { + [itemArg]: item, + [indexArg]: i + }; + loopSelf.__proto__ = self; + return this.__createVirtualDom( + { + ...schema, + loop: undefined + }, + loopSelf, + parentInfo, + idx ? `${idx}_${i}` : i + ); + }); + }; + + __parseProps = (props, self, path, info) => { + const { schema, Comp, componentInfo = {} } = info; + const propInfo = getValue(componentInfo.props, path); + const propType = propInfo && propInfo.extra && propInfo.extra.propType; + const ignoreParse = schema.__ignoreParse || []; + const checkProps = value => { + if (!propType) return value; + return checkPropTypes(value, path, propType, componentInfo.name) ? value : undefined; + }; + + const parseReactNode = (data, params) => { + if (isEmpty(params)) { + return checkProps(this.__createVirtualDom(data, self, { schema, Comp })); + } else { + return checkProps(function() { + const args = {}; + if (Array.isArray(params) && params.length) { + params.map((item, idx) => { + if (typeof item === 'string') { + args[item] = arguments[idx]; + } else if (item && typeof item === 'object') { + args[item.name] = arguments[idx]; + } + }); + } + args.__proto__ = self; + return self.__createVirtualDom(data, args, { schema, Comp }); + }); + } + }; + + // 判断是否需要解析变量 + if ( + ignoreParse.some(item => { + if (item instanceof RegExp) { + return item.test(path); + } + return item === path; + }) + ) { + return checkProps(props); + } + if (isJSExpression(props)) { + props = parseExpression(props, self); + // 只有当变量解析出来为模型结构的时候才会继续解析 + if (!isSchema(props) && !isJSSlot(props)) return checkProps(props); + } + + if (isJSFunction(props)) { + props = transformStringToFunction(props.value); + } + if (isJSSlot(props)) { + const { params, value } = props; + if (!isSchema(value) || isEmpty(value)) return undefined; + return parseReactNode(value, params); + } + // 兼容通过componentInfo判断的情况 + if (isSchema(props)) { + const isReactNodeFunction = !!( + propInfo && + propInfo.type === 'ReactNode' && + propInfo.props && + propInfo.props.type === 'function' + ); + + const isMixinReactNodeFunction = !!( + propInfo && + propInfo.type === 'Mixin' && + propInfo.props && + propInfo.props.types && + propInfo.props.types.indexOf('ReactNode') > -1 && + propInfo.props.reactNodeProps && + propInfo.props.reactNodeProps.type === 'function' + ); + return parseReactNode( + props, + isReactNodeFunction + ? propInfo.props.params + : isMixinReactNodeFunction + ? propInfo.props.reactNodeProps.params + : null + ); + } else if (Array.isArray(props)) { + return checkProps(props.map((item, idx) => this.__parseProps(item, self, path ? `${path}.${idx}` : idx, info))); + } else if (typeof props === 'function') { + return checkProps(props.bind(self)); + } else if (props && typeof props === 'object') { + if (props.$$typeof) return checkProps(props); + const res = {}; + forEach(props, (val, key) => { + if (key.startsWith('__')) { + res[key] = val; + return; + } + res[key] = this.__parseProps(val, self, path ? `${path}.${key}` : key, info); + }); + return checkProps(res); + } else if (typeof props === 'string') { + return checkProps(props.trim()); + } + return checkProps(props); + }; + + get utils() { + return this.appHelper && this.appHelper.utils; + } + get constants() { + return this.appHelper && this.appHelper.constants; + } + get history() { + return this.appHelper && this.appHelper.history; + } + get location() { + return this.appHelper && this.appHelper.location; + } + get match() { + return this.appHelper && this.appHelper.match; + } + render() { + return null; + } +} diff --git a/packages/react-renderer/src/engine/blockEngine.jsx b/packages/react-renderer/src/engine/blockEngine.jsx new file mode 100644 index 000000000..ede4a5065 --- /dev/null +++ b/packages/react-renderer/src/engine/blockEngine.jsx @@ -0,0 +1,113 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Debug from 'debug'; +import classnames from 'classnames'; +import Loading from '@alifd/next/lib/loading'; +import '@alifd/next/lib/loading/style'; + +import BaseEngine from './base'; +import AppContext from '../context/appContext'; +import { isSchema, getFileCssName } from '../utils'; +const debug = Debug('engine:block'); +export default class BlockEngine extends BaseEngine { + static dislayName = 'block-engine'; + static propTypes = { + __schema: PropTypes.object + }; + static defaultProps = { + __schema: {} + }; + + static getDerivedStateFromProps(props, state) { + debug(`block.getDerivedStateFromProps`); + const func = props.__schema.lifeCycles && props.__schema.lifeCycles.getDerivedStateFromProps; + if (func) { + return func(props, state); + } + return null; + } + + constructor(props, context) { + super(props, context); + this.__generateCtx(); + const schema = props.__schema || {}; + this.state = this.__parseData(schema.state || {}); + this.__initDataSource(props); + this.__setLifeCycleMethods('constructor', arguments); + debug(`block.constructor - ${schema.fileName}`); + } + + async getSnapshotBeforeUpdate() { + super.getSnapshotBeforeUpdate(...arguments); + debug(`block.getSnapshotBeforeUpdate - ${this.props.__schema.fileName}`); + } + async componentDidMount() { + super.componentDidMount(...arguments); + debug(`block.componentDidMount - ${this.props.__schema.fileName}`); + } + async componentDidUpdate() { + super.componentDidUpdate(...arguments); + debug(`block.componentDidUpdate - ${this.props.__schema.fileName}`); + } + async componentWillUnmount() { + super.componentWillUnmount(...arguments); + debug(`block.componentWillUnmount - ${this.props.__schema.fileName}`); + } + async componentDidCatch() { + await super.componentDidCatch(...arguments); + debug(`block.componentDidCatch - ${this.props.__schema.fileName}`); + } + + render() { + const { __schema } = this.props; + + if (!isSchema(__schema, true) || __schema.componentName !== 'Block') { + return '区块schema结构异常!'; + } + + debug(`block.render - ${__schema.fileName}`); + this.__generateCtx(); + this.__render(); + + const { id, className, style, autoLoading, defaultHeight = 300, loading } = this.__parseData(__schema.props); + const renderContent = () => ( + + {this.__createDom()} + + ); + + if (autoLoading || loading !== undefined) { + return ( + + {!this.__showPlaceholder && renderContent()} + + ); + } + + return ( +
+ {renderContent()} +
+ ); + } +} diff --git a/packages/react-renderer/src/engine/compEngine.jsx b/packages/react-renderer/src/engine/compEngine.jsx new file mode 100644 index 000000000..30043b951 --- /dev/null +++ b/packages/react-renderer/src/engine/compEngine.jsx @@ -0,0 +1,122 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Debug from 'debug'; +import classnames from 'classnames'; +import Loading from '@alifd/next/lib/loading'; +import '@alifd/next/lib/loading/style'; + +import AppContext from '../context/appContext'; +import BaseEngine from './base'; +import { isSchema, getFileCssName } from '../utils'; +const debug = Debug('engine:comp'); +export default class CompEngine extends BaseEngine { + static dislayName = 'comp-engine'; + static propTypes = { + __schema: PropTypes.object + }; + static defaultProps = { + __schema: {} + }; + + static getDerivedStateFromProps(props, state) { + debug(`comp.getDerivedStateFromProps`); + const func = props.__schema.lifeCycles && props.__schema.lifeCycles.getDerivedStateFromProps; + if (func) { + return func(props, state); + } + return null; + } + + constructor(props, context) { + super(props, context); + this.__generateCtx({ + component: this + }); + const schema = props.__schema || {}; + this.state = this.__parseData(schema.state || {}); + this.__initDataSource(props); + this.__setLifeCycleMethods('constructor', arguments); + debug(`comp.constructor - ${schema.fileName}`); + } + + async getSnapshotBeforeUpdate() { + super.getSnapshotBeforeUpdate(...arguments); + debug(`comp.getSnapshotBeforeUpdate - ${this.props.__schema.fileName}`); + } + async componentDidMount() { + super.componentDidMount(...arguments); + debug(`comp.componentDidMount - ${this.props.__schema.fileName}`); + } + async componentDidUpdate() { + super.componentDidUpdate(...arguments); + debug(`comp.componentDidUpdate - ${this.props.__schema.fileName}`); + } + async componentWillUnmount() { + super.componentWillUnmount(...arguments); + debug(`comp.componentWillUnmount - ${this.props.__schema.fileName}`); + } + async componentDidCatch(e) { + super.componentDidCatch(...arguments); + debug(`comp.componentDidCatch - ${this.props.__schema.fileName}`); + } + + render() { + const { __schema } = this.props; + + if (!isSchema(__schema, true) || __schema.componentName !== 'Component') { + return '自定义组件schema结构异常!'; + } + + debug(`comp.render - ${__schema.fileName}`); + this.__generateCtx({ + component: this + }); + this.__render(); + + const { id, className, style, noContainer, autoLoading, defaultHeight = 300, loading } = this.__parseData( + __schema.props + ); + const renderContent = () => ( + + {this.__createDom()} + + ); + + if (noContainer) { + return renderContent(); + } + if (autoLoading || loading !== undefined) { + return ( + + {!this.__showPlaceholder && renderContent()} + + ); + } + return ( +
+ {renderContent()} +
+ ); + } +} diff --git a/packages/react-renderer/src/engine/index.jsx b/packages/react-renderer/src/engine/index.jsx new file mode 100644 index 000000000..5c8f8b866 --- /dev/null +++ b/packages/react-renderer/src/engine/index.jsx @@ -0,0 +1,127 @@ +import React, { PureComponent } from 'react'; +import ReactDOM from 'react-dom'; +import PropTypes from 'prop-types'; +import Debug from 'debug'; +import AppContext from '../context/appContext'; +import { isFileSchema, goldlog } from '../utils'; +import Page from './pageEngine'; +import Component from './compEngine'; +import Block from './blockEngine'; +import Addon from './addonEngine'; +import Temp from './tempEngine'; +import { isEmpty } from '@ali/b3-one/lib/obj'; + +window.React = React; +window.ReactDom = ReactDOM; + +const debug = Debug('engine:entry'); +const ENGINE_COMPS = { + Page, + Component, + Block, + Addon, + Temp +}; +export default class Engine extends PureComponent { + static dislayName = 'engine'; + static propTypes = { + appHelper: PropTypes.object, + components: PropTypes.object, + componentsMap: PropTypes.object, + designMode: PropTypes.string, + suspended: PropTypes.bool, + schema: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), + onCompGetRef: PropTypes.func, + onCompGetCtx: PropTypes.func + }; + static defaultProps = { + appHelper: null, + components: {}, + componentsMap: {}, + designMode: '', + suspended: false, + schema: {}, + onCompGetRef: () => {}, + onCompGetCtx: () => {} + }; + + constructor(props, context) { + super(props, context); + this.state = {}; + debug(`entry.constructor - ${props.schema && props.schema.componentName}`); + } + + async componentDidMount() { + goldlog( + 'EXP', + { + action: 'appear', + value: !!this.props.designMode + }, + 'engine' + ); + debug(`entry.componentDidMount - ${this.props.schema && this.props.schema.componentName}`); + } + + async componentDidUpdate() { + debug(`entry.componentDidUpdate - ${this.props.schema && this.props.schema.componentName}`); + } + + async componentWillUnmount() { + debug(`entry.componentWillUnmount - ${this.props.schema && this.props.schema.componentName}`); + } + + async componentDidCatch(e) { + console.warn(e); + } + + shouldComponentUpdate(nextProps) { + return !nextProps.suspended; + } + + __getRef = ref => { + this.__ref = ref; + if (ref) { + this.props.onCompGetRef(this.props.schema, ref, true); + } + }; + + render() { + const { schema, designMode, appHelper, components, componentsMap } = this.props; + if (isEmpty(schema)) { + return null; + } + if (!isFileSchema(schema)) { + return '模型结构异常'; + } + debug('entry.render'); + const allComponents = { ...ENGINE_COMPS, ...components }; + const Comp = allComponents[schema.componentName]; + if (Comp) { + return ( + + + + ); + } + return null; + } +} + +Engine.findDOMNode = ReactDOM.findDOMNode; diff --git a/packages/react-renderer/src/engine/pageEngine.jsx b/packages/react-renderer/src/engine/pageEngine.jsx new file mode 100644 index 000000000..1f3c7a79b --- /dev/null +++ b/packages/react-renderer/src/engine/pageEngine.jsx @@ -0,0 +1,117 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Debug from 'debug'; +import classnames from 'classnames'; +import Loading from '@alifd/next/lib/loading'; +import '@alifd/next/lib/loading/style'; +import AppContext from '../context/appContext'; +import BaseEngine from './base'; +import { isSchema, getFileCssName } from '../utils'; +const debug = Debug('engine:page'); + +export default class PageEngine extends BaseEngine { + static dislayName = 'page-engine'; + static propTypes = { + __schema: PropTypes.object + }; + static defaultProps = { + __schema: {} + }; + + static getDerivedStateFromProps(props, state) { + debug(`page.getDerivedStateFromProps`); + const func = props.__schema.lifeCycles && props.__schema.lifeCycles.getDerivedStateFromProps; + if (func) { + return func(props, state); + } + return null; + } + + constructor(props, context) { + super(props, context); + this.__generateCtx({ + page: this + }); + const schema = props.__schema || {}; + this.state = this.__parseData(schema.state || {}); + this.__initDataSource(props); + this.__setLifeCycleMethods('constructor', arguments); + + debug(`page.constructor - ${schema.fileName}`); + } + + async getSnapshotBeforeUpdate() { + super.getSnapshotBeforeUpdate(...arguments); + debug(`page.getSnapshotBeforeUpdate - ${this.props.__schema.fileName}`); + } + async componentDidMount() { + super.componentDidMount(...arguments); + debug(`page.componentDidMount - ${this.props.__schema.fileName}`); + } + async componentDidUpdate() { + super.componentDidUpdate(...arguments); + debug(`page.componentDidUpdate - ${this.props.__schema.fileName}`); + } + async componentWillUnmount() { + super.componentWillUnmount(...arguments); + debug(`page.componentWillUnmount - ${this.props.__schema.fileName}`); + } + async componentDidCatch() { + await super.componentDidCatch(...arguments); + debug(`page.componentDidCatch - ${this.props.__schema.fileName}`); + } + + render() { + const { __schema } = this.props; + if (!isSchema(__schema, true) || __schema.componentName !== 'Page') { + return '页面schema结构异常!'; + } + debug(`page.render - ${__schema.fileName}`); + this.__generateCtx({ + page: this + }); + this.__render(); + + const { id, className, style, autoLoading, defaultHeight = 300, loading } = this.__parseData(__schema.props); + const renderContent = () => ( + + {this.__createDom()} + + ); + + if (autoLoading || loading !== undefined) { + return ( + + {!this.__showPlaceholder && renderContent()} + + ); + } + + return ( +
+ {renderContent()} +
+ ); + } +} diff --git a/packages/react-renderer/src/engine/tempEngine.jsx b/packages/react-renderer/src/engine/tempEngine.jsx new file mode 100644 index 000000000..373ff80a4 --- /dev/null +++ b/packages/react-renderer/src/engine/tempEngine.jsx @@ -0,0 +1,66 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Debug from 'debug'; +import AppContext from '../context/appContext'; +import BaseEngine from './base'; +import { isSchema } from '../utils'; +const debug = Debug('engine:temp'); +export default class TempEngine extends BaseEngine { + static dislayName = 'temp-engine'; + static propTypes = { + __ctx: PropTypes.object, + __schema: PropTypes.object + }; + static defaultProps = { + __ctx: {}, + __schema: {} + }; + + constructor(props, context) { + super(props, context); + this.state = {}; + this.cacheSetState = {}; + debug(`temp.constructor - ${props.__schema.fileName}`); + } + + componentDidMount() { + const ctx = this.props.__ctx; + if (!ctx) return; + const setState = ctx.setState; + this.cacheSetState = setState; + ctx.setState = (...args) => { + setState.call(ctx, ...args); + setTimeout(() => this.forceUpdate(), 0); + }; + debug(`temp.componentDidMount - ${this.props.__schema.fileName}`); + } + componentDidUpdate(prevProps, prevState, snapshot) { + debug(`temp.componentDidUpdate - ${this.props.__schema.fileName}`); + } + componentWillUnmount() { + const ctx = this.props.__ctx; + if (!ctx || !this.cacheSetState) return; + ctx.setState = this.cacheSetState; + delete this.cacheSetState; + debug(`temp.componentWillUnmount - ${this.props.__schema.fileName}`); + } + componentDidCatch(e) { + console.warn(e); + debug(`temp.componentDidCatch - ${this.props.__schema.fileName}`); + } + + render() { + const { __schema, __ctx } = this.props; + if (!isSchema(__schema, true) || __schema.componentName !== 'Temp') { + return '下钻编辑schema结构异常!'; + } + + debug(`temp.render - ${__schema.fileName}`); + + return ( +
+ {this.__createDom()} +
+ ); + } +} diff --git a/packages/react-renderer/src/hoc/addonFactory.js b/packages/react-renderer/src/hoc/addonFactory.js new file mode 100644 index 000000000..7113414d4 --- /dev/null +++ b/packages/react-renderer/src/hoc/addonFactory.js @@ -0,0 +1,55 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import AddonEngine from '../engine/addonEngine'; +import BlockEngine from '../engine/blockEngine'; +import AppContext from '../context/appContext'; +import { forEach, isFileSchema } from '../utils'; +export default function addonFactory(schema, components = {}, componentsMap = {}, config = {}) { + class LNAddonView extends PureComponent { + static dislayName = 'luna-addon-factory'; + static version = config.version || '0.0.0'; + static contextType = AppContext; + static propTypes = { + forwardedRef: PropTypes.func + }; + render() { + if (!schema || schema.componentName !== 'Addon' || !isFileSchema(schema)) { + console.warn('编辑器插件模型结构异常!'); + return null; + } + const { forwardedRef, ...otherProps } = this.props; + const props = { + ...schema.defaultProps, + ...otherProps, + __schema: schema, + ref: forwardedRef + }; + return ( + + + + ); + } + } + const ResComp = React.forwardRef((props, ref) => ); + forEach(schema.static, (val, key) => { + ResComp[key] = val; + }); + ResComp.version = config.version || '0.0.0'; + return ResComp; +} diff --git a/packages/react-renderer/src/hoc/compFactory.js b/packages/react-renderer/src/hoc/compFactory.js new file mode 100644 index 000000000..f966453d8 --- /dev/null +++ b/packages/react-renderer/src/hoc/compFactory.js @@ -0,0 +1,75 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import CompEngine from '../engine/compEngine'; +import BlockEngine from '../engine/blockEngine'; +import AppContext from '../context/appContext'; +import AppHelper from '../utils/appHelper'; +import { forEach, isFileSchema } from '../utils'; +export default function compFactory(schema, components = {}, componentsMap = {}, config = {}) { + // 自定义组件需要有自己独立的appHelper + const appHelper = new AppHelper(config); + class LNCompView extends PureComponent { + static dislayName = 'luna-comp-factory'; + 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 appCtx = ['utils', 'constants']; + appCtx.forEach(key => { + if (!appHelper[key] && this.context && this.context.appHelper && this.context.appHelper[key]) { + appHelper.set(key, this.context.appHelper[key]); + } + }); + const routerCtx = ['history', 'location', 'match']; + routerCtx.forEach(key => { + if (this.context && 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 ( + + + + ); + } + } + + const ResComp = React.forwardRef((props, ref) => ); + forEach(schema.static, (val, key) => { + ResComp[key] = val; + }); + ResComp.version = config.version || '0.0.0'; + return ResComp; +} diff --git a/packages/react-renderer/src/hoc/localeConfig.js b/packages/react-renderer/src/hoc/localeConfig.js new file mode 100644 index 000000000..1829a7a21 --- /dev/null +++ b/packages/react-renderer/src/hoc/localeConfig.js @@ -0,0 +1,29 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import AppContext from '../context/appContext'; +export default function localeConfig(componentName, Component) { + class LNLocaleConfigView extends PureComponent { + static dislayName = 'luna-locale-config'; + static contextType = AppContext; + static propTypes = { + forwardedRef: PropTypes.func + }; + render() { + const { forwardedRef, ...otherProps } = this.props; + const { locale, messages } = this.context; + const localeProps = {}; + if (locale && messages && messages[componentName]) { + localeProps.locale = locale; + localeProps.messages = messages[componentName]; + } + const props = { + ...localeProps, + ...otherProps, + ref: forwardedRef + }; + return ; + } + } + + return React.forwardRef((props, ref) => ); +} diff --git a/packages/react-renderer/src/hoc/suspenseWrapper.js b/packages/react-renderer/src/hoc/suspenseWrapper.js new file mode 100644 index 000000000..6a0224af7 --- /dev/null +++ b/packages/react-renderer/src/hoc/suspenseWrapper.js @@ -0,0 +1,15 @@ +import React, { PureComponent, Suspense } from 'react'; +export default function SuspenseWrapper(fallback = null) { + return function(Component) { + class SuspenseWrapper extends PureComponent { + render() { + return ( + + + + ); + } + } + return SuspenseWrapper; + }; +} diff --git a/packages/react-renderer/src/index.js b/packages/react-renderer/src/index.js new file mode 100644 index 000000000..eb74cd17a --- /dev/null +++ b/packages/react-renderer/src/index.js @@ -0,0 +1,16 @@ +export { default as Canvas } from './comp/canvas'; +export { default as Addon } from './comp/addon'; + +export { default as Engine } from './engine'; + +export { default as CompFactory } from './hoc/compFactory'; +export { default as AddonFatory } from './hoc/addonFactory'; +export { default as LocaleConfig } from './hoc/localeConfig'; + +export { default as AppHelper } from './utils/appHelper'; +export { default as DataHelper } from './utils/dataHelper'; +export { default as DndHelper } from './utils/dndHelper'; +export { default as SchemaHelper } from './utils/schemaHelper'; +export { default as StorageHelper } from './utils/storageHelper'; +export { default as UndoRedoHelper } from './utils/undoRedoHelper'; +export { default as WSHelper } from './utils/wsHelper'; diff --git a/packages/react-renderer/src/utils/appHelper.js b/packages/react-renderer/src/utils/appHelper.js new file mode 100644 index 000000000..0f0a069e8 --- /dev/null +++ b/packages/react-renderer/src/utils/appHelper.js @@ -0,0 +1,49 @@ +import EventEmitter from 'events'; +import Debug from 'debug'; +let instance = null; +const debug = Debug('utils:appHelper'); +EventEmitter.defaultMaxListeners = 100; + +export default class AppHelper extends EventEmitter { + static getInstance = () => { + if (!instance) { + instance = new AppHelper(); + } + return instance; + }; + + constructor(config) { + super(); + instance = this; + Object.assign(this, config); + } + + get(key) { + return this[key]; + } + + set(key, val) { + if (typeof key === 'string') { + this[key] = val; + } else if (typeof key === 'object') { + Object.keys(key).forEach(item => { + this[item] = key[item]; + }); + } + } + + batchOn(events, lisenter) { + if (!Array.isArray(events)) return; + events.forEach(event => this.on(event, lisenter)); + } + + batchOnce(events, lisenter) { + if (!Array.isArray(events)) return; + events.forEach(event => this.once(event, lisenter)); + } + + batchOff(events, lisenter) { + if (!Array.isArray(events)) return; + events.forEach(event => this.off(event, lisenter)); + } +} diff --git a/packages/react-renderer/src/utils/dataHelper.js b/packages/react-renderer/src/utils/dataHelper.js new file mode 100644 index 000000000..cfa5ebd30 --- /dev/null +++ b/packages/react-renderer/src/utils/dataHelper.js @@ -0,0 +1,299 @@ +import { transformArrayToMap, isJSFunction, transformStringToFunction, clone } from './index'; +import { jsonp, mtop, request, get, post, bzb } from './request'; +import Debug from 'debug'; +const DS_STATUS = { + INIT: 'init', + LOADING: 'loading', + LOADED: 'loaded', + ERROR: 'error' +}; +const debug = Debug('utils:dataHelper'); +export default class DataHelper { + constructor(comp, config = {}, appHelper, parser) { + this.host = comp; + this.config = config; + this.parser = parser; + this.ajaxList = (config && config.list) || []; + this.ajaxMap = transformArrayToMap(this.ajaxList, 'id'); + this.dataSourceMap = this.generateDataSourceMap(); + this.appHelper = appHelper; + } + + // 重置config,dataSourceMap状态会被重置; + resetConfig(config = {}) { + this.config = config; + this.ajaxList = (config && config.list) || []; + this.ajaxMap = transformArrayToMap(this.ajaxList, 'id'); + this.dataSourceMap = this.generateDataSourceMap(); + return this.dataSourceMap; + } + + // 更新config,只会更新配置,状态保存; + updateConfig(config = {}) { + this.config = config; + this.ajaxList = (config && config.list) || []; + const ajaxMap = transformArrayToMap(this.ajaxList, 'id'); + // 删除已经移除的接口 + Object.keys(this.ajaxMap).forEach(key => { + if (!ajaxMap[key]) { + delete this.dataSourceMap[key]; + } + }); + this.ajaxMap = ajaxMap; + // 添加未加入到dataSourceMap中的接口 + this.ajaxList.forEach(item => { + if (!this.dataSourceMap[item.id]) { + this.dataSourceMap[item.id] = { + status: DS_STATUS.INIT, + load: (...args) => { + return this.getDataSource(item.id, ...args); + } + }; + } + }); + return this.dataSourceMap; + } + + generateDataSourceMap() { + const res = {}; + this.ajaxList.forEach(item => { + res[item.id] = { + status: DS_STATUS.INIT, + load: (...args) => { + return this.getDataSource(item.id, ...args); + } + }; + }); + return res; + } + + updateDataSourceMap(id, data, error) { + this.dataSourceMap[id].error = error ? error : undefined; + this.dataSourceMap[id].data = data; + this.dataSourceMap[id].status = error ? DS_STATUS.ERROR : DS_STATUS.LOADED; + } + + getInitData() { + const initSyncData = this.parser(this.ajaxList).filter(item => { + if (item.isInit) { + this.dataSourceMap[item.id].status = DS_STATUS.LOADING; + return true; + } + return false; + }); + return this.asyncDataHandler(initSyncData).then(res => { + let dataHandler = this.config.dataHandler; + if (isJSFunction(dataHandler)) { + dataHandler = transformStringToFunction(dataHandler.value); + } + if (!dataHandler || typeof dataHandler !== 'function') return res; + try { + return dataHandler.call(this.host, res); + } catch (e) { + console.error('请求数据处理函数运行出错', e); + return; + } + }); + } + + getDataSource(id, params, otherOptions, callback) { + const req = this.parser(this.ajaxMap[id]); + const options = req.options || {}; + if (typeof otherOptions === 'function') { + callback = otherOptions; + otherOptions = {}; + } + const { headers, ...otherProps } = otherOptions || {}; + if (!req) { + console.warn(`getDataSource API named ${id} not exist`); + return; + } + return this.asyncDataHandler([ + { + ...req, + options: { + ...options, + // 支持参数为array的情况,当参数为array时,不做参数合并 + params: + Array.isArray(options.params) || Array.isArray(params) + ? params || options.params + : { + ...options.params, + ...params + }, + headers: { + ...options.headers, + ...headers + }, + ...otherProps + } + } + ]) + .then(res => { + try { + callback && callback(res && res[id]); + } catch (e) { + console.error('load请求回调函数报错', e); + } + + return res && res[id]; + }) + .catch(err => { + try { + callback && callback(null, err); + } catch (e) { + console.error('load请求回调函数报错', e); + } + + return err; + }); + } + + asyncDataHandler(asyncDataList) { + return new Promise((resolve, reject) => { + const allReq = []; + const doserReq = []; + const doserList = []; + const beforeRequest = this.appHelper && this.appHelper.utils && this.appHelper.utils.beforeRequest; + const afterRequest = this.appHelper && this.appHelper.utils && this.appHelper.utils.afterRequest; + const csrfInput = document.getElementById('_csrf_token'); + const _tb_token_ = csrfInput && csrfInput.value; + asyncDataList.map(req => { + const { id, type, options } = req; + if (!id || !type) return; + if (type === 'doServer') { + const { uri, params } = options || {}; + if (!uri) return; + doserList.push(id); + doserReq.push({ name: uri, package: 'cms', params }); + } else { + allReq.push(req); + } + }); + + if (doserReq.length > 0) { + allReq.push({ + type: 'doServer', + options: { + uri: '/nrsService.do', + cors: true, + method: 'POST', + params: { + data: JSON.stringify(doserReq), + _tb_token_ + } + } + }); + } + if (allReq.length === 0) resolve({}); + const res = {}; + Promise.all( + allReq.map(item => { + return new Promise(resolve => { + const { type, id, dataHandler, options } = item; + const doFetch = (type, options) => { + this.fetchOne(type, options) + .then(data => { + if (afterRequest) { + this.appHelper.utils.afterRequest(item, data, undefined, (data, error) => { + fetchHandler(data, error); + }); + } else { + fetchHandler(data, undefined); + } + }) + .catch(err => { + if (afterRequest) { + // 必须要这么调用,否则beforeRequest中的this会丢失 + this.appHelper.utils.afterRequest(item, undefined, err, (data, error) => { + fetchHandler(data, error); + }); + } else { + fetchHandler(undefined, err); + } + }); + }; + const fetchHandler = (data, error) => { + if (type === 'doServer') { + if (!Array.isArray(data)) { + data = [data]; + } + doserList.forEach((id, idx) => { + const req = this.ajaxMap[id]; + if (req) { + res[id] = this.dataHandler(id, req.dataHandler, data && data[idx], error); + this.updateDataSourceMap(id, res[id], error); + } + }); + } else { + res[id] = this.dataHandler(id, dataHandler, data, error); + this.updateDataSourceMap(id, res[id], error); + } + resolve(); + }; + + if (type === 'doServer') { + doserList.forEach(item => { + this.dataSourceMap[item].status = DS_STATUS.LOADING; + }); + } else { + this.dataSourceMap[id].status = DS_STATUS.LOADING; + } + // 请求切片 + if (beforeRequest) { + // 必须要这么调用,否则beforeRequest中的this会丢失 + this.appHelper.utils.beforeRequest(item, clone(options), options => doFetch(type, options)); + } else { + doFetch(type, options); + } + }); + }) + ) + .then(() => { + resolve(res); + }) + .catch(e => { + reject(e); + }); + }); + } + + dataHandler(id, dataHandler, data, error) { + if (isJSFunction(dataHandler)) { + dataHandler = transformStringToFunction(dataHandler.value); + } + if (!dataHandler || typeof dataHandler !== 'function') return data; + try { + return dataHandler.call(this.host, data, error); + } catch (e) { + console.error('[' + id + ']单个请求数据处理函数运行出错', e); + return; + } + } + + fetchOne(type, options) { + let { uri, method = 'GET', headers, params, ...otherProps } = options; + otherProps = otherProps || {}; + switch (type) { + case 'mtop': + method && (otherProps.method = method); + return mtop(uri, params, otherProps); + case 'jsonp': + return jsonp(uri, params, otherProps); + case 'bzb': + return bzb(uri, params, { + method, + headers, + ...otherProps + }); + default: + method = method.toUpperCase(); + if (method === 'GET') { + return get(uri, params, headers, otherProps); + } else if (method === 'POST') { + return post(uri, params, headers, otherProps); + } + return request(uri, method, params, headers, otherProps); + } + } +} diff --git a/packages/react-renderer/src/utils/dndHelper.js b/packages/react-renderer/src/utils/dndHelper.js new file mode 100644 index 000000000..4eab22fdf --- /dev/null +++ b/packages/react-renderer/src/utils/dndHelper.js @@ -0,0 +1,574 @@ +import ReactDOM from 'react-dom'; +import Debug from 'debug'; +import { isFileSchema, isEmpty, throttle, deepEqual } from './index'; +const DICT = { + left: '左', + right: '右', + top: '上', + bottom: '下', + in: '里' +}; +const TOP_COMPONENT = ['Page', 'Component', 'Temp']; // 顶端模块,不支持放置兄弟节点 +const debug = Debug('utils:dndHelper'); +export default class DndHelper { + constructor(appHelper) { + this.appHelper = appHelper; + this.dragDom = null; + this.canvasEffectDom = null; + this.treeEffectDom = null; + this.containrDom = null; + this.sourceEntity = null; + this.tempEntity = null; + this.dragInfo = null; + this.canvasClearTimer = null; + this.treeClearTimer = null; + this.isDragging = false; + this.dragOverFunc = throttle(this.dragOverFunc, 50); + } + + setCanvasWin(win) { + this.canvasWin = win; + if (this.canvasEffectDom) { + this.canvasWin.document.body.appendChild(this.canvasEffectDom); + } + } + + emit(msg, ...args) { + this.appHelper && this.appHelper.emit(msg, ...args); + } + + dragOverFunc(ev, schemaOrNode, isTree) { + if (!this.isDragging || !this.sourceEntity) return; + const entity = isTree + ? this.getTreeEntity(schemaOrNode, ev) + : { + target: ev.currentTarget, + schema: schemaOrNode + }; + if (this.sourceEntity.schema.__ctx && this.sourceEntity.schema.__ctx.lunaKey === entity.schema.__ctx.lunaKey) + return; + let dragInfo = null; + if (isTree) { + dragInfo = this.getTreeDragInfo(ev, entity); + } else { + dragInfo = this.getDragInfo(ev, entity); + } + if (!dragInfo || deepEqual(this.dragInfo, dragInfo)) return; + this.dragInfo = dragInfo; + this.tempEntity = dragInfo.entity; + this.clearEffect(isTree); + this.addEffect(isTree); + } + + changeCanvas() { + debug('change canvas', this.sourceEntity, this.tempEntity); + if (!this.sourceEntity || !this.tempEntity) return; + if (this.sourceEntity.isAdd) { + debug('add material', this.sourceEntity.schema, this.tempEntity.schema.__ctx.lunaKey, this.dragInfo.position); + this.emit('material.add', { + schema: this.sourceEntity.schema, + targetKey: this.tempEntity.schema.__ctx.lunaKey, + direction: this.dragInfo.position + }); + } else { + this.emit('material.move', { + lunaKey: this.sourceEntity.schema.__ctx.lunaKey, + targetKey: this.tempEntity.schema.__ctx.lunaKey, + direction: this.dragInfo.position + }); + } + } + + getTreeEntity(node, ev) { + if (!node) return; + const schemaHelper = this.appHelper.schemaHelper; + const lunaKey = node.props.eventKey; + const schema = schemaHelper.schemaMap[lunaKey]; + if (!schema) return; + const ref = schemaHelper.compThisMap[lunaKey]; + const currentTarget = ev.currentTarget; + return { + schema, + target: ref && ReactDOM.findDOMNode(ref), + treeNodeTarget: currentTarget + }; + } + + getDragTagDom(tagName) { + if (!this.dragDom) { + const dragDom = document.createElement('div'); + dragDom.id = 'luna-drag-dom'; + dragDom.style.height = '24px'; + dragDom.style.position = 'absolute'; + dragDom.style.zIndex = 10000000; + dragDom.style.transform = 'translateY(-10000px)'; + dragDom.style.background = 'rgba(0, 0, 0, .5)'; + dragDom.style.lineHeight = '24px'; + dragDom.style.color = '#fff'; + dragDom.style.padding = '0px 10px'; + dragDom.style.display = 'inline-block'; + document.body.appendChild(dragDom); + this.dragDom = dragDom; + } + this.dragDom.innerHTML = ` ${tagName}`; + return this.dragDom; + } + + getCanvasEffectDom() { + if (!this.canvasWin) { + throw new Error('should set the canvasWin first'); + } + if (this.canvasClearTimer) { + clearTimeout(this.canvasClearTimer); + this.canvasClearTimer = null; + } + + const { position } = this.dragInfo; + let canvasEffectDom = this.canvasEffectDom; + if (!canvasEffectDom) { + canvasEffectDom = document.createElement('div'); + this.canvasWin.document.body.appendChild(canvasEffectDom); + this.canvasEffectDom = canvasEffectDom; + } + canvasEffectDom.id = 'luna-canvas-effect'; + canvasEffectDom.innerHTML = `${DICT[position]}`; + canvasEffectDom.className = position; + canvasEffectDom.style.display = 'block'; + + return canvasEffectDom; + } + + getTreeEffectDom() { + if (this.treeClearTimer) { + clearTimeout(this.treeClearTimer); + this.treeClearTimer = null; + } + let treeEffectDom = this.treeEffectDom; + if (!treeEffectDom) { + treeEffectDom = document.createElement('div'); + this.treeEffectDom = treeEffectDom; + } + treeEffectDom.id = 'luna-tree-effect'; + treeEffectDom.style.display = 'block'; + return treeEffectDom; + } + + getLunaContainerDom(target) { + if (!target) return null; + let parent = target.parentNode; + while (parent && (!parent.dataset || !parent.dataset.lunaKey)) { + parent = parent.parentNode; + } + return parent; + } + + clearCompTreeEffect() { + const container = document.querySelector('.luna-comp-tree'); + if (!container) return; + + let treeItems = container.querySelectorAll('.tree-item'); + (treeItems || []).forEach(item => { + const classList = item.classList; + if (classList) { + classList.remove('top'); + classList.remove('in'); + classList.remove('bottom'); + classList.remove('tree-item'); + } + }); + } + + getDragInfo(ev, entity) { + if (!this.sourceEntity || !entity) return null; + const { target, schema } = entity; + const sourcePath = this.sourceEntity.schema.__ctx && this.sourceEntity.schema.__ctx.lunaPath; + const targetPath = schema.__ctx.lunaPath; + const sourceTarget = this.sourceEntity.target; + + if (sourcePath === targetPath) return null; + if (targetPath && targetPath.startsWith(sourcePath)) return null; + const componentsMap = this.appHelper.get('componentsMap'); + // if (!componentsMap || !componentsMap[schema.componentName]) return null; + let isContainer = + (componentsMap[schema.componentName] && componentsMap[schema.componentName].isContainer) || isFileSchema(schema); //是否是容器组件 + if (schema.children && typeof schema.children !== 'object') { + //如果children是文本, 非模型结构,则非容器; + isContainer = false; + } + const rect = target.getBoundingClientRect(); + const isSupportIn = + isContainer && + (!schema.children || (schema.children && typeof schema.children === 'object' && isEmpty(schema.children))); + const sourceIsInline = sourceTarget && ['inline-block', 'inline'].includes(getComputedStyle(sourceTarget).display); + const isInline = ['inline-block', 'inline'].includes(getComputedStyle(target).display) && sourceIsInline; + const measure = isInline ? 'width' : 'height'; + + let sn = 0; + let position = 'top'; + if (isContainer) { + sn = isSupportIn ? rect[measure] * 0.25 : Math.min(rect[measure] * 0.5, 10); + } else { + sn = rect[measure] * 0.5; + } + if (TOP_COMPONENT.includes(schema.componentName)) { + // 顶端组件,拖拽over时,只能放在其内部 + position = 'in'; + } else if (isInline && !isContainer) { + if (Math.abs(ev.clientX - rect.left) <= sn) { + position = 'left'; + } else if (Math.abs(ev.clientX - rect.right) <= sn) { + position = 'right'; + } + } else { + if (Math.abs(ev.clientY - rect.top) <= sn) { + position = 'top'; + } else if (Math.abs(ev.clientY - rect.bottom) <= sn) { + position = 'bottom'; + } else { + position = 'in'; + } + } + + // 判断是否是相邻元素, 往左|上拖 + const isPrevSibling = sourceTarget === target.nextElementSibling; + if (isPrevSibling) { + if (position === 'right') position = 'left'; + if (position === 'bottom') { + position = isContainer ? 'in' : 'top'; + } + } + // 判断是否相邻元素,往右|下拖 + const isPostSibling = sourceTarget === target.previousElementSibling; + if (isPostSibling) { + if (position === 'left') position = 'right'; + if (position === 'top') { + position = isContainer ? 'in' : 'bottom'; + } + } + + //如果是容器组件,且包含有子组件,且是in状态,进行智能识别处理; + let subChildren = []; + const getChildren = node => { + if (!node || !node.childNodes || node.childNodes.length === 0) return; + node.childNodes.forEach(child => { + if (child === sourceTarget) return; + if (child && child.getAttribute && child.getAttribute('draggable')) { + const isInline = ['inline', 'inline-block'].includes(getComputedStyle(child).display) && sourceIsInline; + const rect = child.getBoundingClientRect(); + const l = Math.abs(ev.clientX - rect.left); + const r = Math.abs(ev.clientX - rect.right); + const t = Math.abs(ev.clientY - rect.top); + const b = Math.abs(ev.clientY - rect.bottom); + const minXDistance = Math.min(l, r); + const minYDistance = Math.min(t, b); + subChildren.push({ + lunaKey: child.dataset.lunaKey, + node: child, + minDistance: isInline ? [minXDistance, minYDistance] : [minYDistance, minXDistance], + position: isInline ? (l > r ? 'right' : 'left') : b > t ? 'top' : 'bottom' + }); + } else { + getChildren(child); + } + }); + }; + if (position === 'in' && isContainer && !isSupportIn) { + getChildren(target); + subChildren = subChildren.sort((a, b) => { + if (a.minDistance[0] === b.minDistance[0]) { + return a.minDistance[1] - b.minDistance[1]; + } + return a.minDistance[0] - b.minDistance[0]; + }); + const tempChild = subChildren[0]; + if (tempChild) { + if (sourceTarget === tempChild.node.nextElementSibling && ['bottom', 'right'].includes(tempChild.position)) + return null; + if (sourceTarget === tempChild.node.previousElementSibling && ['top', 'left'].includes(tempChild.position)) + return null; + position = tempChild.position; + entity = { + target: tempChild.node, + schema: this.appHelper.schemaHelper.schemaMap[tempChild.lunaKey] + }; + } + } + + const containrDom = position === 'in' ? entity.target : this.getLunaContainerDom(entity.target); + if (this.containrDom !== containrDom) { + if (this.containrDom) { + this.containrDom.style.outline = ''; + } + this.containrDom = containrDom; + } + if (this.containrDom) { + containrDom.style.outline = '1px solid #1aab11'; + } + // debug('drag info:', position, isSupportIn, isContainer, entity); + return { + position, + isSupportIn, + isContainer, + entity + }; + } + + getTreeDragInfo(ev, entity) { + if (!this.sourceEntity || !entity) return null; + const { schema, treeNodeTarget } = entity; + const sourcePath = this.sourceEntity.schema.__ctx && this.sourceEntity.schema.__ctx.lunaPath; + const targetPath = schema.__ctx.lunaPath; + if (sourcePath === targetPath) return null; + if (targetPath && targetPath.startsWith(sourcePath)) return null; + const componentsMap = this.appHelper.get('componentsMap'); + // if (!componentsMap || !componentsMap[schema.componentName]) return null; + let isContainer = + (componentsMap[schema.componentName] && componentsMap[schema.componentName].isContainer) || isFileSchema(schema); //是否是容器组件 + if (schema.children && typeof schema.children !== 'object') { + //如果children是文本, 非模型结构,则非容器; + isContainer = false; + } + const rect = treeNodeTarget.getBoundingClientRect(); + const isSupportIn = + isContainer && + (!schema.children || (schema.children && typeof schema.children === 'object' && isEmpty(schema.children))); + + const sn = isContainer && isSupportIn ? rect.height * 0.25 : rect.height * 0.5; + let position = 'in'; + if (Math.abs(ev.clientY - rect.top) <= sn) { + position = 'top'; + } else if (Math.abs(ev.clientY - rect.bottom) <= sn) { + position = 'bottom'; + } + return { + position, + isSupportIn, + isContainer, + entity + }; + } + + addEffect(isTree) { + if (!this.tempEntity) return; + const { position } = this.dragInfo; + const { target, treeNodeTarget } = this.tempEntity; + // this.clearCompTreeEffect(); + if (isTree) { + //画父元素外框 + let status = true; + let node = treeNodeTarget.parentNode; + while (status) { + if (node && node.parentNode) { + if (node.parentNode.tagName == 'LI' && node.parentNode.classList.contains('next-tree-node')) { + status = false; + if (this.treeNodeTargetParent !== node.parentNode || position === 'in') { + this.treeNodeTargetParent && this.treeNodeTargetParent.classList.remove('selected'); + } + this.treeNodeTargetParent = node.parentNode; + if (position !== 'in') this.treeNodeTargetParent.classList.add('selected'); + } else { + node = node.parentNode; + } + } else { + status = false; + } + } + treeNodeTarget.appendChild(this.getTreeEffectDom()); + this.treeEffectDom.className = position; + } else { + const effectDom = this.getCanvasEffectDom(); + const rect = target.getBoundingClientRect(); + effectDom.style.left = (position === 'right' ? rect.right : rect.left) + 'px'; + effectDom.style.top = + (position === 'bottom' ? rect.bottom : position === 'in' ? (rect.top + rect.bottom) / 2 : rect.top) + 'px'; + effectDom.style.height = ['top', 'in', 'bottom'].includes(position) ? '2px' : rect.height + 'px'; + effectDom.style.width = ['left', 'right'].includes(position) ? '2px' : rect.width + 'px'; + } + } + + clearCanvasEffect() { + if (this.canvasEffectDom) { + this.canvasEffectDom.style.display = 'none'; + } + if (this.containrDom) { + this.containrDom.style.outline = ''; + } + } + + clearTreeEffect() { + if (this.treeEffectDom) { + this.treeEffectDom.style.display = 'none'; + } + if (this.treeNodeTargetParent) { + this.treeNodeTargetParent.classList.remove('selected'); + } + const tempTarget = this.tempEntity && this.tempEntity.treeNodeTarget; + const classList = tempTarget && tempTarget.classList; + if (classList) { + classList.remove('top'); + classList.remove('bottom'); + classList.remove('in'); + classList.remove('tree-item'); + } + } + + clearEffect(isTree) { + if (this.isDragging) { + // if (isTree) { + if (this.treeClearTimer) { + clearTimeout(this.treeClearTimer); + this.treeClearTimer = null; + } + this.treeClearTimer = setTimeout(() => { + this.clearTreeEffect(); + }, 300); + // } else { + if (this.canvasClearTimer) { + clearTimeout(this.canvasClearTimer); + this.canvasClearTimer = null; + } + this.canvasClearTimer = setTimeout(() => { + this.clearCanvasEffect(); + }, 300); + // } + } else { + // if (isTree) { + this.clearTreeEffect(); + // } else { + this.clearCanvasEffect(); + // } + } + } + + handleDragStart(ev, lunaKey) { + ev.stopPropagation(); + const target = ev.currentTarget; + target.style.filter = 'blur(2px)'; + const schema = this.appHelper.schemaHelper.schemaMap[lunaKey]; + ev.dataTransfer.setDragImage(this.getDragTagDom(schema.componentName), 0, 0); + this.sourceEntity = { + target, + schema + }; + this.isDragging = true; + } + + handleDragEnd(ev) { + ev.stopPropagation(); + ev.preventDefault(); + this.isDragging = false; + if (!this.sourceEntity) return; + if (this.sourceEntity.target) { + this.sourceEntity.target.style.filter = ''; + } + this.clearEffect(); + } + + handleDragOver(ev, lunaKey) { + ev.preventDefault(); + ev.stopPropagation(); + this.isDragging = true; + const schema = this.appHelper.schemaHelper.schemaMap[lunaKey]; + this.dragOverFunc( + { + clientX: ev.clientX, + clientY: ev.clientY, + currentTarget: ev.currentTarget + }, + schema + ); + } + + handleDragLeave(ev) { + //避免移动到treeEffectDom上的抖动 + ev.stopPropagation(); + if (!this.tempEntity) return; + const rect = ev.target.getBoundingClientRect(); + // 如果鼠标位置还在当前元素范围内则不认为是dragLeave + if (ev.x >= rect.left && ev.x <= rect.right && ev.y >= rect.top && ev.y <= rect.bottom) return; + debug('canvas drag leave', ev); + this.clearEffect(); + this.dragInfo = null; + this.isDragging = false; + } + + handleDrop(ev) { + ev.stopPropagation(); + debug('drop+++++'); + this.isDragging = false; + this.changeCanvas(); + this.clearEffect(); + } + + handleTreeDragStart(ev) { + const { event, node } = ev; + event.stopPropagation(); + const lunaKey = node.props.eventKey; + const schema = this.appHelper.schemaHelper.schemaMap[lunaKey]; + if (!schema) return; + + event.dataTransfer.setDragImage(this.getDragTagDom(schema.componentName), 0, 0); + this.sourceEntity = this.getTreeEntity(node, event); + if (this.sourceEntity.target) { + this.sourceEntity.target.style.filter = 'blur(2px)'; + } + this.isDragging = true; + } + + handleTreeDragEnd(ev) { + const { event } = ev; + event.stopPropagation(); + event.preventDefault(); + this.isDragging = false; + if (!this.sourceEntity) return; + if (this.sourceEntity.target) { + this.sourceEntity.target.style.filter = ''; + } + this.clearEffect(true); + } + + handleTreeDragOver(ev) { + const { event, node } = ev; + event.preventDefault(); + event.stopPropagation(); + this.isDragging = true; + this.dragOverFunc( + { + clientX: event.clientX, + clientY: event.clientY, + currentTarget: event.currentTarget.children[0] + }, + node, + true + ); + } + + handleTreeDragLeave(ev) { + const { event } = ev; + event.stopPropagation(); + if (!this.tempEntity) return; + //避免移动到treeEffectDom上的抖动 + if (this.treeEffectDom && this.treeEffectDom.parentNode.parentNode === event.currentTarget) return; + debug('++++ drag leave tree', ev, this.isDragging); + this.clearEffect(true); + this.isDragging = false; + } + + handleTreeDrop(ev) { + const { event } = ev; + event.stopPropagation(); + this.isDragging = false; + this.changeCanvas(); + this.clearEffect(true); + } + + handleResourceDragStart(ev, title, schema) { + ev.stopPropagation(); + ev.dataTransfer.setDragImage(this.getDragTagDom(title), -2, -2); + this.sourceEntity = { + isAdd: true, + schema + }; + this.isDragging = true; + } +} diff --git a/packages/react-renderer/src/utils/index.js b/packages/react-renderer/src/utils/index.js new file mode 100644 index 000000000..15cc3b850 --- /dev/null +++ b/packages/react-renderer/src/utils/index.js @@ -0,0 +1,618 @@ +import Debug from 'debug'; +import _keymaster from 'keymaster'; +export const keymaster = _keymaster; +import { forEach as _forEach, shallowEqual as _shallowEqual } from '@ali/b3-one/lib/obj'; +import { serialize as serializeParams } from '@ali/b3-one/lib/url'; +export const forEach = _forEach; +export const shallowEqual = _shallowEqual; +//moment对象配置 +import _moment from 'moment'; +import 'moment/locale/zh-cn'; +export const moment = _moment; +moment.locale('zh-cn'); +import pkg from '../../package.json'; +window.sdkVersion = pkg.version; + +import _pick from 'lodash/pick'; +import _deepEqual from 'lodash/isEqualWith'; +import _clone from 'lodash/cloneDeep'; +import _isEmpty from 'lodash/isEmpty'; +import _throttle from 'lodash/throttle'; +import _debounce from 'lodash/debounce'; + +export const pick = _pick; +export const deepEqual = _deepEqual; +export const clone = _clone; +export const isEmpty = _isEmpty; +export const throttle = _throttle; +export const debounce = _debounce; + +import _serialize from 'serialize-javascript'; +export const serialize = _serialize; +import * as _jsonuri from 'jsonuri'; +export const jsonuri = _jsonuri; +export { get, post, jsonp, mtop, request } from './request'; + +import IntlMessageFormat from 'intl-messageformat'; + +const ReactIs = require('react-is'); +const ReactPropTypesSecret = require('prop-types/lib/ReactPropTypesSecret'); +const factoryWithTypeCheckers = require('prop-types/factoryWithTypeCheckers'); +const PropTypes2 = factoryWithTypeCheckers(ReactIs.isElement, true); + +const EXPRESSION_TYPE = { + JSEXPRESSION: 'JSExpression', + JSFUNCTION: 'JSFunction', + JSSLOT: 'JSSlot' +}; +const EXPRESSION_REG = /^\{\{(\{.*\}|.*?)\}\}$/; +const hasSymbol = typeof Symbol === 'function' && Symbol['for']; +const REACT_FORWARD_REF_TYPE = hasSymbol ? Symbol['for']('react.forward_ref') : 0xead0; +const debug = Debug('utils:index'); + +const ENV = { + TBE: 'TBE', + WEBIDE: 'WEB-IDE', + VSCODE: 'VSCODE', + WEB: 'WEB' +}; + +/** + * @name isSchema + * @description 判断是否是模型结构 + */ +export function isSchema(schema, ignoreArr) { + if (isEmpty(schema)) return false; + if (!ignoreArr && Array.isArray(schema)) return schema.every(item => isSchema(item)); + return !!(schema.componentName && schema.props && (typeof schema.props === 'object' || isJSExpression(schema.props))); +} + +export function isFileSchema(schema) { + if (isEmpty(schema)) return false; + return ['Page', 'Block', 'Component', 'Addon', 'Temp'].includes(schema.componentName); +} + +// 判断当前页面是否被嵌入到同域的页面中 +export function inSameDomain() { + try { + return window.parent !== window && window.parent.location.host === window.location.host; + } catch (e) { + return false; + } +} + +export function getFileCssName(fileName) { + if (!fileName) return; + let name = fileName.replace(/([A-Z])/g, '-$1').toLowerCase(); + return ('luna-' + name) + .split('-') + .filter(p => !!p) + .join('-'); +} + +export function isJSSlot(obj) { + return obj && typeof obj === 'object' && EXPRESSION_TYPE.JSSLOT === obj.type; +} +export function isJSFunction(obj) { + return obj && typeof obj === 'object' && EXPRESSION_TYPE.JSFUNCTION === obj.type; +} +export function isJSExpression(obj) { + //兼容两种写法,有js构造表达式的情况 + const isJSExpressionObj = + obj && typeof obj === 'object' && EXPRESSION_TYPE.JSEXPRESSION === obj.type && typeof obj.value === 'string'; + const isJSExpressionStr = typeof obj === 'string' && EXPRESSION_REG.test(obj.trim()); + return isJSExpressionObj || isJSExpressionStr; +} + +/** + * @name wait + * @description 等待函数 + */ +export function wait(ms) { + return new Promise(resolve => setTimeout(() => resolve(true), ms)); +} + +export function curry(Comp, hocs = []) { + return hocs.reverse().reduce((pre, cur) => { + return cur(pre); + }, Comp); +} + +export function getValue(obj, path, defaultValue) { + if (isEmpty(obj) || typeof obj !== 'object') return defaultValue; + const res = path.split('.').reduce((pre, cur) => { + return pre && pre[cur]; + }, obj); + if (res === undefined) return defaultValue; + return res; +} + +export function parseObj(schemaStr) { + if (typeof schemaStr !== 'string') return schemaStr; + //默认调用顶层窗口的parseObj,保障new Function的window对象是顶层的window对象 + try { + if (inSameDomain() && window.parent.__newFunc) { + return window.parent.__newFunc(`"use strict"; return ${schemaStr}`)(); + } + return new Function(`"use strict"; return ${schemaStr}`)(); + } catch (err) { + return undefined; + } +} + +export function fastClone(obj) { + return parseObj(serialize(obj, { unsafe: true })); +} + +// 更新obj的内容但不改变obj的指针 +export function fillObj(receiver = {}, ...suppliers) { + Object.keys(receiver).forEach(item => { + delete receiver[item]; + }); + Object.assign(receiver, ...suppliers); + return receiver; +} + +// 中划线转驼峰 +export function toHump(name) { + return name.replace(/\-(\w)/g, function(all, letter) { + return letter.toUpperCase(); + }); +} +// 驼峰转中划线 +export function toLine(name) { + return name.replace(/([A-Z])/g, '-$1').toLowerCase(); +} + +// 获取当前环境 +export function getEnv() { + const userAgent = navigator.userAgent; + const isVscode = /Electron\//.test(userAgent); + if (isVscode) return ENV.VSCODE; + const isTheia = window.is_theia === true; + if (isTheia) return ENV.WEBIDE; + return ENV.WEB; +} + +/** + * 用于构造国际化字符串处理函数 + * @param {*} locale 国际化标识,例如 zh-CN、en-US + * @param {*} messages 国际化语言包 + */ +export function generateI18n(locale = 'zh-CN', messages = {}) { + return (key, values = {}) => { + if (!messages || !messages[key]) return ''; + const formater = new IntlMessageFormat(messages[key], locale); + return formater.format(values); + }; +} + +/** + * 判断当前组件是否能够设置ref + * @param {*} Comp 需要判断的组件 + */ +export function acceptsRef(Comp) { + return ( + (Comp.$$typeof && Comp.$$typeof === REACT_FORWARD_REF_TYPE) || (Comp.prototype && Comp.prototype.isReactComponent) + ); +} + +/** + * 黄金令箭埋点 + * @param {String} gmKey 为黄金令箭业务类型 + * @param {Object} params 参数 + * @param {String} logKey 属性串 + */ +export function goldlog(gmKey, params = {}, logKey = 'other') { + // vscode 黄金令箭API + const sendIDEMessage = window.sendIDEMessage || (inSameDomain() && window.parent.sendIDEMessage); + const goKey = serializeParams({ + sdkVersion: pkg.version, + env: getEnv(), + ...params + }); + if (sendIDEMessage) { + sendIDEMessage({ + action: 'goldlog', + data: { + logKey: `/iceluna.core.${logKey}`, + gmKey, + goKey + } + }); + } + window.goldlog && window.goldlog.record(`/iceluna.core.${logKey}`, gmKey, goKey, 'POST'); +} + +// utils为编辑器打包生成的utils文件内容,utilsConfig为数据库存放的utils配置 +export function generateUtils(utils, utilsConfig) { + if (!Array.isArray(utilsConfig)) return { ...utils }; + const res = {}; + utilsConfig.forEach(item => { + if (!item.name || !item.type || !item.content) return; + if (item.type === 'function' && typeof item.content === 'function') { + res[item.name] = item.content; + } else if (item.type === 'npm' && utils[item.name]) { + res[item.name] = utils[item.name]; + } + }); + return res; +} +// 复制到粘贴板 +export function setClipboardData(str) { + return new Promise((resolve, reject) => { + if (typeof str !== 'string') reject('不支持拷贝'); + if (navigator.clipboard) { + navigator.clipboard + .writeText(str) + .then(() => { + resolve(); + }) + .catch(err => { + reject('复制失败,请重试!', err); + }); + } else { + const textArea = document.createElement('textarea'); + textArea.value = str; + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + try { + let successful = document.execCommand('copy'); + if (successful) { + document.body.removeChild(textArea); + resolve(); + } + } catch (err) { + document.body.removeChild(textArea); + reject('复制失败,请重试!', err); + } + } + }); +} +// 获取粘贴板数据 +export function getClipboardData() { + return new Promise((resolve, reject) => { + if (window.clipboardData) { + resolve(window.clipboardData.getData('text')); + } else if (navigator.clipboard) { + return navigator.clipboard + .readText() + .then(res => { + resolve(res); + }) + .catch(err => { + reject('粘贴板获取失败', err); + }); + } else { + reject('粘贴板获取失败'); + } + }); +} +// 将函数返回结果转成promise形式,如果函数有返回值则根据返回值的bool类型判断是reject还是resolve,若函数无返回值默认执行resolve +export function transformToPromise(input) { + if (input instanceof Promise) return input; + return new Promise((resolve, reject) => { + if (input || input === undefined) { + resolve(); + } else { + reject(); + } + }); +} + +export function moveArrayItem(arr, sourceIdx, distIdx, direction) { + if ( + !Array.isArray(arr) || + sourceIdx === distIdx || + sourceIdx < 0 || + sourceIdx >= arr.length || + distIdx < 0 || + distIdx >= arr.length + ) + return arr; + const item = arr[sourceIdx]; + if (direction === 'after') { + arr.splice(distIdx + 1, 0, item); + } else { + arr.splice(distIdx, 0, item); + } + if (sourceIdx < distIdx) { + arr.splice(sourceIdx, 1); + } else { + arr.splice(sourceIdx + 1, 1); + } + return arr; +} + +export function transformArrayToMap(arr, key, overwrite = true) { + if (isEmpty(arr) || !Array.isArray(arr)) return {}; + const res = {}; + arr.forEach(item => { + const curKey = item[key]; + if (item[key] === undefined) return; + if (res[curKey] && !overwrite) return; + res[curKey] = item; + }); + return res; +} + +export function checkPropTypes(value, name, rule, componentName) { + if (typeof rule === 'string') { + rule = new Function(`"use strict"; const PropTypes = arguments[0]; return ${rule}`)(PropTypes2); + } + if (!rule || typeof rule !== 'function') { + console.warn('checkPropTypes should have a function type rule argument'); + return true; + } + const err = rule( + { + [name]: value + }, + name, + componentName, + 'prop', + null, + ReactPropTypesSecret + ); + if (err) { + console.warn(err); + } + return !err; +} + +export function transformSchemaToPure(obj) { + const pureObj = obj => { + if (Array.isArray(obj)) { + return obj.map(item => pureObj(item)); + } else if (typeof obj === 'object') { + // 对于undefined及null直接返回 + if (!obj) return obj; + const res = {}; + forEach(obj, (val, key) => { + if (key.startsWith('__') && key !== '__ignoreParse') return; + res[key] = pureObj(val); + }); + return res; + } + return obj; + }; + return pureObj(obj); +} + +export function transformSchemaToStandard(obj) { + const standardObj = obj => { + if (Array.isArray(obj)) { + return obj.map(item => standardObj(item)); + } else if (typeof obj === 'object') { + // 对于undefined及null直接返回 + if (!obj) return obj; + const res = {}; + forEach(obj, (val, key) => { + if (key.startsWith('__') && key !== '__ignoreParse') return; + if (isSchema(val) && key !== 'children' && obj.type !== 'JSSlot') { + res[key] = { + type: 'JSSlot', + value: standardObj(val) + }; + // table特殊处理 + if (key === 'cell') { + res[key].params = ['value', 'index', 'record']; + } + } else { + res[key] = standardObj(val); + } + }); + return res; + } else if (typeof obj === 'function') { + return { + type: 'JSFunction', + value: obj.toString() + }; + } else if (typeof obj === 'string' && EXPRESSION_REG.test(obj.trim())) { + const regRes = obj.trim().match(EXPRESSION_REG); + return { + type: 'JSExpression', + value: (regRes && regRes[1]) || '' + }; + } + return obj; + }; + return standardObj(obj, false); +} + +export function transformStringToFunction(str) { + if (typeof str !== 'string') return str; + if (inSameDomain() && window.parent.__newFunc) { + return window.parent.__newFunc(`"use strict"; return ${str}`)(); + } else { + return new Function(`"use strict"; return ${str}`)(); + } +} + +export function addCssTag(id, content) { + let styleTag = document.getElementById(id); + if (styleTag) { + styleTag.innerHTML = content; + return; + } + styleTag = document.createElement('style'); + styleTag.id = id; + styleTag.class = 'luna-style'; + styleTag.innerHTML = content; + document.head.appendChild(styleTag); +} + +// 注册快捷 +export function registShortCuts(config, appHelper) { + const keyboardFilter = (keymaster.filter = event => { + let eTarget = event.target || event.srcElement; + let tagName = eTarget.tagName; + let isInput = !!(tagName == 'INPUT' || tagName == 'SELECT' || tagName == 'TEXTAREA'); + let isContenteditable = !!eTarget.getAttribute('contenteditable'); + if (isInput || isContenteditable) { + if (event.metaKey === true && [70, 83].includes(event.keyCode)) event.preventDefault(); //禁止触发chrome原生的页面保存或查找 + return false; + } else { + return true; + } + }); + + const ideMessage = appHelper.utils && appHelper.utils.ideMessage; + + //复制 + if (!document.copyListener) { + document.copyListener = e => { + if (!keyboardFilter(e) || appHelper.isCopying) return; + const schema = appHelper.schemaHelper && appHelper.schemaHelper.schemaMap[appHelper.activeKey]; + if (!schema || !isSchema(schema)) return; + appHelper.isCopying = true; + const schemaStr = serialize(transformSchemaToPure(schema), { + unsafe: true + }); + setClipboardData(schemaStr) + .then(() => { + ideMessage && ideMessage('success', '当前内容已复制到剪贴板,请使用快捷键Command+v进行粘贴'); + appHelper.emit('schema.copy', schemaStr, schema); + appHelper.isCopying = false; + }) + .catch(errMsg => { + ideMessage && ideMessage('error', errMsg); + appHelper.isCopying = false; + }); + }; + document.addEventListener('copy', document.copyListener); + if (window.parent.vscode) { + keymaster('command+c', document.copyListener); + } + } + + //粘贴 + if (!document.pasteListener) { + const doPaste = (e, text) => { + if (!keyboardFilter(e) || appHelper.isPasting) return; + const schemaHelper = appHelper.schemaHelper; + let targetKey = appHelper.activeKey; + let direction = 'after'; + const topKey = schemaHelper.schema && schemaHelper.schema.__ctx && schemaHelper.schema.__ctx.lunaKey; + if (!targetKey || topKey === targetKey) { + const schemaHelper = appHelper.schemaHelper; + const topKey = schemaHelper.schema && schemaHelper.schema.__ctx && schemaHelper.schema.__ctx.lunaKey; + if (!topKey) return; + targetKey = topKey; + direction = 'in'; + } + appHelper.isPasting = true; + const schema = parseObj(text); + if (!isSchema(schema)) { + appHelper.emit('illegalSchema.paste', text); + // ideMessage && ideMessage('error', '当前内容不是模型结构,不能粘贴进来!'); + console.warn('paste schema illegal'); + appHelper.isPasting = false; + return; + } + appHelper.emit('material.add', { + schema, + targetKey, + direction + }); + appHelper.isPasting = false; + appHelper.emit('schema.paste', schema); + }; + document.pasteListener = e => { + const clipboardData = e.clipboardData || window.clipboardData; + const text = clipboardData && clipboardData.getData('text'); + doPaste(e, text); + }; + document.addEventListener('paste', document.pasteListener); + if (window.parent.vscode) { + keymaster('command+v', e => { + const sendIDEMessage = window.parent.sendIDEMessage; + sendIDEMessage && + sendIDEMessage({ + action: 'readClipboard' + }) + .then(text => { + doPaste(e, text); + }) + .catch(err => { + console.warn(err); + }); + }); + } + } + + (config || []).forEach(item => { + keymaster(item.keyboard, ev => { + ev.preventDefault(); + item.handler(ev, appHelper, keymaster); + }); + }); +} + +// 取消注册快捷 +export function unRegistShortCuts(config) { + (config || []).forEach(item => { + keymaster.unbind(item.keyboard); + }); + if (window.parent.vscode) { + keymaster.unbind('command+c'); + keymaster.unbind('command+v'); + } + if (document.copyListener) { + document.removeEventListener('copy', document.copyListener); + delete document.copyListener; + } + if (document.pasteListener) { + document.removeEventListener('paste', document.pasteListener); + delete document.pasteListener; + } +} + +export function parseData(schema, self) { + if (isJSExpression(schema)) { + return parseExpression(schema, self); + } else if (typeof schema === 'string') { + return schema.trim(); + } else if (Array.isArray(schema)) { + return schema.map(item => parseData(item, self)); + } else if (typeof schema === 'function') { + return schema.bind(self); + } else if (typeof schema === 'object') { + // 对于undefined及null直接返回 + if (!schema) return schema; + const res = {}; + forEach(schema, (val, key) => { + if (key.startsWith('__')) return; + res[key] = parseData(val, self); + }); + return res; + } + return schema; +} + +/*全匹配{{开头,}}结尾的变量表达式,或者对象类型JSExpression,且均不支持省略this */ +export function parseExpression(str, self) { + try { + const contextArr = ['"use strict";', 'var __self = arguments[0];']; + contextArr.push('return '); + let tarStr; + //向前兼容,支持标准协议新格式 + if (typeof str === 'string') { + const regRes = str.trim().match(EXPRESSION_REG); + tarStr = regRes[1]; + } else { + tarStr = (str.value || '').trim(); + } + tarStr = tarStr.replace(/this(\W|$)/g, (a, b) => `__self${b}`); + tarStr = contextArr.join('\n') + tarStr; + //默认调用顶层窗口的parseObj,保障new Function的window对象是顶层的window对象 + if (inSameDomain() && window.parent.__newFunc) { + return window.parent.__newFunc(tarStr)(self); + } + return new Function(tarStr)(self); + } catch (err) { + debug('parseExpression.error', err, str, self); + return undefined; + } +} diff --git a/packages/react-renderer/src/utils/postMessager.js b/packages/react-renderer/src/utils/postMessager.js new file mode 100644 index 000000000..c0a72e7c6 --- /dev/null +++ b/packages/react-renderer/src/utils/postMessager.js @@ -0,0 +1,59 @@ +import EventEmitter from 'events'; +import Debug from 'debug'; +const debug = Debug('utils:postMessager'); +EventEmitter.defaultMaxListeners = 100; + +export class InnerMessager extends EventEmitter { + constructor() { + super(); + this.handleReceive = this.handleReceive.bind(this); + window.addEventListener('message', this.handleReceive, false); + } + + sendMsg(type, data, targetOrigin = '*') { + window.parent && + window.parent.postMessage( + { + type, + data + }, + targetOrigin + ); + } + + handleReceive(e) { + if (!e.data || !e.data.type) return; + this.emit(e.data.type, e.data.data); + } + + destroy() { + window.removeEventListener('message', this.handleReceive); + } +} + +export class OuterMessager extends EventEmitter { + constructor(innerWindow) { + super(); + this.innerWindow = innerWindow; + this.handleReceive = this.handleReceive.bind(this); + window.addEventListener('message', this.handleReceive, false); + } + sendMsg(type, data, targetOrigin = '*') { + this.innerWindow && + this.innerWindow.postMessage( + { + type, + data + }, + targetOrigin + ); + } + + handleReceive(e) { + if (!e.data || !e.data.type) return; + this.emit(e.data.type, e.data.data); + } + destroy() { + window.removeEventListener('message', this.handleReceive); + } +} diff --git a/packages/react-renderer/src/utils/request.js b/packages/react-renderer/src/utils/request.js new file mode 100644 index 000000000..6b33bfc81 --- /dev/null +++ b/packages/react-renderer/src/utils/request.js @@ -0,0 +1,172 @@ +import 'whatwg-fetch'; +import fetchMtop from '@ali/lib-mtop'; +import fetchJsonp from 'fetch-jsonp'; +import bzbRequest from '@ali/bzb-request'; +import Debug from 'debug'; +import { serialize, buildUrl, parseUrl } from '@ali/b3-one/lib/url'; +const debug = Debug('utils:request'); +export function get(dataAPI, params = {}, headers = {}, otherProps = {}) { + headers = { + Accept: 'application/json', + ...headers + }; + dataAPI = buildUrl(dataAPI, params); + return request(dataAPI, 'GET', null, headers, otherProps); +} + +export function post(dataAPI, params = {}, headers = {}, otherProps = {}) { + headers = { + Accept: 'application/json', + 'Content-Type': 'application/x-www-form-urlencoded', + ...headers + }; + return request( + dataAPI, + 'POST', + headers['Content-Type'].indexOf('application/json') > -1 || Array.isArray(params) + ? JSON.stringify(params) + : serialize(params), + headers, + otherProps + ); +} + +export function request(dataAPI, method = 'GET', data, headers = {}, otherProps = {}) { + switch (method) { + case 'PUT': + case 'DELETE': + headers = { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...headers + }; + data = JSON.stringify(data || {}); + break; + } + return new Promise((resolve, reject) => { + if (otherProps.timeout) { + setTimeout(() => { + reject(new Error('timeout')); + }, otherProps.timeout); + } + fetch(dataAPI, { + method, + credentials: 'include', + headers, + body: data, + ...otherProps + }) + .then(response => { + switch (response.status) { + case 200: + case 201: + case 202: + return response.json(); + case 204: + if (method === 'DELETE') { + return { + success: true + }; + } else { + return { + __success: false, + code: response.status + }; + } + case 400: + case 401: + case 403: + case 404: + case 406: + case 410: + case 422: + case 500: + return response + .json() + .then(res => { + return { + __success: false, + code: response.status, + data: res + }; + }) + .catch(() => { + return { + __success: false, + code: response.status + }; + }); + } + return null; + }) + .then(json => { + if (json && json.__success !== false) { + resolve(json); + } else { + delete json.__success; + reject(json); + } + }) + .catch(err => { + reject(err); + }); + }); +} + +export function jsonp(dataAPI, params = {}, otherProps = {}) { + return new Promise((resolve, reject) => { + otherProps = { + timeout: 5000, + ...otherProps + }; + fetchJsonp(buildUrl(dataAPI, params), otherProps) + .then(response => response.json()) + .then(json => { + if (json) { + resolve(json); + } else { + reject(); + } + }) + .catch(err => { + reject(err); + }); + }); +} + +export function mtop(dataAPI, params, otherProps = {}) { + fetchMtop.config.subDomain = otherProps.subDomain || 'm'; + return fetchMtop.request({ + api: dataAPI, + v: '1.0', + data: params, + ecode: otherProps.ecode || 0, + type: otherProps.method || 'GET', + dataType: otherProps.dataType || 'jsonp', + AntiFlood: true, // 防刷 + timeout: otherProps.timeout || 20000 + }); +} + +export function bzb(apiCode, params, otherProps = {}) { + // 通过url参数设置小二工作台请求环境 + const getUrlEnv = () => { + try { + if (window.parent && window.parent.location.host === window.location.host) { + const urlInfo = parseUrl(window.parent && window.parent.location.href); + return urlInfo && urlInfo.params && urlInfo.params._env; + } + const urlInfo = parseUrl(window.location.href); + return urlInfo && urlInfo.params && urlInfo.params._env; + } catch (e) { + return null; + } + }; + + otherProps.method = otherProps.method || 'GET'; + otherProps.env = getUrlEnv() || otherProps.env || 'prod'; + return bzbRequest(apiCode, { + data: params, + ...otherProps + }); +} diff --git a/packages/react-renderer/src/utils/schemaHelper.js b/packages/react-renderer/src/utils/schemaHelper.js new file mode 100644 index 000000000..474edc705 --- /dev/null +++ b/packages/react-renderer/src/utils/schemaHelper.js @@ -0,0 +1,482 @@ +import { forEach } from '@ali/b3-one/lib/obj'; +import { + clone, + fastClone, + jsonuri, + isSchema, + isFileSchema, + isJSFunction, + isJSExpression, + parseObj, + transformSchemaToPure, + transformSchemaToStandard, + isEmpty, + moveArrayItem, + serialize, + deepEqual +} from './index'; +import Debug from 'debug'; +import compFactory from '../hoc/compFactory'; +const debug = Debug('utils:schemaHelper'); +let keyIndex = 0; +export default class SchemaHelper { + constructor(schema, appHelper) { + this.appHelper = appHelper; + this.reset(schema, true); + } + + reset(schema, isInit) { + debug('start reset'); + this.emit('schemaHelper.schema.beforeReset'); + this.schemaMap = {}; + this.blockSchemaMap = {}; + this.compThisMap = {}; + this.blockTree = {}; + this.compTreeMap = {}; + this.compCtxMap = {}; + this.rebuild(schema, isInit); + this.emit('schemaHelper.schema.afterReset'); + } + + add(schema, targetKey, direction) { + this.emit('schemaHelper.material.beforeAdd'); + const targetSchema = this.schemaMap[targetKey]; + if (isEmpty(schema) || !targetSchema) return; + let targetPath = targetSchema.__ctx.lunaPath; + if (targetPath === '' && direction !== 'in') { + console.warn('add error'); + return; + } + let newSchema = []; + if (Array.isArray(schema)) { + newSchema = schema.filter(item => isSchema(item, true)); + } else if (isSchema(schema)) { + newSchema = [schema]; + } else { + console.error('模型结构异常'); + return; + } + if (direction === 'in') { + const targetNode = jsonuri.get(this.schema, targetPath); + targetNode.children = (targetNode.children || []).concat(newSchema); + //jsonuri.set(this.schema, targetPath, targetNode); + } else { + direction = ['left', 'top'].includes(direction) ? 'before' : 'after'; + newSchema.reverse().forEach(item => { + jsonuri.insert(this.schema, targetPath, item, direction); + }); + } + const addKey = `luna_${keyIndex + 1}`; + this.rebuild(this.schema); + this.emit('schemaHelper.material.afterAdd', addKey); + return addKey; + } + + remove(lunaKey) { + this.emit('schemaHelper.material.beforeRemove'); + const schema = this.schemaMap[lunaKey]; + if (!schema) return; + const lunaPath = schema.__ctx.lunaPath; + if (lunaPath === '') { + console.warn('root node can not be removed'); + return; + } + + jsonuri.rm(this.schema, lunaPath); + delete this.schemaMap[lunaKey]; + delete this.blockSchemaMap[lunaKey]; + this.rebuild(this.schema); + this.emit('schemaHelper.material.afterRemove'); + } + + move(lunaKey, targetKey, direction) { + this.emit('schemaHelper.material.beforeMove'); + debug('start move'); + const schema = this.schemaMap[lunaKey]; + const targetSchema = this.schemaMap[targetKey]; + if (!schema || !targetSchema) return; + let lunaPath = schema.__ctx.lunaPath; + let targetPath = targetSchema.__ctx.lunaPath; + if (lunaPath === '' || (targetPath === '' && direction !== 'in')) { + console.warn('move error'); + return; + } + const res = /(.*)\/(\d+)$/.exec(lunaPath); + const prefix = res && res[1]; + const attr = res && res[2]; + if (!prefix || !attr) { + console.warn('异常结构'); + return; + } + const sourceIdx = parseInt(attr); + const reg = new RegExp(`^${prefix}/(\\d+)$`); + const regRes = reg.exec(targetPath); + const sourceParent = jsonuri.get(this.schema, prefix); + direction = direction === 'in' ? 'in' : ['left', 'top'].includes(direction) ? 'before' : 'after'; + if (regRes && regRes[1] && direction !== 'in') { + const distIdx = parseInt(regRes[1]); + moveArrayItem(sourceParent, sourceIdx, distIdx, direction); + } else { + if (direction === 'in') { + const targetNode = jsonuri.get(this.schema, targetPath); + targetNode.children = targetNode.children || []; + if (Array.isArray(targetNode.children)) { + targetNode.children.push(schema); + } + jsonuri.set(this.schema, targetPath, targetNode); + } else { + jsonuri.insert(this.schema, targetPath, schema, direction); + } + sourceParent.splice(sourceIdx, 1); + } + this.rebuild(this.schema); + this.emit('schemaHelper.material.afterMove'); + } + + //组件上移 下移 + // direction 取值 down/up + slide(lunaKey, direction) { + const schema = this.schemaMap[lunaKey]; + if (!schema || !direction) return; + const lunaPath = schema.__ctx && schema.__ctx.lunaPath; + if (!lunaPath) return; + if (direction === 'up' && lunaPath.endsWith('/0')) return; + const targetPath = lunaPath.replace(/\/(\d+)$/, (res, idx) => { + return `/${direction === 'down' ? parseInt(idx) + 1 : parseInt(idx) - 1}`; + }); + const targetSchema = this.getSchemaByPath(targetPath); + const targetKey = targetSchema && targetSchema.__ctx && targetSchema.__ctx.lunaKey; + if (!targetKey) return; + this.move(lunaKey, targetKey, direction === 'down' ? 'bottom' : 'top'); + } + + // 快速复制 + copy(lunaKey) { + this.emit('schemaHelper.material.beforeCopy'); + const schema = this.schemaMap[lunaKey]; + if (!schema) return; + const newSchema = transformSchemaToPure(fastClone(schema)); + delete newSchema.__ctx; + const addKey = this.add(newSchema, schema.__ctx.lunaKey, 'bottom'); + this.emit('schemaHelper.material.afterCopy', addKey); + return addKey; + } + + update(lunaKey, props) { + this.emit('schemaHelper.material.beforeUpdate'); + const schema = this.schemaMap[lunaKey]; + if (!schema) return; + const { + __state, + __defaultProps, + __fileName, + __scss, + __loop, + __loopArgs, + __condition, + __lifeCycles, + __methods, + __dataSource, + children, + ...otherProps + } = props; + debug('update props', props); + + //自定义组件才处理defaultProps + if (schema.componentName === 'Component' && '__defaultProps' in props) { + if (!__defaultProps || typeof __defaultProps !== 'object' || isEmpty(__defaultProps)) { + delete schema.defaultProps; + } else { + schema.defaultProps = __defaultProps; + } + this.appHelper.components[schema.fileName.replace(/^\w/, a => a.toUpperCase())] = compFactory(schema); + } + + // 如果loop值没有设置有效值,则删除schema中这个的字段 + if ('__loop' in props) { + if (!__loop || isEmpty(__loop)) { + delete schema.loop; + } else { + schema.loop = __loop; + } + } + + // 指定循环上下文变量名 + if ('__loopArgs' in props) { + if ( + __loopArgs === undefined || + (typeof __loopArgs === 'object' && isEmpty(__loopArgs)) || + !Array.isArray(__loopArgs) || + __loopArgs.every(item => !item) + ) { + delete schema.loopArgs; + } else { + schema.loopArgs = __loopArgs; + } + } + + // 判断条件 + if ('__condition' in props) { + if (__condition === undefined) { + delete schema.condition; + } else { + schema.condition = __condition; + } + } + + // 处理容器类组件需要考虑的字段 + if (isFileSchema(schema)) { + // filename + if ('__fileName' in props) { + schema.fileName = __fileName; + } + // state + if ('__state' in props) { + // 重走生命周期 + schema.__ctx && ++schema.__ctx.idx; + if (!__state || typeof __state !== 'object' || isEmpty(__state)) { + delete schema.state; + } else { + schema.state = __state; + } + } + // 生命周期 + if ('__lifeCycles' in props) { + if (!__lifeCycles || typeof __lifeCycles !== 'object' || isEmpty(__lifeCycles)) { + delete schema.lifeCycles; + } else { + schema.lifeCycles = __lifeCycles; + } + } + // 自定义方法 + if ('__methods' in props) { + if (!__methods || typeof __methods !== 'object' || isEmpty(__methods)) { + delete schema.methods; + } else { + schema.methods = __methods; + } + } + + // 数据源设置 + if ('__dataSource' in props) { + if (this.needContainerReload(schema.dataSource, __dataSource)) { + schema.__ctx && ++schema.__ctx.idx; + } + if (__dataSource === undefined || (typeof __dataSource === 'object' && isEmpty(__dataSource))) { + delete schema.dataSource; + } else { + schema.dataSource = __dataSource; + } + } + + // 如果scss值没有设置有效值,则删除schema中这个的字段 + if ('__scss' in props) { + if (!__scss) { + delete schema.scss; + } else { + schema.scss = __scss; + } + } + } + + // 子组件 + if ('children' in props) { + if (children === undefined || (typeof children === 'object' && isEmpty(children))) { + delete schema.children; + } else { + schema.children = children; + } + } + + schema.props = { + ...schema.props, + ...otherProps + }; + + //过滤undefined属性 + Object.keys(schema.props).map(key => { + if (schema.props[key] === undefined) { + delete schema.props[key]; + } + }); + + this.rebuild(this.schema); + this.emit('schemaHelper.material.afterUpdate'); + } + + createSchema(componentName, props, isContainer) { + const schema = { + componentName, + props: props || {}, + __ctx: { + lunaKey: ++this.lunaKey + } + }; + if (isContainer) { + schema.children = []; + } + return schema; + } + + rebuild(schema, isInit) { + if (!isFileSchema(schema)) { + debug('top schema should be a file type'); + //对于null的schema特殊处理一下 + if (schema === null) { + this.schema = schema; + this.emit(`schemaHelper.schema.${isInit ? 'afterInit' : 'afterUpdate'}`); + } + return; + } + this.blockTree = null; + this.compTreeMap = {}; + this.compTree = null; + this.schemaMap = {}; + this.blockSchemaMap = {}; + this.compCtxMap = {}; + const buildSchema = (schema, parentBlockNode, parentCompNode, path = '') => { + if (Array.isArray(schema)) { + return schema.map((item, idx) => buildSchema(item, parentBlockNode, parentCompNode, `${path}/${idx}`)); + } else if (typeof schema === 'object') { + // 对于undefined及null直接返回 + if (!schema) return schema; + //JSFunction转函数 + if (isJSFunction(schema)) { + if (typeof schema.value === 'string') { + let tarFun = parseObj(schema.value); + if (typeof tarFun === 'function') { + return tarFun; + } + } else if (typeof schema.value === 'function') { + return schema.value; + } + return schema; + } + //如果是对象且是JSExpression + if (isJSExpression(schema)) { + return '{{' + schema.value + '}}'; + } + const res = {}; + if (isSchema(schema)) { + res.__ctx = schema.__ctx; + if (!res.__ctx) { + const lunaKey = `luna_${++keyIndex}`; + res.__ctx = { + idx: 0, + lunaKey, + lunaPath: path, + parentKey: parentCompNode && parentCompNode.lunaKey, + blockKey: parentBlockNode && parentBlockNode.lunaKey + }; + } else { + res.__ctx.lunaPath = path; + } + const label = schema.componentName + (schema.fileName ? '-' + schema.fileName : ''); + const lunaKey = res.__ctx && res.__ctx.lunaKey; + this.schemaMap[lunaKey] = res; + if (isFileSchema(schema)) { + this.blockSchemaMap[lunaKey] = res; + + const blockNode = { + label, + lunaKey, + isFile: true, + children: [] + }; + this.compTreeMap[lunaKey] = blockNode; + const compNode = clone(blockNode); + if (parentBlockNode) { + parentBlockNode.children.push(blockNode); + } else { + this.blockTree = blockNode; + } + parentBlockNode = blockNode; + if (parentCompNode) { + parentCompNode.children.push(compNode); + } else { + this.compTree = compNode; + } + parentCompNode = compNode; + } else { + const compNode = { + label, + lunaKey, + children: [] + }; + parentCompNode.children.push(compNode); + parentCompNode = compNode; + } + } + forEach(schema, (val, key) => { + if (key.startsWith('__')) { + res[key] = val; + } else { + res[key] = buildSchema(val, parentBlockNode, parentCompNode, `${path}/${key}`); + } + }); + return res; + } + return schema; + }; + this.emit(`schemaHelper.schema.${isInit ? 'beforeInit' : 'beforeUpdate'}`); + this.schema = buildSchema(schema); + this.emit(`schemaHelper.schema.${isInit ? 'afterInit' : 'afterUpdate'}`); + } + + needContainerReload(preData = {}, nextData = {}) { + if ( + typeof preData.dataHandler === 'function' && + typeof nextData.dataHandler === 'function' && + preData.dataHandler.toString() !== nextData.dataHandler.toString() + ) { + return true; + } else if (preData.dataHandler !== nextData.dataHandler) { + return true; + } + return !deepEqual( + (preData.list || []).filter(item => item.isInit), + (nextData.list || []).filter(item => item.isInit), + (pre, next) => { + if (typeof pre === 'function' && next === 'function') { + return pre.toString() === next.toString(); + } + } + ); + } + + emit(msg, ...args) { + this.appHelper && this.appHelper.emit(msg, ...args); + } + + get(key) { + return this[key]; + } + + getSchemaByPath(path) { + return jsonuri.get(this.schema, path); + } + + getSchema() { + return this.schema; + } + + getPureSchema() { + return transformSchemaToPure(this.schema); + } + + getPureSchemaStr() { + return serialize(this.getPureSchema(), { + unsafe: true + }); + } + + getStandardSchema() { + return transformSchemaToStandard(this.schema); + } + + getStandardSchemaStr() { + return serialize(this.getStandardSchema(), { + unsafe: true + }); + } +} diff --git a/packages/react-renderer/src/utils/storageHelper.js b/packages/react-renderer/src/utils/storageHelper.js new file mode 100644 index 000000000..4f840bde9 --- /dev/null +++ b/packages/react-renderer/src/utils/storageHelper.js @@ -0,0 +1,81 @@ +import localforage from 'localforage'; +import Debug from 'debug'; +import { serialize } from './index'; + +const debug = Debug('utils:storageHelper'); +export default class StorageHelper { + constructor(name) { + this.store = localforage.createInstance(name); + } + + getItem(key) { + if (!this.store) { + throw new Error('store instance not exist'); + } + return this.store.getItem(key); + } + + setItem(key, value) { + if (!this.store) { + throw new Error('store instance not exist'); + } + return this.store.setItem(key, value); + } + + removeItem(key) { + if (!this.store) { + throw new Error('store instance not exist'); + } + return this.store.removeItem(key); + } + + clear() { + if (!this.store) { + throw new Error('store instance not exist'); + } + return this.store.clear(); + } + + addHistory(key, code, limit = 10) { + return new Promise((resolve, reject) => { + key = '__luna_history_' + key; + this.store + .getItem(key) + .then(res => { + let codeStr = serialize(code, { + unsafe: true + }); + if (res && res[0] && res[0].code) { + if (codeStr === res[0].code) return; + } + res = res || []; + let newId = 1; + if (res && res[0] && res[0].id) { + newId = res[0].id + 1; + } + res.unshift({ + id: newId, + time: +new Date(), + code: codeStr + }); + this.store + .setItem(key, res.slice(0, limit)) + .then(res => { + resolve(res); + }) + .catch(reject); + }) + .catch(reject); + }); + } + + getHistory(key) { + key = '__luna_history_' + key; + return this.store.getItem(key); + } + + clearHistory(key) { + key = '__luna_history_' + key; + this.store.removeItem(key); + } +} diff --git a/packages/react-renderer/src/utils/undoRedoHelper.js b/packages/react-renderer/src/utils/undoRedoHelper.js new file mode 100644 index 000000000..1b6d030ba --- /dev/null +++ b/packages/react-renderer/src/utils/undoRedoHelper.js @@ -0,0 +1,88 @@ +import Debug from 'debug'; +import { fastClone } from './index'; +const DEFAULT_CONFIG = { + limit: 20 +}; +const debug = Debug('utils:undoRedoHelper'); +export default class UndoRedoHelper { + constructor(config) { + this.config = { ...DEFAULT_CONFIG, ...config }; + this.data = {}; + } + + create(key, value, forceCreate) { + if (!this.data[key] || forceCreate) { + this.data[key] = { + list: [fastClone(value)], + idx: 0 + }; + } + return this.data[key]; + } + + delete(key) { + delete this.data[key]; + } + + resetRecord(key, value) { + const data = this.data[key]; + if (!data || !data.list) return; + data.list = data.list.slice(0, data.idx + 1); + data.list[data.idx] = fastClone(value); + } + + record(key, value) { + const data = this.data[key]; + const limit = this.config.limit; + if (!data || !data.list) return; + data.list = data.list.slice(0, data.idx + 1); + if (data.list.length >= limit) { + data.list.shift(); + } + data.list.push(fastClone(value)); + ++data.idx; + } + + undo(key) { + const data = this.data[key]; + if (!data || !data.list) return null; + //若没有前置操作,返回当前数据 + if (data.idx <= 0) return data.list[data.idx]; + --data.idx; + return data.list[data.idx]; + } + redo(key) { + const data = this.data[key]; + if (!data || !data.list) return null; + //若没有后续操作,返回当前数据 + if (data.idx >= data.list.length - 1) return data.list[data.idx]; + ++data.idx; + return data.list[data.idx]; + } + + past(key) { + const data = this.data[key]; + if (!data || !data.list || data.idx <= 0) return null; + return data.list[data.idx - 1]; + } + + present(key) { + const data = this.data[key]; + if (!data || !data.list) return null; + return data.list[data.idx]; + } + + future(key) { + const data = this.data[key]; + if (!data || !data.list || data.idx >= data.list.length - 1) return null; + return data.list[data.idx + 1]; + } + + get(key) { + return { + past: this.past(key), + present: this.present(key), + future: this.future(key) + }; + } +} diff --git a/packages/react-renderer/src/utils/wsHelper.js b/packages/react-renderer/src/utils/wsHelper.js new file mode 100644 index 000000000..e9caa07db --- /dev/null +++ b/packages/react-renderer/src/utils/wsHelper.js @@ -0,0 +1,87 @@ +import Debug from 'debug'; +import client from 'socket.io-client'; +import { parseUrl } from '@ali/b3-one/lib/url'; +const debug = Debug('utils:wsHelper'); + +export default class WSHelper { + constructor(appHelper, namespace, options) { + this.appHelper = appHelper; + this.ws = null; + this.init(namespace, options); + } + + init(namespace = '/', options = {}) { + if (this.ws) { + this.close(); + } + const urlInfo = parseUrl(); + const ws = (this.ws = client(namespace, { + reconnectionDelay: 3000, + transports: ['websocket'], + query: urlInfo.params, + ...options + })); + const appHelper = this.appHelper; + debug('ws.init'); + + ws.on('connect', msg => { + appHelper.emit('wsHelper.connect.success', msg); + debug('ws.connect'); + }); + + ws.on('error', msg => { + appHelper.emit('wsHelper.connect.error', msg); + debug('ws.error', msg); + }); + + ws.on('disconnect', msg => { + appHelper.emit('wsHelper.connect.break', msg); + debug('ws.disconnect', msg); + }); + + ws.on('reconnecting', msg => { + appHelper.emit('wsHelper.connect.retry', msg); + debug('ws.reconnecting', msg); + }); + + ws.on('ping', msg => { + debug('ws.ping', msg); + }); + + ws.on('pong', msg => { + debug('ws.pong', msg); + }); + + ws.on('data', msg => { + appHelper.emit('wsHelper.data.receive', msg); + if (msg.eventName) { + appHelper.emit(`wsHelper.result.${msg.eventName}`, msg); + } + debug('ws.data', msg); + }); + } + + close() { + if (!this.ws) return; + this.ws.close(); + this.ws = null; + this.appHelper.emit('wsHelper.connect.close'); + } + + send(eventName, ...args) { + return new Promise((resolve, reject) => { + try { + this.appHelper.emit('wsHelper.data.request', { + eventName, + params: args + }); + this.appHelper.once(`wsHelper.result.${eventName}`, resolve); + this.ws && this.ws.emit(eventName, ...args); + debug('ws.send', eventName); + } catch (err) { + console.error('websocket error:', err); + reject(err); + } + }); + } +}