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-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 (
+
+ );
+ }
+}
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);
+ }
+ });
+ }
+}