mirror of
https://github.com/alibaba/lowcode-engine.git
synced 2026-03-25 14:53:57 +00:00
Merge branch 'feat/settings-pane' of gitlab.alibaba-inc.com:ali-lowcode/ali-lowcode-engine into feat/editor-framework
This commit is contained in:
commit
290809033a
16
packages/demo/.editorconfig
Normal file
16
packages/demo/.editorconfig
Normal file
@ -0,0 +1,16 @@
|
||||
# EditorConfig is awesome: http://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# Tab indentation
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
8
packages/demo/.eslintignore
Normal file
8
packages/demo/.eslintignore
Normal file
@ -0,0 +1,8 @@
|
||||
.idea/
|
||||
.vscode/
|
||||
.theia/
|
||||
.recore/
|
||||
build/
|
||||
.*
|
||||
~*
|
||||
node_modules
|
||||
3
packages/demo/.eslintrc
Normal file
3
packages/demo/.eslintrc
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "./node_modules/@recore/config/.eslintrc"
|
||||
}
|
||||
40
packages/demo/.gitignore
vendored
Normal file
40
packages/demo/.gitignore
vendored
Normal file
@ -0,0 +1,40 @@
|
||||
node_modules/
|
||||
coverage/
|
||||
build/
|
||||
dist/
|
||||
.idea/
|
||||
.vscode/
|
||||
.theia/
|
||||
.recore/
|
||||
~*
|
||||
package-lock.json
|
||||
|
||||
# Packages #
|
||||
############
|
||||
# it's better to unpack these files and commit the raw source
|
||||
# git has its own built in compression methods
|
||||
*.7z
|
||||
*.dmg
|
||||
*.gz
|
||||
*.iso
|
||||
*.jar
|
||||
*.rar
|
||||
*.tar
|
||||
*.zip
|
||||
|
||||
# Logs and databases #
|
||||
######################
|
||||
*.log
|
||||
*.sql
|
||||
*.sqlite
|
||||
|
||||
# OS generated files #
|
||||
######################
|
||||
.DS_Store
|
||||
.Trash*
|
||||
*.swp
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
6
packages/demo/.prettierrc
Normal file
6
packages/demo/.prettierrc
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"printWidth": 120,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
34
packages/demo/README.md
Normal file
34
packages/demo/README.md
Normal file
@ -0,0 +1,34 @@
|
||||
# demo
|
||||
|
||||
A Recore application demo.
|
||||
|
||||
## Recore 文档
|
||||
|
||||
https://yuque.antfin-inc.com/recore/docs
|
||||
|
||||
## DEEP 物料站点
|
||||
|
||||
https://fusion.alibaba-inc.com/deep/
|
||||
|
||||
## 安装运行
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
tnpm install
|
||||
|
||||
# 启动调试
|
||||
npm start
|
||||
|
||||
# 本地构建(一般来说不需要)
|
||||
npm run build
|
||||
|
||||
# 日常部署:将资源发布到日常 CDN
|
||||
npm run deploy
|
||||
|
||||
# 线上部署:将资源发布到线上 CDN
|
||||
npm run deploy:online
|
||||
```
|
||||
|
||||
## 项目发布
|
||||
|
||||
集团的前端项目发布全部收口到 def 工程研发平台,如果之前没有使用过,请先阅读此文档:https://yuque.antfin-inc.com/xux/docs/rmsztg
|
||||
12
packages/demo/abc.json
Normal file
12
packages/demo/abc.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "demo",
|
||||
"assets": {
|
||||
"type": "command",
|
||||
"command": {
|
||||
"cmd": [
|
||||
"tnpm ii",
|
||||
"tnpm run cloud"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
27
packages/demo/index.html
Normal file
27
packages/demo/index.html
Normal file
@ -0,0 +1,27 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, maximum-scale=1.0, user-scalable=no" />
|
||||
<title>demo</title>
|
||||
<link rel="shortcut icon" type="image/png" href="https://img.alicdn.com/tfs/TB1zgoCemrqK1RjSZK9XXXyypXa-96-96.png" />
|
||||
<script src="https://g.alicdn.com/code/lib/??react/16.9.0/umd/react.production.min.js,react-dom/16.9.0/umd/react-dom.production.min.js,prop-types/15.7.2/prop-types.js"></script>
|
||||
<!-- React 非压缩版代码,可根据需要替换或通过代理替换后方便调试 -->
|
||||
<!-- <script src="https://g.alicdn.com/code/lib/??react/16.9.0/umd/react.development.js,react-dom/16.9.0/umd/react-dom.development.js,prop-types/15.7.2/prop-types.js"></script> -->
|
||||
<script> React.PropTypes = PropTypes; </script>
|
||||
<script src="/node_modules/@ali/recore/umd/recore.js"></script>
|
||||
<!-- Recore 压缩版代码,线上 VM 请使用下面的地址,注意选择自己需要的版本号 -->
|
||||
<!--<script src="https://gw.alipayobjects.com/os/lib/ali/recore/1.5.3/umd/recore.js"></script>-->
|
||||
<!-- 可选是否导入 mockjs, 可增强 mock 能力 -->
|
||||
<script src="https://g.alicdn.com/code/lib/Mock.js/1.0.0/mock-min.js"></script>
|
||||
<link rel="stylesheet" href="https://alifd.alicdn.com/npm/@alifd/next/1.11.6/next.min.css">
|
||||
<script src="https://g.alicdn.com/mylib/moment/2.24.0/min/moment.min.js"></script>
|
||||
<style type="text/css">
|
||||
body {
|
||||
-webkit-overflow-scrolling : touch;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
192782
packages/demo/lowcode-renderer.js
Normal file
192782
packages/demo/lowcode-renderer.js
Normal file
File diff suppressed because one or more lines are too long
55
packages/demo/package.json
Normal file
55
packages/demo/package.json
Normal file
@ -0,0 +1,55 @@
|
||||
{
|
||||
"name": "demo",
|
||||
"version": "1.0.0",
|
||||
"description": "A Recore application demo.",
|
||||
"scripts": {
|
||||
"start": "nowa2 start",
|
||||
"build": "nowa2 build",
|
||||
"cloud": "nowa2 build -o",
|
||||
"precommit": "lint-staged",
|
||||
"test": "jest",
|
||||
"upgrade": "nowa2 upgrade",
|
||||
"deploy": "nowa2 deploy",
|
||||
"deploy:online": "nowa2 deploy -o"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.9.0",
|
||||
"npm": ">=6.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ali/api-loader": "^0.7.8",
|
||||
"@ali/api-runtime": "^0.1.4",
|
||||
"@ali/deep": "1.17.2",
|
||||
"@ali/recore": "^1.5.0",
|
||||
"@alifd/layout": "^0.1.19",
|
||||
"@alife/theme-254": "^0.6.2",
|
||||
"@antv/data-set": "^0.10",
|
||||
"bizcharts": "^3.2",
|
||||
"react": "^16"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ali/nowa-recore-solution": "^1.7.0",
|
||||
"@ali/recore-loader": "^2.0.0",
|
||||
"@nowa/cli": "^0.6",
|
||||
"@recore/config": "^2.0.0",
|
||||
"@types/jest": "^21",
|
||||
"@types/node": "^7",
|
||||
"@types/react": "^16",
|
||||
"eslint": "^6.5.1",
|
||||
"husky": "^1.1.2",
|
||||
"jest": "^23.4.1",
|
||||
"lint-staged": "^7.1.2",
|
||||
"tslib": "^1.9.3",
|
||||
"typescript": "^3.1.3",
|
||||
"prettier": "^1.18.2"
|
||||
},
|
||||
"lint-staged": {
|
||||
"./src/**/*.{ts,tsx}": [
|
||||
"eslint --fix",
|
||||
"git add"
|
||||
]
|
||||
},
|
||||
"nowa": {
|
||||
"solution": "@ali/nowa-recore-solution"
|
||||
}
|
||||
}
|
||||
20
packages/demo/recore.config.js
Normal file
20
packages/demo/recore.config.js
Normal file
@ -0,0 +1,20 @@
|
||||
module.exports = {
|
||||
'[start]': {
|
||||
writeToDisk: true,
|
||||
},
|
||||
'[build]': {},
|
||||
deep: {
|
||||
themeConfig: {},
|
||||
},
|
||||
externals: {
|
||||
'@ali/iceluna-sdk': 'var window.LowCodeRenderer',
|
||||
'@recore/obx': 'var window.Recore',
|
||||
'@recore/core-obx': 'var window.Recore',
|
||||
// '@alifd/next': 'var window.Next',
|
||||
'moment': 'var window.moment',
|
||||
},
|
||||
extraEntry: {
|
||||
'simulator-renderer': '../designer/src/builtins/simulator/renderer/index.ts',
|
||||
},
|
||||
vendors: false,
|
||||
};
|
||||
12
packages/demo/src/app.less
Normal file
12
packages/demo/src/app.less
Normal file
@ -0,0 +1,12 @@
|
||||
.editor {
|
||||
display: flex;
|
||||
}
|
||||
.lc-designer {
|
||||
height: 400px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.lc-settings-main {
|
||||
width: 300px;
|
||||
border-left: 1px solid rgba(31,56,88,.1);
|
||||
}
|
||||
54
packages/demo/src/app.ts
Normal file
54
packages/demo/src/app.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { ViewController } from '@ali/recore';
|
||||
import DesignView from '../../designer/src';
|
||||
import NumberSetter from '../../plugin-setters/number-setter';
|
||||
import SettingsPane, { registerSetter } from '../../plugin-settings-pane/src';
|
||||
import { EventEmitter } from 'events';
|
||||
import { Input } from '@alifd/next';
|
||||
import { createElement } from 'react';
|
||||
|
||||
registerSetter('ClassNameSetter', () => {
|
||||
return createElement('div', {
|
||||
className: 'lc-block-setter'
|
||||
}, '这里是类名绑定');
|
||||
});
|
||||
|
||||
registerSetter('EventsSetter', () => {
|
||||
return createElement('div', {
|
||||
className: 'lc-block-setter'
|
||||
}, '这里是事件设置');
|
||||
});
|
||||
|
||||
registerSetter('StringSetter', { component: Input, props: { placeholder: "请输入" } });
|
||||
|
||||
registerSetter('NumberSetter', NumberSetter as any);
|
||||
|
||||
const emitter = new EventEmitter();
|
||||
|
||||
// 应用入口视图,导航和所有页面复用的 UI 在这里处理
|
||||
export default class App extends ViewController {
|
||||
static components = {
|
||||
DesignView,
|
||||
SettingsPane
|
||||
};
|
||||
|
||||
editor = emitter;
|
||||
|
||||
$didMount() {
|
||||
const designer = this.$refs.d.designer;
|
||||
const pane = this.$refs.pane;
|
||||
(window as any).LCDesigner = designer;
|
||||
(this.editor as any).designer = designer;
|
||||
designer.dragon.from(pane, () => {
|
||||
return {
|
||||
type: 'nodedata',
|
||||
data: {
|
||||
componentName: 'Button',
|
||||
props: {
|
||||
type: 'primary',
|
||||
},
|
||||
children: 'awefawef'
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
663
packages/demo/src/app.vx
Normal file
663
packages/demo/src/app.vx
Normal file
@ -0,0 +1,663 @@
|
||||
<div className="editor">
|
||||
<DesignView
|
||||
eventPipe={editor}
|
||||
defaultSchema={{
|
||||
componentsTree: [
|
||||
{
|
||||
componentName: 'Page',
|
||||
fileName: 'form',
|
||||
props: { style: { paddingTop: 20, paddingRight: 20, paddingLeft: 20 } },
|
||||
children: [
|
||||
{
|
||||
componentName: 'Div',
|
||||
props: { style: { height: '255px' } },
|
||||
children: [
|
||||
{
|
||||
componentName: 'Text',
|
||||
props: { text: '内容筛选', style: { fontWeight: 'bold', fontSize: '16px' } },
|
||||
},
|
||||
{
|
||||
componentName: 'Form',
|
||||
props: {
|
||||
style: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
marginTop: 10,
|
||||
paddingLeft: 10,
|
||||
textAlign: 'center',
|
||||
float: 'left',
|
||||
minWidth: '900px',
|
||||
},
|
||||
labelTextAlign: 'right',
|
||||
wrapperCol: 12,
|
||||
labelAlign: 'left',
|
||||
inline: false,
|
||||
labelCol: 6,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
componentName: 'FormItem',
|
||||
props: {
|
||||
label: {
|
||||
type: 'JSSlot',
|
||||
value: {
|
||||
componentName: 'Fragment',
|
||||
children: '所属应用:'
|
||||
}
|
||||
},
|
||||
// '所属应用:',
|
||||
name: 'appApply',
|
||||
initValue: '22',
|
||||
style: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'nowrap',
|
||||
width: '33.33%',
|
||||
height: '40px',
|
||||
verticalAlign: 'middle',
|
||||
float: 'left',
|
||||
},
|
||||
labelTextAlign: 'right',
|
||||
asterisk: false,
|
||||
labelAlign: 'left',
|
||||
size: 'medium',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
componentName: 'Input',
|
||||
props: { placeholder: '请输入', size: 'medium', style: { width: '200px' } },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
componentName: 'FormItem',
|
||||
props: {
|
||||
label: '分类ID:',
|
||||
name: 'typeId',
|
||||
initValue: '22',
|
||||
style: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'nowrap',
|
||||
width: '33.33%',
|
||||
height: '40px',
|
||||
verticalAlign: 'middle',
|
||||
float: 'left',
|
||||
},
|
||||
},
|
||||
children: [
|
||||
{
|
||||
componentName: 'NumberPicker',
|
||||
props: { size: 'medium', type: 'normal', style: { width: '200px' } },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
componentName: 'FormItem',
|
||||
props: {
|
||||
label: '标签ID:',
|
||||
name: 'tagId',
|
||||
initValue: '22',
|
||||
style: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'nowrap',
|
||||
width: '33.33%',
|
||||
height: '40px',
|
||||
verticalAlign: 'middle',
|
||||
float: 'left',
|
||||
},
|
||||
},
|
||||
children: [
|
||||
{
|
||||
componentName: 'NumberPicker',
|
||||
props: { size: 'medium', type: 'normal', style: { width: '200px' } },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
componentName: 'FormItem',
|
||||
props: {
|
||||
label: '开始时间:',
|
||||
name: 'startTime',
|
||||
style: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'nowrap',
|
||||
width: '33.33%',
|
||||
height: '40px',
|
||||
verticalAlign: 'middle',
|
||||
float: 'left',
|
||||
},
|
||||
},
|
||||
children: [{ componentName: 'TimePicker', props: {} }],
|
||||
},
|
||||
{
|
||||
componentName: 'FormItem',
|
||||
props: {
|
||||
label: '结束时间:',
|
||||
name: 'workendTIme',
|
||||
style: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'nowrap',
|
||||
width: '33.33%',
|
||||
height: '40px',
|
||||
verticalAlign: 'middle',
|
||||
float: 'left',
|
||||
},
|
||||
},
|
||||
children: [{ componentName: 'TimePicker', props: {} }],
|
||||
},
|
||||
{
|
||||
componentName: 'FormItem',
|
||||
props: {
|
||||
label: '尺寸:',
|
||||
name: 'size',
|
||||
style: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'nowrap',
|
||||
width: '33.33%',
|
||||
height: '40px',
|
||||
verticalAlign: 'middle',
|
||||
float: 'left',
|
||||
},
|
||||
labelTextAlign: 'right',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
componentName: 'Select',
|
||||
props: {
|
||||
dataSource: [
|
||||
{ label: '教师', value: 't' },
|
||||
{ label: '医生', value: 'd' },
|
||||
{ label: '歌手', value: 's' },
|
||||
],
|
||||
style: { width: '200px' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
componentName: 'FormItem',
|
||||
props: {
|
||||
label: '删除状态:',
|
||||
name: 'isRemoved',
|
||||
style: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'nowrap',
|
||||
width: '33.33%',
|
||||
height: '40px',
|
||||
verticalAlign: 'middle',
|
||||
float: 'left',
|
||||
},
|
||||
},
|
||||
children: [
|
||||
{
|
||||
componentName: 'Select',
|
||||
props: {
|
||||
dataSource: [
|
||||
{ label: '教师', value: 't' },
|
||||
{ label: '医生', value: 'd' },
|
||||
{ label: '歌手', value: 's' },
|
||||
],
|
||||
style: { width: '200px' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
componentName: 'FormItem',
|
||||
props: {
|
||||
label: '讨论ID:',
|
||||
name: 'talkId',
|
||||
initValue: '22',
|
||||
style: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'nowrap',
|
||||
width: '33.33%',
|
||||
height: '40px',
|
||||
verticalAlign: 'middle',
|
||||
float: 'left',
|
||||
},
|
||||
labelTextAlign: 'right',
|
||||
asterisk: false,
|
||||
labelAlign: 'left',
|
||||
size: 'medium',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
componentName: 'Input',
|
||||
props: { placeholder: '请输入', size: 'medium', style: { width: '200px' } },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
componentName: 'FormItem',
|
||||
props: {
|
||||
label: '置顶:',
|
||||
name: 'isTop',
|
||||
style: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'nowrap',
|
||||
width: '33.33%',
|
||||
height: '40px',
|
||||
verticalAlign: 'middle',
|
||||
float: 'left',
|
||||
},
|
||||
},
|
||||
children: [
|
||||
{
|
||||
componentName: 'Select',
|
||||
props: {
|
||||
dataSource: [
|
||||
{ label: '教师', value: 't' },
|
||||
{ label: '医生', value: 'd' },
|
||||
{ label: '歌手', value: 's' },
|
||||
],
|
||||
style: { width: '200px' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
componentName: 'Div',
|
||||
props: { style: { display: 'block', width: '100%', textAlign: 'left' } },
|
||||
children: [
|
||||
{
|
||||
componentName: 'ButtonGroup',
|
||||
props: {},
|
||||
children: [
|
||||
{
|
||||
componentName: 'Button',
|
||||
props: { type: 'normal', style: { margin: '0 5px 0 5px' }, htmlType: 'reset' },
|
||||
children: '重置',
|
||||
},
|
||||
{
|
||||
componentName: 'Button',
|
||||
props: { type: 'primary', style: { margin: '0 5px 0 5px' }, htmlType: 'submit' },
|
||||
children: '确定',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
dataSource: {
|
||||
dataHandler: function dataHandler(dataMap) {
|
||||
let dataSource = [
|
||||
{
|
||||
id: 1,
|
||||
title: '2017秋冬新款背带裙复古格子连衣裙清新背心裙a字裙短裙子',
|
||||
url: 'https://item.taobao.com/item.htm?id=558559528377',
|
||||
type: '清单',
|
||||
publishTime: '17-04-28 20:29:20',
|
||||
publishStatus: '已发布',
|
||||
pushStatus: '已推送至订阅号',
|
||||
operation: { edit: true },
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '2017秋冬新款 高质感特定纱线欧美宽松马海毛羊毛毛衣',
|
||||
url: 'https://item.taobao.com/item.htm?id=558559528377',
|
||||
type: '清单',
|
||||
publishTime: '17-04-28 20:29:20',
|
||||
publishStatus: '已发布',
|
||||
pushStatus: '已推送至订阅号',
|
||||
operation: { edit: true },
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '日式天然玉米皮草编碗垫锅垫隔热垫茶垫加厚餐垫GD-29',
|
||||
url: 'https://item.taobao.com/item.htm?id=558559528377',
|
||||
type: '清单',
|
||||
publishTime: '17-04-28 20:29:20',
|
||||
publishStatus: '已发布',
|
||||
pushStatus: '已推送至订阅号',
|
||||
operation: { edit: true },
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: '2017秋冬新款 绑带腰封设计感超顺滑质感落肩铜氨丝连衣裙',
|
||||
url: 'https://item.taobao.com/item.htm?id=558559528377',
|
||||
type: '清单',
|
||||
publishTime: '17-04-28 20:29:20',
|
||||
publishStatus: '已发布',
|
||||
pushStatus: '已推送至订阅号',
|
||||
operation: { edit: true },
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: '日式糖果色陶瓷柄不锈钢餐具西餐牛扒刀叉勺子咖啡勺',
|
||||
url: 'https://item.taobao.com/item.htm?id=558559528377',
|
||||
type: '清单',
|
||||
publishTime: '17-04-28 20:29:20',
|
||||
publishStatus: '已发布',
|
||||
pushStatus: '已推送至订阅号',
|
||||
operation: { edit: true },
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
title: '日式和风深蓝素色文艺餐巾餐垫围裙锅垫隔热手套厨房桌布',
|
||||
url: 'https://item.taobao.com/item.htm?id=558559528377',
|
||||
type: '清单',
|
||||
publishTime: '17-04-28 20:29:20',
|
||||
publishStatus: '已发布',
|
||||
pushStatus: '已推送至订阅号',
|
||||
operation: { edit: true },
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
title: '日式雪点樱花手绘陶瓷餐具米饭碗拉面碗寿司盘子汤碗',
|
||||
url: 'https://item.taobao.com/item.htm?id=558559528377',
|
||||
type: '清单',
|
||||
publishTime: '17-04-28 20:29:20',
|
||||
publishStatus: '已发布',
|
||||
pushStatus: '已推送至订阅号',
|
||||
operation: { edit: true },
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
title: '川岛屋 釉下彩复古日式陶瓷盘子菜盘圆盘调味碟 米饭碗日式餐具',
|
||||
url: 'https://item.taobao.com/item.htm?id=558559528377',
|
||||
type: '清单',
|
||||
publishTime: '17-04-28 20:29:20',
|
||||
publishStatus: '已发布',
|
||||
pushStatus: '已推送至订阅号',
|
||||
operation: { edit: true },
|
||||
},
|
||||
];
|
||||
return { ...dataMap, dataSource };
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
componentName: 'Page',
|
||||
fileName: 'list',
|
||||
props: { style: { paddingTop: 20, paddingRight: 20, paddingLeft: 20 } },
|
||||
children: [
|
||||
{
|
||||
componentName: 'Div',
|
||||
props: { style: { height: '90px', lineHeight: '30px' } },
|
||||
children: [
|
||||
{
|
||||
componentName: 'Div',
|
||||
props: { style: { float: 'left' } },
|
||||
children: [
|
||||
{ componentName: 'Text', props: { style: { paddingLeft: 5, cursor: 'pointer' }, text: '最热' } },
|
||||
{
|
||||
componentName: 'Icon',
|
||||
props: {
|
||||
size: 'small',
|
||||
style: { paddingRight: 5, paddingLeft: 5, fontSize: 14, color: '#4a90e2', cursor: 'pointer' },
|
||||
type: 'sorting',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
componentName: 'Div',
|
||||
props: { style: { float: 'left' } },
|
||||
children: [
|
||||
{ componentName: 'Text', props: { text: '最新', style: { paddingLeft: 10, cursor: 'pointer' } } },
|
||||
{
|
||||
componentName: 'Icon',
|
||||
props: {
|
||||
size: 'small',
|
||||
style: {
|
||||
paddingRight: 5,
|
||||
paddingLeft: 5,
|
||||
fontSize: '14px',
|
||||
color: '#9b9b9b',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
type: 'sorting',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
componentName: 'Div',
|
||||
props: { style: { float: 'left' } },
|
||||
children: [
|
||||
{
|
||||
componentName: 'Text',
|
||||
props: { text: '距离接稿日期最近', style: { paddingLeft: 10, cursor: 'pointer' } },
|
||||
},
|
||||
{
|
||||
componentName: 'Icon',
|
||||
props: {
|
||||
size: 'small',
|
||||
style: { fontSize: '14px', color: '#9b9b9b', cursor: 'pointer' },
|
||||
type: 'sorting',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
componentName: 'Div',
|
||||
props: { style: { marginTop: 5, marginBottom: 15, borderBottom: '1px solid rgba(244,244,244)' } },
|
||||
children: [
|
||||
{
|
||||
componentName: 'Div',
|
||||
props: { style: { marginBottom: 15 } },
|
||||
children: [
|
||||
{
|
||||
componentName: 'Text',
|
||||
props: { text: '{{this.item.title}}', style: { color: 'rgba(51,51,51)' } },
|
||||
},
|
||||
{
|
||||
componentName: 'Text',
|
||||
props: {
|
||||
text: '{{this.item.datetime}}',
|
||||
style: { fontSize: '12px', color: '#666', float: 'right' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
componentName: 'Div',
|
||||
props: { style: { paddingBottom: 15, fontSize: '13px', color: '#666' } },
|
||||
children: '{{this.item.description}}',
|
||||
},
|
||||
{
|
||||
componentName: 'Div',
|
||||
props: { style: { marginBottom: 15 } },
|
||||
children: [
|
||||
{
|
||||
componentName: 'Button',
|
||||
props: { type: 'normal', style: { marginRight: 5, marginLeft: 5 }, size: 'small' },
|
||||
children: '{{this.item}}',
|
||||
loop: '{{this.item.tags}}',
|
||||
},
|
||||
{
|
||||
componentName: 'Div',
|
||||
props: { style: { marginBottom: 15, float: 'right' } },
|
||||
children: [
|
||||
{
|
||||
componentName: 'Div',
|
||||
props: {
|
||||
style: {
|
||||
display: 'inline-block',
|
||||
marginRight: 5,
|
||||
marginBottom: 15,
|
||||
marginLeft: 5,
|
||||
fontSize: 12,
|
||||
color: '#666',
|
||||
float: 'none',
|
||||
},
|
||||
},
|
||||
children: '{{"点赞:"+this.item.star}}',
|
||||
},
|
||||
{
|
||||
componentName: 'Div',
|
||||
props: {
|
||||
style: {
|
||||
display: 'inline-block',
|
||||
marginRight: 5,
|
||||
marginBottom: 15,
|
||||
marginLeft: 5,
|
||||
fontSize: 12,
|
||||
color: '#666',
|
||||
float: 'none',
|
||||
},
|
||||
},
|
||||
children: '{{"喜爱:"+this.item.like}}',
|
||||
},
|
||||
{
|
||||
componentName: 'Div',
|
||||
props: {
|
||||
style: {
|
||||
display: 'inline-block',
|
||||
marginRight: 5,
|
||||
marginBottom: 15,
|
||||
marginLeft: 5,
|
||||
fontSize: 12,
|
||||
color: '#66',
|
||||
float: 'none',
|
||||
},
|
||||
},
|
||||
children: '{{"评论:"+this.item.comment}}',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
loop: '{{this.state.dataSource}}',
|
||||
},
|
||||
{
|
||||
componentName: 'Pagination',
|
||||
props: {
|
||||
shape: 'normal',
|
||||
type: 'normal',
|
||||
size: 'medium',
|
||||
style: { marginTop: 10, marginBottom: 30, textAlign: 'right' },
|
||||
},
|
||||
},
|
||||
],
|
||||
dataSource: {
|
||||
dataHandler: function dataHandler(dataMap) {
|
||||
const dataSource = [
|
||||
{
|
||||
title: '越夏越嗨皮-7月官方营销活动-技能提升方向',
|
||||
description:
|
||||
'商家通过V任务选择主播并达成合作,费用按照商品链接计算,一个商品为一个价格,建议主播在一场直播里最多接60个商品,并提供不少于两个小时的直播服务,每个商品讲解时间不少于5分钟。 ',
|
||||
tags: ['直播', '大促', '简介'],
|
||||
datetime: '2017年12月12日 18:00',
|
||||
star: Math.floor(Math.random() * 100) + 100,
|
||||
like: Math.floor(Math.random() * 100) + 200,
|
||||
comment: Math.floor(Math.random() * 100) + 100,
|
||||
},
|
||||
{
|
||||
title: '越夏越嗨皮-7月官方营销活动-技能提升方向',
|
||||
description:
|
||||
'商家通过V任务选择主播并达成合作,费用按照商品链接计算,一个商品为一个价格,建议主播在一场直播里最多接60个商品,并提供不少于两个小时的直播服务,每个商品讲解时间不少于5分钟。 ',
|
||||
tags: ['直播', '大促', '简介'],
|
||||
datetime: '2017年12月12日 18:00',
|
||||
star: Math.floor(Math.random() * 100) + 100,
|
||||
like: Math.floor(Math.random() * 100) + 200,
|
||||
comment: Math.floor(Math.random() * 100) + 100,
|
||||
},
|
||||
{
|
||||
title: '越夏越嗨皮-7月官方营销活动-技能提升方向',
|
||||
description:
|
||||
'商家通过V任务选择主播并达成合作,费用按照商品链接计算,一个商品为一个价格,建议主播在一场直播里最多接60个商品,并提供不少于两个小时的直播服务,每个商品讲解时间不少于5分钟。 ',
|
||||
tags: ['直播', '大促', '简介'],
|
||||
datetime: '2017年12月12日 18:00',
|
||||
star: Math.floor(Math.random() * 100) + 100,
|
||||
like: Math.floor(Math.random() * 100) + 200,
|
||||
comment: Math.floor(Math.random() * 100) + 100,
|
||||
},
|
||||
{
|
||||
title: '越夏越嗨皮-7月官方营销活动-技能提升方向',
|
||||
description:
|
||||
'商家通过V任务选择主播并达成合作,费用按照商品链接计算,一个商品为一个价格,建议主播在一场直播里最多接60个商品,并提供不少于两个小时的直播服务,每个商品讲解时间不少于5分钟。 ',
|
||||
tags: ['直播', '大促', '简介'],
|
||||
datetime: '2017年12月12日 18:00',
|
||||
star: Math.floor(Math.random() * 100) + 100,
|
||||
like: Math.floor(Math.random() * 100) + 200,
|
||||
comment: Math.floor(Math.random() * 100) + 100,
|
||||
},
|
||||
{
|
||||
title: '越夏越嗨皮-7月官方营销活动-技能提升方向',
|
||||
description:
|
||||
'商家通过V任务选择主播并达成合作,费用按照商品链接计算,一个商品为一个价格,建议主播在一场直播里最多接60个商品,并提供不少于两个小时的直播服务,每个商品讲解时间不少于5分钟。 ',
|
||||
tags: ['直播', '大促', '简介'],
|
||||
datetime: '2017年12月12日 18:00',
|
||||
star: Math.floor(Math.random() * 100) + 100,
|
||||
like: Math.floor(Math.random() * 100) + 200,
|
||||
comment: Math.floor(Math.random() * 100) + 100,
|
||||
},
|
||||
];
|
||||
return { dataSource };
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}}
|
||||
simulatorProps={{
|
||||
device: 'legao',
|
||||
componentsAsset: [
|
||||
{
|
||||
type: 'jsUrl',
|
||||
content: 'https://unpkg.alibaba-inc.com/@alifd/next@1.18.17/dist/next.min.js',
|
||||
id: 'next',
|
||||
level: 2,
|
||||
},
|
||||
{
|
||||
type: 'cssUrl',
|
||||
content: 'https://unpkg.alibaba-inc.com/@alifd/next@1.18.17/dist/next.min.css',
|
||||
id: 'next',
|
||||
level: 2,
|
||||
},
|
||||
],
|
||||
}}
|
||||
componentsDescription={[
|
||||
{
|
||||
description: 'Button',
|
||||
npm: { package: '@ali/deep', subName: 'Button', destructuring: false, exportName: 'Next', version: '1.17.2' },
|
||||
componentName: 'Button',
|
||||
title: 'Button',
|
||||
},
|
||||
]}
|
||||
ref="d"
|
||||
/>
|
||||
|
||||
<SettingsPane editor={editor} />
|
||||
|
||||
</div>
|
||||
<div ref="pane">
|
||||
<a href="afeawef">aewfawfe</a>
|
||||
</div>
|
||||
19
packages/demo/src/module.d.ts
vendored
Normal file
19
packages/demo/src/module.d.ts
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
/// <reference types="react" />
|
||||
// tslint:disable
|
||||
|
||||
declare const __mock__: boolean;
|
||||
|
||||
declare module '*.vx' {
|
||||
const RecoreComponent: React.ComponentClass<{ [key: string]: any }>;
|
||||
export default RecoreComponent;
|
||||
}
|
||||
|
||||
declare module '*.svg' {
|
||||
const SvgIcon: React.ComponentClass<any>;
|
||||
export default SvgIcon;
|
||||
}
|
||||
|
||||
declare module '@alifd/layout';
|
||||
declare module '@antv/data-set';
|
||||
declare module '@ali/iceluna-sdk';
|
||||
|
||||
10
packages/demo/tsconfig.json
Normal file
10
packages/demo/tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "./node_modules/@recore/config/tsconfig",
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true
|
||||
},
|
||||
"include": [
|
||||
"./src/",
|
||||
"../src/"
|
||||
]
|
||||
}
|
||||
@ -2,7 +2,7 @@
|
||||
"name": "lowcode-designer",
|
||||
"version": "0.9.0",
|
||||
"description": "alibaba lowcode designer",
|
||||
"main": "index.js",
|
||||
"main": "src/index.ts",
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@ -2,10 +2,8 @@ import { Component } from 'react';
|
||||
import { obx } from '@recore/obx';
|
||||
import { observer } from '@recore/core-obx';
|
||||
import Designer from '../../designer/designer';
|
||||
import './ghost.less';
|
||||
import { NodeSchema } from '../../designer/schema';
|
||||
import Node from '../../designer/document/node/node';
|
||||
import { isDragNodeObject, DragObject, isDragNodeDataObject } from '../../designer/helper/dragon';
|
||||
import './ghost.less';
|
||||
|
||||
type offBinding = () => any;
|
||||
|
||||
|
||||
@ -72,12 +72,10 @@ export class OutlineHovering extends Component {
|
||||
render() {
|
||||
const host = this.context as SimulatorHost;
|
||||
const current = this.current;
|
||||
console.info('current', current)
|
||||
if (!current || host.viewport.scrolling) {
|
||||
return <Fragment />;
|
||||
}
|
||||
const instances = host.getComponentInstances(current);
|
||||
console.info('current instances', instances)
|
||||
if (!instances || instances.length < 1) {
|
||||
return <Fragment />;
|
||||
}
|
||||
|
||||
@ -46,7 +46,11 @@
|
||||
}
|
||||
|
||||
&-device-legao {
|
||||
margin: 15px;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
bottom: 15px;
|
||||
left: 15px;
|
||||
width: auto;
|
||||
box-shadow: 0 2px 10px 0 rgba(31,56,88,.15);
|
||||
}
|
||||
|
||||
|
||||
@ -29,7 +29,7 @@ import {
|
||||
CanvasPoint,
|
||||
} from '../../../designer/helper/location';
|
||||
import { isNodeSchema, NodeSchema } from '../../../designer/schema';
|
||||
import { ComponentDescriptionSpec } from '../../../designer/component-config';
|
||||
import { ComponentDescription } from '../../../designer/component-type';
|
||||
import { ReactInstance } from 'react';
|
||||
import { setNativeSelection } from '../../../designer/helper/navtive-selection';
|
||||
import cursor from '../../../designer/helper/cursor';
|
||||
@ -68,7 +68,7 @@ const defaultDepends = [
|
||||
'window.PropTypes=parent.PropTypes;React.PropTypes=parent.PropTypes; window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = window.parent.__REACT_DEVTOOLS_GLOBAL_HOOK__;',
|
||||
),
|
||||
assetItem(AssetType.JSUrl, 'https://g.alicdn.com/mylib/@ali/recore/1.5.7/umd/recore.min.js'),
|
||||
assetItem(AssetType.JSUrl, 'http://localhost:4444/js/index.js'),
|
||||
assetItem(AssetType.JSUrl, '/lowcode-renderer.js'),
|
||||
];
|
||||
|
||||
export class SimulatorHost implements ISimulator<SimulatorProps> {
|
||||
@ -290,8 +290,6 @@ export class SimulatorHost implements ISimulator<SimulatorProps> {
|
||||
return;
|
||||
}
|
||||
const nodeInst = this.getNodeInstanceFromElement(e.target as Element);
|
||||
// TODO: enhance only hover one instance
|
||||
console.info(nodeInst);
|
||||
hovering.hover(nodeInst?.node || null);
|
||||
e.stopPropagation();
|
||||
};
|
||||
@ -337,7 +335,7 @@ export class SimulatorHost implements ISimulator<SimulatorProps> {
|
||||
/**
|
||||
* @see ISimulator
|
||||
*/
|
||||
describeComponent(component: Component): ComponentDescriptionSpec {
|
||||
describeComponent(component: Component): ComponentDescription {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
@ -633,7 +631,6 @@ export class SimulatorHost implements ISimulator<SimulatorProps> {
|
||||
this.sensing = true;
|
||||
this.scroller.scrolling(e);
|
||||
const dropTarget = this.getDropTarget(e);
|
||||
console.info('aa', dropTarget);
|
||||
if (!dropTarget) {
|
||||
return null;
|
||||
}
|
||||
@ -820,7 +817,6 @@ export class SimulatorHost implements ISimulator<SimulatorProps> {
|
||||
}
|
||||
}*/
|
||||
else if (isNode(res)) {
|
||||
console.info('res', res);
|
||||
container = res;
|
||||
upward = null;
|
||||
}
|
||||
@ -834,7 +830,7 @@ export class SimulatorHost implements ISimulator<SimulatorProps> {
|
||||
return this.checkDropTarget(container, dragObject as any);
|
||||
}
|
||||
|
||||
const config = container.componentConfig;
|
||||
const config = container.componentType;
|
||||
|
||||
if (!config.isContainer) {
|
||||
return false;
|
||||
@ -919,7 +915,7 @@ export class SimulatorHost implements ISimulator<SimulatorProps> {
|
||||
|
||||
checkNestingUp(parent: NodeParent, target: NodeSchema | Node): boolean {
|
||||
if (isNode(target) || isNodeSchema(target)) {
|
||||
const config = isNode(target) ? target.componentConfig : this.designer.getComponentConfig(target.componentName);
|
||||
const config = isNode(target) ? target.componentType : this.designer.getComponentType(target.componentName);
|
||||
if (config) {
|
||||
return config.checkNestingUp(target, parent);
|
||||
}
|
||||
@ -929,7 +925,7 @@ export class SimulatorHost implements ISimulator<SimulatorProps> {
|
||||
}
|
||||
|
||||
checkNestingDown(parent: NodeParent, target: NodeSchema | Node): boolean {
|
||||
const config = parent.componentConfig;
|
||||
const config = parent.componentType;
|
||||
return config.checkNestingDown(parent, target) && this.checkNestingUp(parent, target);
|
||||
}
|
||||
// #endregion
|
||||
|
||||
@ -7,9 +7,9 @@ import { RootSchema, NpmInfo } from '../../../designer/schema';
|
||||
import { getClientRects } from '../../../utils/get-client-rects';
|
||||
import { Asset } from '../utils/asset';
|
||||
import loader from '../utils/loader';
|
||||
import { ComponentDescriptionSpec } from '../../../designer/component-config';
|
||||
import { ComponentDescription } from '../../../designer/component-type';
|
||||
import { reactFindDOMNodes, FIBER_KEY } from '../utils/react-find-dom-nodes';
|
||||
import { isESModule } from '../../../utils/is-es-module';
|
||||
import { isESModule } from '../../../../../utils/is-es-module';
|
||||
import { NodeInstance } from '../../../designer/simulator';
|
||||
import { isElement } from '../../../utils/is-element';
|
||||
import cursor from '../../../designer/helper/cursor';
|
||||
@ -281,7 +281,7 @@ function findComponent(componentName: string, npm?: NpmInfo) {
|
||||
return getSubComponent(library, paths);
|
||||
}
|
||||
|
||||
function buildComponents(componentsMap: { [componentName: string]: ComponentDescriptionSpec }) {
|
||||
function buildComponents(componentsMap: { [componentName: string]: ComponentDescription }) {
|
||||
const components: any = {};
|
||||
Object.keys(componentsMap).forEach(componentName => {
|
||||
components[componentName] = findComponent(componentName, componentsMap[componentName].npm);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { ReactNode, ReactElement, ComponentType } from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
import Node, { NodeParent } from './document/node/node';
|
||||
import { NodeData, NodeSchema } from './schema';
|
||||
|
||||
@ -17,166 +17,13 @@ export interface PropConfig {
|
||||
defaultValue?: any;
|
||||
}
|
||||
|
||||
export type CustomView = ReactElement | ComponentType<any>;
|
||||
|
||||
export interface TipConfig {
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
theme?: string;
|
||||
direction?: string; // 'n|s|w|e|top|bottom|left|right';
|
||||
}
|
||||
|
||||
export interface IconConfig {
|
||||
name: string;
|
||||
size?: string;
|
||||
className?: string;
|
||||
effect?: string;
|
||||
}
|
||||
|
||||
export interface TitleConfig {
|
||||
label?: ReactNode;
|
||||
tip?: string | ReactElement | TipConfig;
|
||||
icon?: string | ReactElement | IconConfig;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export type Title = string | ReactElement | TitleConfig;
|
||||
|
||||
export enum DisplayType {
|
||||
Inline = 'inline',
|
||||
Block = 'block',
|
||||
Accordion = 'Accordion',
|
||||
Plain = 'plain',
|
||||
Caption = 'caption',
|
||||
}
|
||||
|
||||
export interface SetterConfig {
|
||||
/**
|
||||
* if *string* passed must be a registered Setter Name
|
||||
*/
|
||||
componentName: string | CustomView;
|
||||
/**
|
||||
* the props pass to Setter Component
|
||||
*/
|
||||
props?: {
|
||||
[prop: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* if *string* passed must be a registered Setter Name
|
||||
*/
|
||||
export type SetterType = SetterConfig | string | CustomView;
|
||||
|
||||
export interface SettingFieldConfig {
|
||||
/**
|
||||
* the name of this setting field, which used in quickEditor
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* the field body contains
|
||||
*/
|
||||
setter: SetterType;
|
||||
/**
|
||||
* the prop target which to set, eg. "style.width"
|
||||
* @default sameas .name
|
||||
*/
|
||||
propTarget?: string;
|
||||
/**
|
||||
* the field title
|
||||
* @default sameas .propTarget
|
||||
*/
|
||||
title?: Title;
|
||||
extraProps?: {
|
||||
/**
|
||||
* default value of target prop for setter use
|
||||
*/
|
||||
defaultValue?: any;
|
||||
onChange?: (value: any) => void;
|
||||
getValue?: () => any;
|
||||
/**
|
||||
* the field conditional show, is not set always true
|
||||
* @default undefined
|
||||
*/
|
||||
condition?: (node: Node) => boolean;
|
||||
/**
|
||||
* quick add "required" validation
|
||||
*/
|
||||
required?: boolean;
|
||||
/**
|
||||
* the field display
|
||||
* @default DisplayType.Block
|
||||
*/
|
||||
display?: DisplayType.Inline | DisplayType.Block | DisplayType.Accordion | DisplayType.Plain;
|
||||
/**
|
||||
* default collapsed when display accordion
|
||||
*/
|
||||
defaultCollapsed?: boolean;
|
||||
/**
|
||||
* layout control
|
||||
* number or [column number, left offset]
|
||||
* @default 6
|
||||
*/
|
||||
span?: number | [number, number];
|
||||
};
|
||||
}
|
||||
|
||||
export interface SettingGroupConfig {
|
||||
/**
|
||||
* the type "group"
|
||||
*/
|
||||
type: 'group';
|
||||
/**
|
||||
* the name of this setting group, which used in quickEditor
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* the setting items which group body contains
|
||||
*/
|
||||
items: Array<SettingFieldConfig | SettingGroupConfig | CustomView>;
|
||||
/**
|
||||
* the group title
|
||||
* @default sameas .name
|
||||
*/
|
||||
title?: Title;
|
||||
extraProps: {
|
||||
/**
|
||||
* the field conditional show, is not set always true
|
||||
* @default undefined
|
||||
*/
|
||||
condition?: (node: Node) => boolean;
|
||||
/**
|
||||
* the group display
|
||||
* @default DisplayType.Block
|
||||
*/
|
||||
display?: DisplayType.Block | DisplayType.Accordion;
|
||||
/**
|
||||
* default collapsed when display accordion
|
||||
*/
|
||||
defaultCollapsed?: boolean;
|
||||
/**
|
||||
* the gap between span
|
||||
* @default 0 px
|
||||
*/
|
||||
gap?: number;
|
||||
/**
|
||||
* layout control
|
||||
* number or [column number, left offset]
|
||||
* @default 6
|
||||
*/
|
||||
span?: number | [number, number];
|
||||
};
|
||||
}
|
||||
|
||||
export type PropSettingConfig = SettingFieldConfig | SettingGroupConfig | CustomView;
|
||||
|
||||
export interface NestingRule {
|
||||
childWhitelist?: string[];
|
||||
parentWhitelist?: string[];
|
||||
}
|
||||
|
||||
export interface Configure {
|
||||
props?: PropSettingConfig[];
|
||||
props?: any[];
|
||||
styles?: object;
|
||||
events?: object;
|
||||
component?: {
|
||||
@ -187,7 +34,7 @@ export interface Configure {
|
||||
};
|
||||
}
|
||||
|
||||
export interface ComponentDescriptionSpec {
|
||||
export interface ComponentDescription {
|
||||
componentName: string;
|
||||
/**
|
||||
* unique id
|
||||
@ -215,7 +62,7 @@ export interface ComponentDescriptionSpec {
|
||||
version: string;
|
||||
};
|
||||
props?: PropConfig[];
|
||||
configure?: PropSettingConfig[] | Configure;
|
||||
configure?: any[] | Configure;
|
||||
}
|
||||
|
||||
function ensureAList(list?: string | string[]): string[] | null {
|
||||
@ -268,8 +115,8 @@ function generatePropsConfigure(props: PropConfig[]) {
|
||||
return [];
|
||||
}
|
||||
|
||||
export class ComponentConfig {
|
||||
readonly isComponentConfig = true;
|
||||
export class ComponentType {
|
||||
readonly isComponentType = true;
|
||||
private _uri?: string;
|
||||
get uri(): string {
|
||||
return this._uri!;
|
||||
@ -280,7 +127,7 @@ export class ComponentConfig {
|
||||
}
|
||||
private _isContainer?: boolean;
|
||||
get isContainer(): boolean {
|
||||
return this._isContainer! || this.isRootComponent();
|
||||
return true; // this._isContainer! || this.isRootComponent();
|
||||
}
|
||||
private _isModal?: boolean;
|
||||
get isModal(): boolean {
|
||||
@ -295,30 +142,82 @@ export class ComponentConfig {
|
||||
return this._acceptable!;
|
||||
}
|
||||
private _configure?: Configure;
|
||||
get configure(): Configure {
|
||||
return this._configure!;
|
||||
get configure() {
|
||||
return [{
|
||||
name: '#props',
|
||||
title: "属性",
|
||||
items: [{
|
||||
name: 'label',
|
||||
title: '标签',
|
||||
setter: 'StringSetter'
|
||||
}, {
|
||||
name: 'name',
|
||||
title: '名称',
|
||||
setter: {
|
||||
componentName: 'ArraySetter',
|
||||
props: {
|
||||
itemConfig: {
|
||||
setter: 'StringSetter',
|
||||
defaultValue: ''
|
||||
}
|
||||
}
|
||||
}
|
||||
}, {
|
||||
name: 'size',
|
||||
title: '大小',
|
||||
setter: 'StringSetter'
|
||||
}, {
|
||||
name: 'age',
|
||||
title: '年龄',
|
||||
setter: 'NumberSetter'
|
||||
}]
|
||||
}, {
|
||||
name: '#styles',
|
||||
title: "样式",
|
||||
items: [{
|
||||
name: 'className',
|
||||
title: '类名绑定',
|
||||
setter: 'ClassNameSetter'
|
||||
}, {
|
||||
name: 'className2',
|
||||
title: '类名绑定',
|
||||
setter: 'StringSetter'
|
||||
}, {
|
||||
name: '#inlineStyles',
|
||||
title: '行内样式',
|
||||
items: []
|
||||
}]
|
||||
}, {
|
||||
name: '#events',
|
||||
title: "事件",
|
||||
items: [{
|
||||
name: '!events',
|
||||
title: '事件绑定',
|
||||
setter: 'EventsSetter'
|
||||
}]
|
||||
}, {
|
||||
name: '#data',
|
||||
title: "数据",
|
||||
items: []
|
||||
}];
|
||||
}
|
||||
|
||||
private parentWhitelist?: string[] | null;
|
||||
private childWhitelist?: string[] | null;
|
||||
|
||||
get title() {
|
||||
return this._spec.title;
|
||||
return this._spec.title || this.componentName;
|
||||
}
|
||||
|
||||
get icon() {
|
||||
return this._spec.icon;
|
||||
}
|
||||
|
||||
get propsConfigure() {
|
||||
return this.configure.props;
|
||||
}
|
||||
|
||||
constructor(private _spec: ComponentDescriptionSpec) {
|
||||
constructor(private _spec: ComponentDescription) {
|
||||
this.parseSpec(_spec);
|
||||
}
|
||||
|
||||
private parseSpec(spec: ComponentDescriptionSpec) {
|
||||
private parseSpec(spec: ComponentDescription) {
|
||||
const { componentName, uri, configure, npm, props } = spec;
|
||||
this._uri = uri || (npm ? npmToURI(npm) : componentName);
|
||||
this._componentName = componentName;
|
||||
@ -335,10 +234,10 @@ export class ComponentConfig {
|
||||
} else {
|
||||
this._configure = configure;
|
||||
}
|
||||
if (!this.configure.props) {
|
||||
this.configure.props = props ? generatePropsConfigure(props) : [];
|
||||
if (!this._configure.props) {
|
||||
this._configure.props = props ? generatePropsConfigure(props) : [];
|
||||
}
|
||||
const { component } = this.configure;
|
||||
const { component } = this._configure;
|
||||
if (component) {
|
||||
this._isContainer = component.isContainer ? true : false;
|
||||
this._isModal = component.isModal ? true : false;
|
||||
@ -358,16 +257,15 @@ export class ComponentConfig {
|
||||
return this.componentName === 'Page' || this.componentName === 'Block' || this.componentName === 'Component';
|
||||
}
|
||||
|
||||
set spec(spec: ComponentDescriptionSpec) {
|
||||
set spec(spec: ComponentDescription) {
|
||||
this._spec = spec;
|
||||
this.parseSpec(spec);
|
||||
}
|
||||
|
||||
get spec(): ComponentDescriptionSpec {
|
||||
get spec(): ComponentDescription {
|
||||
return this._spec;
|
||||
}
|
||||
|
||||
|
||||
checkNestingUp(my: Node | NodeData, parent: NodeParent) {
|
||||
if (this.parentWhitelist) {
|
||||
return this.parentWhitelist.includes(parent.componentName);
|
||||
@ -1,5 +1,5 @@
|
||||
import { ComponentType } from 'react';
|
||||
import { obx, computed } from '@recore/obx';
|
||||
import { ComponentType as ReactComponentType } from 'react';
|
||||
import { obx, computed, autorun } from '@recore/obx';
|
||||
import BuiltinSimulatorView from '../builtins/simulator';
|
||||
import Project from './project';
|
||||
import { ProjectSchema } from './schema';
|
||||
@ -10,10 +10,11 @@ import Location, { LocationData, isLocationChildrenDetail } from './helper/locat
|
||||
import DocumentModel from './document/document-model';
|
||||
import Node, { insertChildren } from './document/node/node';
|
||||
import { isRootNode } from './document/node/root-node';
|
||||
import { ComponentDescriptionSpec, ComponentConfig } from './component-config';
|
||||
import { ComponentDescription, ComponentType } from './component-type';
|
||||
import Scroller, { IScrollable } from './helper/scroller';
|
||||
import { INodeSelector } from './simulator';
|
||||
import OffsetObserver, { createOffsetObserver } from './helper/offset-observer';
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
export interface DesignerProps {
|
||||
className?: string;
|
||||
@ -21,15 +22,15 @@ export interface DesignerProps {
|
||||
defaultSchema?: ProjectSchema;
|
||||
hotkeys?: object;
|
||||
simulatorProps?: object | ((document: DocumentModel) => object);
|
||||
simulatorComponent?: ComponentType<any>;
|
||||
dragGhostComponent?: ComponentType<any>;
|
||||
simulatorComponent?: ReactComponentType<any>;
|
||||
dragGhostComponent?: ReactComponentType<any>;
|
||||
suspensed?: boolean;
|
||||
componentDescriptionSpecs?: ComponentDescriptionSpec[];
|
||||
componentsDescription?: ComponentDescription[];
|
||||
eventPipe?: EventEmitter;
|
||||
onMount?: (designer: Designer) => void;
|
||||
onDragstart?: (e: LocateEvent) => void;
|
||||
onDrag?: (e: LocateEvent) => void;
|
||||
onDragend?: (e: { dragObject: DragObject; copy: boolean }, loc?: Location) => void;
|
||||
// TODO: ...add other events support
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
@ -40,7 +41,21 @@ export default class Designer {
|
||||
readonly hovering = new Hovering();
|
||||
readonly project: Project;
|
||||
|
||||
get currentDocument() {
|
||||
return this.project.currentDocument;
|
||||
}
|
||||
|
||||
get currentHistory() {
|
||||
return this.currentDocument?.history;
|
||||
}
|
||||
|
||||
get currentSelection() {
|
||||
return this.currentDocument?.selection;
|
||||
}
|
||||
|
||||
constructor(props: DesignerProps) {
|
||||
this.setProps(props);
|
||||
|
||||
this.project = new Project(this, props.defaultSchema);
|
||||
|
||||
this.dragon.onDragstart(e => {
|
||||
@ -53,13 +68,14 @@ export default class Designer {
|
||||
if (this.props?.onDragstart) {
|
||||
this.props.onDragstart(e);
|
||||
}
|
||||
this.postEvent('dragstart', e);
|
||||
});
|
||||
|
||||
this.dragon.onDrag(e => {
|
||||
console.info('dropLocation', this._dropLocation);
|
||||
if (this.props?.onDrag) {
|
||||
this.props.onDrag(e);
|
||||
}
|
||||
this.postEvent('drag', e);
|
||||
});
|
||||
|
||||
this.dragon.onDragend(e => {
|
||||
@ -85,6 +101,7 @@ export default class Designer {
|
||||
if (this.props?.onDragend) {
|
||||
this.props.onDragend(e, loc);
|
||||
}
|
||||
this.postEvent('dragend', e, loc);
|
||||
this.hovering.enable = true;
|
||||
});
|
||||
|
||||
@ -92,7 +109,30 @@ export default class Designer {
|
||||
node.document.simulator?.scrollToNode(node, detail);
|
||||
});
|
||||
|
||||
this.setProps(props);
|
||||
let selectionDispose: undefined | (() => void);
|
||||
const setupSelection = () => {
|
||||
if (selectionDispose) {
|
||||
selectionDispose();
|
||||
selectionDispose = undefined;
|
||||
}
|
||||
if (this.currentSelection) {
|
||||
const currentSelection = this.currentSelection;
|
||||
selectionDispose = currentSelection.onSelectionChange(() => {
|
||||
this.postEvent('current-selection-change', currentSelection);
|
||||
});
|
||||
}
|
||||
}
|
||||
this.project.onCurrentDocumentChange(() => {
|
||||
this.postEvent('current-document-change', this.currentDocument);
|
||||
this.postEvent('current-selection-change', this.currentSelection);
|
||||
this.postEvent('current-history-change', this.currentHistory);
|
||||
setupSelection();
|
||||
});
|
||||
setupSelection();
|
||||
}
|
||||
|
||||
postEvent(event: string, ...args: any[]) {
|
||||
this.props?.eventPipe?.emit(`designer.${event}`, ...args);
|
||||
}
|
||||
|
||||
private _dropLocation?: Location;
|
||||
@ -129,7 +169,7 @@ export default class Designer {
|
||||
* 获得合适的插入位置
|
||||
*/
|
||||
getSuitableInsertion() {
|
||||
const activedDoc = this.project.activedDocuments[0];
|
||||
const activedDoc = this.project.currentDocument;
|
||||
if (!activedDoc) {
|
||||
return null;
|
||||
}
|
||||
@ -166,8 +206,8 @@ export default class Designer {
|
||||
if (props.suspensed !== this.props.suspensed && props.suspensed != null) {
|
||||
this.suspensed = props.suspensed;
|
||||
}
|
||||
if (props.componentDescriptionSpecs !== this.props.componentDescriptionSpecs && props.componentDescriptionSpecs != null) {
|
||||
this.buildComponentConfigsMap(props.componentDescriptionSpecs);
|
||||
if (props.componentsDescription !== this.props.componentsDescription && props.componentsDescription != null) {
|
||||
this.buildComponentTypesMap(props.componentsDescription);
|
||||
}
|
||||
} else {
|
||||
// init hotkeys
|
||||
@ -183,8 +223,8 @@ export default class Designer {
|
||||
if (props.suspensed != null) {
|
||||
this.suspensed = props.suspensed;
|
||||
}
|
||||
if (props.componentDescriptionSpecs != null) {
|
||||
this.buildComponentConfigsMap(props.componentDescriptionSpecs);
|
||||
if (props.componentsDescription != null) {
|
||||
this.buildComponentTypesMap(props.componentsDescription);
|
||||
}
|
||||
}
|
||||
this.props = props;
|
||||
@ -194,9 +234,9 @@ export default class Designer {
|
||||
return this.props ? this.props[key] : null;
|
||||
}
|
||||
|
||||
@obx.ref private _simulatorComponent?: ComponentType<any>;
|
||||
@obx.ref private _simulatorComponent?: ReactComponentType<any>;
|
||||
|
||||
@computed get simulatorComponent(): ComponentType<any> {
|
||||
@computed get simulatorComponent(): ReactComponentType<any> {
|
||||
return this._simulatorComponent || BuiltinSimulatorView;
|
||||
}
|
||||
|
||||
@ -228,38 +268,60 @@ export default class Designer {
|
||||
// todo:
|
||||
}
|
||||
|
||||
@obx.val private _componentConfigsMap = new Map<string, ComponentConfig>();
|
||||
@obx.val private _componentTypesMap = new Map<string, ComponentType>();
|
||||
private _lostComponentTypesMap = new Map<string, ComponentType>();
|
||||
|
||||
private buildComponentConfigsMap(specs: ComponentDescriptionSpec[]) {
|
||||
private buildComponentTypesMap(specs: ComponentDescription[]) {
|
||||
specs.forEach(spec => {
|
||||
const key = spec.componentName;
|
||||
const had = this._componentConfigsMap.get(key);
|
||||
if (had) {
|
||||
had.spec = spec;
|
||||
let cType = this._componentTypesMap.get(key);
|
||||
if (cType) {
|
||||
cType.spec = spec;
|
||||
} else {
|
||||
this._componentConfigsMap.set(key, new ComponentConfig(spec));
|
||||
cType = this._lostComponentTypesMap.get(key);
|
||||
|
||||
if (cType) {
|
||||
cType.spec = spec;
|
||||
this._lostComponentTypesMap.delete(key);
|
||||
} else {
|
||||
cType = new ComponentType(spec);
|
||||
}
|
||||
|
||||
this._componentTypesMap.set(key, cType);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getComponentConfig(componentName: string): ComponentConfig {
|
||||
if (this._componentConfigsMap.has(componentName)) {
|
||||
return this._componentConfigsMap.get(componentName)!;
|
||||
getComponentType(componentName: string): ComponentType {
|
||||
if (this._componentTypesMap.has(componentName)) {
|
||||
return this._componentTypesMap.get(componentName)!;
|
||||
}
|
||||
|
||||
return new ComponentConfig({
|
||||
if (this._lostComponentTypesMap.has(componentName)) {
|
||||
return this._lostComponentTypesMap.get(componentName)!;
|
||||
}
|
||||
|
||||
const cType = new ComponentType({
|
||||
componentName,
|
||||
});
|
||||
|
||||
this._lostComponentTypesMap.set(componentName, cType);
|
||||
|
||||
return cType;
|
||||
}
|
||||
|
||||
get componentsMap(): { [key: string]: ComponentDescriptionSpec } {
|
||||
get componentsMap(): { [key: string]: ComponentDescription } {
|
||||
const maps: any = {};
|
||||
this._componentConfigsMap.forEach((config, key) => {
|
||||
this._componentTypesMap.forEach((config, key) => {
|
||||
maps[key] = config.spec;
|
||||
});
|
||||
return maps;
|
||||
}
|
||||
|
||||
autorun(action: (context: { firstRun: boolean }) => void, sync: boolean = false): () => void {
|
||||
return autorun(action, sync as true);
|
||||
}
|
||||
|
||||
purge() {
|
||||
// todo:
|
||||
}
|
||||
|
||||
@ -4,9 +4,11 @@ import Node, { isNodeParent, insertChildren, insertChild, NodeParent } from './n
|
||||
import { Selection } from './selection';
|
||||
import RootNode from './node/root-node';
|
||||
import { ISimulator, Component } from '../simulator';
|
||||
import { computed, obx } from '@recore/obx';
|
||||
import { computed, obx, autorun } from '@recore/obx';
|
||||
import Location from '../helper/location';
|
||||
import { ComponentConfig } from '../component-config';
|
||||
import { ComponentType } from '../component-type';
|
||||
import History from '../helper/history';
|
||||
import Prop from './node/props/prop';
|
||||
|
||||
export default class DocumentModel {
|
||||
/**
|
||||
@ -24,11 +26,10 @@ export default class DocumentModel {
|
||||
/**
|
||||
* 操作记录控制
|
||||
*/
|
||||
// TODO
|
||||
// readonly history: History = new History(this);
|
||||
readonly history: History;
|
||||
|
||||
private nodesMap = new Map<string, Node>();
|
||||
private nodes = new Set<Node>();
|
||||
@obx.val private nodes = new Set<Node>();
|
||||
private seqId = 0;
|
||||
private _simulator?: ISimulator;
|
||||
|
||||
@ -40,16 +41,28 @@ export default class DocumentModel {
|
||||
}
|
||||
|
||||
get fileName(): string {
|
||||
return (this.rootNode.extras.get('fileName')?.value as string) || this.id;
|
||||
return (this.rootNode.getExtraProp('fileName')?.getAsString()) || this.id;
|
||||
}
|
||||
|
||||
set fileName(fileName: string) {
|
||||
this.rootNode.extras.get('fileName', true).value = fileName;
|
||||
this.rootNode.getExtraProp('fileName', true)?.setValue(fileName);
|
||||
}
|
||||
|
||||
constructor(readonly project: Project, schema: RootSchema) {
|
||||
this.rootNode = this.createNode(schema) as RootNode;
|
||||
autorun(() => {
|
||||
this.nodes.forEach(item => {
|
||||
if (item.parent == null && item !== this.rootNode) {
|
||||
item.purge();
|
||||
}
|
||||
});
|
||||
}, true);
|
||||
this.rootNode = this.createRootNode(schema);
|
||||
this.id = this.rootNode.id;
|
||||
this.history = new History(
|
||||
() => this.schema,
|
||||
(schema) => this.import(schema as RootSchema, true),
|
||||
);
|
||||
this.setupListenActiveNodes();
|
||||
}
|
||||
|
||||
readonly designer = this.project.designer;
|
||||
@ -76,10 +89,16 @@ export default class DocumentModel {
|
||||
return node ? !node.isPurged : false;
|
||||
}
|
||||
|
||||
@obx.val private activeNodes?: Node[];
|
||||
|
||||
private setupListenActiveNodes() {
|
||||
// todo:
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 schema 创建一个节点
|
||||
*/
|
||||
createNode(data: NodeData): Node {
|
||||
createNode(data: NodeData, slotFor?: Prop): Node {
|
||||
let schema: any;
|
||||
if (isDOMText(data) || isJSExpression(data)) {
|
||||
schema = {
|
||||
@ -89,7 +108,40 @@ export default class DocumentModel {
|
||||
} else {
|
||||
schema = data;
|
||||
}
|
||||
const node = new Node(this, schema);
|
||||
|
||||
let node: Node | null = null;
|
||||
if (schema.id) {
|
||||
node = this.getNode(schema.id);
|
||||
if (node && node.componentName === schema.componentName) {
|
||||
if (node.parent) {
|
||||
node.internalSetParent(null);
|
||||
// will move to another position
|
||||
// todo: this.activeNodes?.push(node);
|
||||
}
|
||||
node.internalSetSlotFor(slotFor);
|
||||
node.import(schema, true);
|
||||
} else if (node) {
|
||||
node = null;
|
||||
}
|
||||
}
|
||||
if (!node) {
|
||||
node = new Node(this, schema, slotFor);
|
||||
// will add
|
||||
// todo: this.activeNodes?.push(node);
|
||||
}
|
||||
|
||||
if (this.nodesMap.has(node.id)) {
|
||||
this.nodesMap.get(node.id)!.internalSetParent(null);
|
||||
}
|
||||
|
||||
this.nodesMap.set(node.id, node);
|
||||
this.nodes.add(node);
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
private createRootNode(schema: RootSchema) {
|
||||
const node = new RootNode(this, schema);
|
||||
this.nodesMap.set(node.id, node);
|
||||
this.nodes.add(node);
|
||||
return node;
|
||||
@ -137,6 +189,7 @@ export default class DocumentModel {
|
||||
}
|
||||
this.nodesMap.delete(node.id);
|
||||
this.nodes.delete(node);
|
||||
this.selection.remove(node.id);
|
||||
node.remove();
|
||||
}
|
||||
|
||||
@ -184,6 +237,12 @@ export default class DocumentModel {
|
||||
return this.rootNode.schema as any;
|
||||
}
|
||||
|
||||
import(schema: RootSchema, checkId: boolean = false) {
|
||||
this.rootNode.import(schema, checkId);
|
||||
// todo: purge something
|
||||
// todo: select added and active track added
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出节点数据
|
||||
*/
|
||||
@ -199,7 +258,7 @@ export default class DocumentModel {
|
||||
* 是否已修改
|
||||
*/
|
||||
isModified() {
|
||||
// return !this.history.isSavePoint();
|
||||
return !this.history.isSavePoint();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -226,14 +285,12 @@ export default class DocumentModel {
|
||||
return this.simulator!.getComponent(componentName);
|
||||
}
|
||||
|
||||
getComponentConfig(componentName: string, component?: Component | null): ComponentConfig {
|
||||
getComponentType(componentName: string, component?: Component | null): ComponentType {
|
||||
// TODO: guess componentConfig from component by simulator
|
||||
return this.designer.getComponentConfig(componentName);
|
||||
return this.designer.getComponentType(componentName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@obx.ref private _opened: boolean = true;
|
||||
@obx.ref private _opened: boolean = false;
|
||||
@obx.ref private _suspensed: boolean = false;
|
||||
|
||||
/**
|
||||
@ -284,7 +341,11 @@ export default class DocumentModel {
|
||||
* 打开,已载入,默认建立时就打开状态,除非手动关闭
|
||||
*/
|
||||
open(): void {
|
||||
const originState = this._opened;
|
||||
this._opened = true;
|
||||
if (originState === false) {
|
||||
this.designer.postEvent('document-open', this);
|
||||
}
|
||||
if (this._suspensed) {
|
||||
this.setSuspense(false);
|
||||
} else {
|
||||
@ -303,7 +364,9 @@ export default class DocumentModel {
|
||||
/**
|
||||
* 从项目中移除
|
||||
*/
|
||||
remove() {}
|
||||
remove() {
|
||||
// todo:
|
||||
}
|
||||
}
|
||||
|
||||
export function isDocumentModel(obj: any): obj is DocumentModel {
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import Node, { NodeParent } from './node';
|
||||
import { NodeData } from '../../schema';
|
||||
import { NodeData, isNodeSchema } from '../../schema';
|
||||
import { obx, computed } from '@recore/obx';
|
||||
|
||||
export default class NodeChildren {
|
||||
@obx.val private children: Node[];
|
||||
constructor(readonly owner: NodeParent, childrenData: NodeData | NodeData[]) {
|
||||
this.children = (Array.isArray(childrenData) ? childrenData : [childrenData]).map(child => {
|
||||
constructor(readonly owner: NodeParent, data: NodeData | NodeData[]) {
|
||||
this.children = (Array.isArray(data) ? data : [data]).map(child => {
|
||||
const node = this.owner.document.createNode(child);
|
||||
node.internalSetParent(this.owner);
|
||||
return node;
|
||||
@ -16,8 +16,33 @@ export default class NodeChildren {
|
||||
* 导出 schema
|
||||
* @param serialize 序列化,加 id 标识符,用于储存为操作记录
|
||||
*/
|
||||
exportSchema(serialize = false): NodeData[] {
|
||||
return this.children.map(node => node.exportSchema(serialize));
|
||||
export(serialize = false): NodeData[] {
|
||||
return this.children.map(node => node.export(serialize));
|
||||
}
|
||||
|
||||
import(data?: NodeData | NodeData[], checkId: boolean = false) {
|
||||
data = data ? (Array.isArray(data) ? data : [data]) : [];
|
||||
|
||||
const originChildren = this.children.slice();
|
||||
this.children.forEach(child => child.internalSetParent(null));
|
||||
|
||||
const children = new Array<Node>(data.length);
|
||||
for (let i = 0, l = data.length; i < l; i++) {
|
||||
const child = originChildren[i];
|
||||
const item = data[i];
|
||||
|
||||
let node: Node | undefined;
|
||||
if (isNodeSchema(item) && !checkId && child && child.componentName === item.componentName) {
|
||||
node = child;
|
||||
node.import(item);
|
||||
} else {
|
||||
node = this.owner.document.createNode(item);
|
||||
}
|
||||
node.internalSetParent(this.owner);
|
||||
children[i] = node;
|
||||
}
|
||||
|
||||
this.children = children;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -34,27 +59,6 @@ export default class NodeChildren {
|
||||
return this.size < 1;
|
||||
}
|
||||
|
||||
/*
|
||||
// 用于数据重新灌入
|
||||
merge() {
|
||||
for (let i = 0, l = data.length; i < l; i++) {
|
||||
const item = this.children[i];
|
||||
if (item && isMergeable(item) && item.tagName === data[i].tagName) {
|
||||
item.merge(data[i]);
|
||||
} else {
|
||||
if (item) {
|
||||
item.purge();
|
||||
}
|
||||
this.children[i] = this.document.createNode(data[i]);
|
||||
this.children[i].internalSetParent(this);
|
||||
}
|
||||
}
|
||||
if (this.children.length > data.length) {
|
||||
this.children.splice(data.length).forEach(child => child.purge());
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* 删除一个节点
|
||||
*/
|
||||
|
||||
@ -57,6 +57,10 @@ export default class NodeContent {
|
||||
}
|
||||
|
||||
constructor(value: any) {
|
||||
this.import(value);
|
||||
}
|
||||
|
||||
import(value: any) {
|
||||
const type = typeof value;
|
||||
if (value == null) {
|
||||
this._value = '';
|
||||
|
||||
@ -1,14 +1,12 @@
|
||||
import { obx, computed } from '@recore/obx';
|
||||
import { obx, computed, untracked } from '@recore/obx';
|
||||
import { NodeSchema, NodeData, PropsMap, PropsList } from '../../schema';
|
||||
import Props from './props/props';
|
||||
import Props, { EXTRA_KEY_PREFIX } from './props/props';
|
||||
import DocumentModel from '../document-model';
|
||||
import NodeChildren from './node-children';
|
||||
import Prop from './props/prop';
|
||||
import NodeContent from './node-content';
|
||||
import { Component } from '../../simulator';
|
||||
import { ComponentConfig } from '../../component-config';
|
||||
|
||||
const DIRECTIVES = ['condition', 'conditionGroup', 'loop', 'loopArgs', 'title', 'ignore', 'hidden', 'locked'];
|
||||
import { ComponentType } from '../../component-type';
|
||||
|
||||
/**
|
||||
* 基础节点
|
||||
@ -50,21 +48,12 @@ export default class Node {
|
||||
* * Component 组件/元件
|
||||
*/
|
||||
readonly componentName: string;
|
||||
protected _props?: Props<Node>;
|
||||
protected _directives?: Props<Node>;
|
||||
protected _extras?: Props<Node>;
|
||||
protected _props?: Props;
|
||||
protected _children: NodeChildren | NodeContent;
|
||||
@obx.ref private _parent: NodeParent | null = null;
|
||||
@obx.ref private _zLevel = 0;
|
||||
get props(): Props<Node> | undefined {
|
||||
get props(): Props | undefined {
|
||||
return this._props;
|
||||
}
|
||||
get directives(): Props<Node> | undefined {
|
||||
return this._directives;
|
||||
}
|
||||
get extras(): Props<Node> | undefined {
|
||||
return this._extras;
|
||||
}
|
||||
/**
|
||||
* 父级节点
|
||||
*/
|
||||
@ -80,14 +69,17 @@ export default class Node {
|
||||
/**
|
||||
* 当前节点深度
|
||||
*/
|
||||
get zLevel(): number {
|
||||
return this._zLevel;
|
||||
@computed get zLevel(): number {
|
||||
if (this._parent) {
|
||||
return this._parent.zLevel + 1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@computed get title(): string {
|
||||
let t = this.getDirective('x-title');
|
||||
if (!t && this.componentConfig.descriptor) {
|
||||
t = this.getProp(this.componentConfig.descriptor, false);
|
||||
let t = this.getExtraProp('title');
|
||||
if (!t && this.componentType.descriptor) {
|
||||
t = this.getProp(this.componentType.descriptor, false);
|
||||
}
|
||||
if (t) {
|
||||
const v = t.getAsString();
|
||||
@ -95,23 +87,20 @@ export default class Node {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
return this.componentName;
|
||||
return this.componentType.title;
|
||||
}
|
||||
|
||||
constructor(readonly document: DocumentModel, nodeSchema: NodeSchema) {
|
||||
get isSlotRoot(): boolean {
|
||||
return this._slotFor != null;
|
||||
}
|
||||
|
||||
constructor(readonly document: DocumentModel, nodeSchema: NodeSchema, slotFor?: Prop) {
|
||||
const { componentName, id, children, props, ...extras } = nodeSchema;
|
||||
this.id = id || `node$${document.nextId()}`;
|
||||
this.componentName = componentName;
|
||||
if (this.isNodeParent) {
|
||||
this._props = new Props(this, props);
|
||||
this._directives = new Props(this, {});
|
||||
Object.keys(extras).forEach(key => {
|
||||
if (DIRECTIVES.indexOf(key) > -1) {
|
||||
this._directives!.add((extras as any)[key], key);
|
||||
delete (extras as any)[key];
|
||||
}
|
||||
});
|
||||
this._extras = new Props(this, extras as any);
|
||||
this._slotFor = slotFor;
|
||||
if (isNodeParent(this)) {
|
||||
this._props = new Props(this, props, extras);
|
||||
this._children = new NodeChildren(this as NodeParent, children || []);
|
||||
} else {
|
||||
this._children = new NodeContent(children);
|
||||
@ -127,30 +116,33 @@ export default class Node {
|
||||
|
||||
/**
|
||||
* 内部方法,请勿使用
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
internalSetParent(parent: NodeParent | null) {
|
||||
if (this._parent === parent) {
|
||||
return;
|
||||
}
|
||||
if (this._parent) {
|
||||
|
||||
if (this._parent && !this.isSlotRoot) {
|
||||
this._parent.children.delete(this);
|
||||
}
|
||||
|
||||
this._parent = parent;
|
||||
if (parent) {
|
||||
this._zLevel = parent.zLevel + 1;
|
||||
} else {
|
||||
this._zLevel = -1;
|
||||
}
|
||||
}
|
||||
|
||||
private _slotFor?: Prop | null = null;
|
||||
internalSetSlotFor(slotFor: Prop | null | undefined) {
|
||||
this._slotFor = slotFor;
|
||||
}
|
||||
|
||||
get slotFor() {
|
||||
return this._slotFor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除当前节点
|
||||
*/
|
||||
remove() {
|
||||
if (this.parent) {
|
||||
if (this.parent && !this.isSlotRoot) {
|
||||
this.parent.children.delete(this, true);
|
||||
}
|
||||
}
|
||||
@ -162,6 +154,17 @@ export default class Node {
|
||||
this.document.selection.select(this.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 悬停高亮
|
||||
*/
|
||||
hover(flag: boolean = true) {
|
||||
if (flag) {
|
||||
this.document.designer.hovering.hover(this);
|
||||
} else {
|
||||
this.document.designer.hovering.unhover(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点组件类
|
||||
*/
|
||||
@ -175,22 +178,15 @@ export default class Node {
|
||||
/**
|
||||
* 节点组件描述
|
||||
*/
|
||||
@obx.ref get componentConfig(): ComponentConfig {
|
||||
return this.document.getComponentConfig(this.componentName, this.component);
|
||||
@computed get componentType(): ComponentType {
|
||||
return this.document.getComponentType(this.componentName, this.component);
|
||||
}
|
||||
|
||||
@obx.ref get propsData(): PropsMap | PropsList | null {
|
||||
@computed get propsData(): PropsMap | PropsList | null {
|
||||
if (!this.isNodeParent || this.componentName === 'Fragment') {
|
||||
return null;
|
||||
}
|
||||
return this.props?.value || null;
|
||||
}
|
||||
|
||||
get directivesData(): PropsMap | null {
|
||||
if (!this.isNodeParent) {
|
||||
return null;
|
||||
}
|
||||
return this.directives?.value as PropsMap || null;
|
||||
return this.props?.export(true).props || null;
|
||||
}
|
||||
|
||||
private _conditionGroup: string | null = null;
|
||||
@ -231,31 +227,44 @@ export default class Node {
|
||||
}
|
||||
|
||||
replaceWith(schema: NodeSchema, migrate: boolean = true) {
|
||||
|
||||
// reuse the same id? or replaceSelection
|
||||
//
|
||||
}
|
||||
|
||||
/*
|
||||
// TODO
|
||||
// 外部修改,merge 进来,产生一次可恢复的历史数据
|
||||
merge(data: ElementData) {
|
||||
this.elementData = data;
|
||||
const { leadingComments } = data;
|
||||
this.leadingComments = leadingComments ? leadingComments.slice() : [];
|
||||
this.parse();
|
||||
this.mergeChildren(data.children || []);
|
||||
}
|
||||
|
||||
// TODO: 再利用历史数据,不产生历史数据
|
||||
reuse(timelineData: NodeSchema) {}
|
||||
*/
|
||||
|
||||
getProp(path: string, useStash: boolean = true): Prop | null {
|
||||
return this.props?.query(path, useStash as any) || null;
|
||||
}
|
||||
|
||||
getDirective(name: string, useStash: boolean = true): Prop | null {
|
||||
return this.directives?.get(name, useStash as any) || null;
|
||||
getExtraProp(key: string, useStash: boolean = true): Prop | null {
|
||||
return this.props?.get(EXTRA_KEY_PREFIX + key, useStash) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个属性值
|
||||
*/
|
||||
getPropValue(path: string): any {
|
||||
return this.getProp(path, false)?.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置单个属性值
|
||||
*/
|
||||
setPropValue(path: string, value: any) {
|
||||
this.getProp(path, true)!.setValue(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置多个属性值,和原有值合并
|
||||
*/
|
||||
mergeProps(props: PropsMap) {
|
||||
this.props?.merge(props);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置多个属性值,替换原有值
|
||||
*/
|
||||
setProps(props?: PropsMap | PropsList | null) {
|
||||
this.props?.import(props);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -300,28 +309,41 @@ export default class Node {
|
||||
* 获取符合搭建协议-节点 schema 结构
|
||||
*/
|
||||
get schema(): NodeSchema {
|
||||
// TODO: ..
|
||||
return this.exportSchema(true);
|
||||
return this.export(true);
|
||||
}
|
||||
|
||||
set schema(data: NodeSchema) {
|
||||
this.import(data);
|
||||
}
|
||||
|
||||
import(data: NodeSchema, checkId: boolean = false) {
|
||||
const { componentName, id, children, props, ...extras } = data;
|
||||
|
||||
if (isNodeParent(this)) {
|
||||
this._props!.import(props, extras);
|
||||
this._children.import(children, checkId);
|
||||
} else {
|
||||
this._children.import(children);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出 schema
|
||||
* @param serialize 序列化,加 id 标识符,用于储存为操作记录
|
||||
*/
|
||||
exportSchema(serialize = false): NodeSchema {
|
||||
// TODO...
|
||||
export(serialize = false): NodeSchema {
|
||||
const { props, extras } = this.props?.export(serialize) || {};
|
||||
const schema: any = {
|
||||
componentName: this.componentName,
|
||||
...this.extras?.value,
|
||||
props: this.props?.value || {},
|
||||
...this.directives?.value,
|
||||
props,
|
||||
...extras,
|
||||
};
|
||||
if (serialize) {
|
||||
schema.id = this.id;
|
||||
}
|
||||
if (isNodeParent(this)) {
|
||||
if (this.children.size > 0) {
|
||||
schema.children = this.children.exportSchema(serialize);
|
||||
schema.children = this.children.export(serialize);
|
||||
}
|
||||
} else {
|
||||
schema.children = (this.children as NodeContent).value;
|
||||
@ -379,17 +401,13 @@ export default class Node {
|
||||
this.children.purge();
|
||||
}
|
||||
this.props?.purge();
|
||||
this.directives?.purge();
|
||||
this.extras?.purge();
|
||||
this.document.internalRemoveAndPurgeNode(this);
|
||||
}
|
||||
}
|
||||
|
||||
export interface NodeParent extends Node {
|
||||
readonly children: NodeChildren;
|
||||
readonly props: Props<Node>;
|
||||
readonly directives: Props<Node>;
|
||||
readonly extras: Props<Node>;
|
||||
readonly props: Props;
|
||||
}
|
||||
|
||||
export function isNode(node: any): node is Node {
|
||||
@ -465,8 +483,8 @@ export function comparePosition(node1: Node, node2: Node): number {
|
||||
|
||||
export function insertChild(container: NodeParent, thing: Node | NodeData, at?: number | null, copy?: boolean): Node {
|
||||
let node: Node;
|
||||
if (copy && isNode(thing)) {
|
||||
thing = thing.exportSchema(false);
|
||||
if (isNode(thing) && (copy || thing.isSlotRoot)) {
|
||||
thing = thing.export(false);
|
||||
}
|
||||
if (isNode(thing)) {
|
||||
node = thing;
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import { obx, autorun, untracked, computed } from '@recore/obx';
|
||||
import Prop, { IPropParent } from './prop';
|
||||
import Prop, { IPropParent, UNSET } from './prop';
|
||||
import Props from './props';
|
||||
|
||||
export type PendingItem = Prop[];
|
||||
export default class StashSpace implements IPropParent {
|
||||
export default class PropStash implements IPropParent {
|
||||
@obx.val private space: Set<Prop> = new Set();
|
||||
@computed private get maps(): Map<string, Prop> {
|
||||
@computed private get maps(): Map<string | number, Prop> {
|
||||
const maps = new Map();
|
||||
if (this.space.size > 0) {
|
||||
this.space.forEach(prop => {
|
||||
@ -15,7 +16,7 @@ export default class StashSpace implements IPropParent {
|
||||
}
|
||||
private willPurge: () => void;
|
||||
|
||||
constructor(write: (item: Prop) => void, before: () => boolean) {
|
||||
constructor(readonly props: Props, write: (item: Prop) => void) {
|
||||
this.willPurge = autorun(() => {
|
||||
if (this.space.size < 1) {
|
||||
return;
|
||||
@ -29,20 +30,18 @@ export default class StashSpace implements IPropParent {
|
||||
}
|
||||
if (pending.length > 0) {
|
||||
untracked(() => {
|
||||
if (before()) {
|
||||
for (const item of pending) {
|
||||
write(item);
|
||||
}
|
||||
for (const item of pending) {
|
||||
write(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get(key: string): Prop {
|
||||
get(key: string | number): Prop {
|
||||
let prop = this.maps.get(key);
|
||||
if (!prop) {
|
||||
prop = new Prop(this, null, key);
|
||||
prop = new Prop(this, UNSET, key);
|
||||
this.space.add(prop);
|
||||
}
|
||||
return prop;
|
||||
@ -1,16 +1,19 @@
|
||||
import { untracked, computed, obx } from '@recore/obx';
|
||||
import { valueToSource } from '../../../../utils/value-to-source';
|
||||
import { CompositeValue, isJSExpression } from '../../../schema';
|
||||
import StashSpace from './stash-space';
|
||||
import { uniqueId } from '../../../../utils/unique-id';
|
||||
import { isPlainObject } from '../../../../utils/is-plain-object';
|
||||
import { CompositeValue, isJSExpression, isJSSlot, NodeSchema, NodeData, isNodeSchema } from '../../../schema';
|
||||
import PropStash from './prop-stash';
|
||||
import { uniqueId } from '../../../../../../utils/unique-id';
|
||||
import { isPlainObject } from '../../../../../../utils/is-plain-object';
|
||||
import { hasOwnProperty } from '../../../../utils/has-own-property';
|
||||
import Props from './props';
|
||||
import Node from '../node';
|
||||
|
||||
export const UNSET = Symbol.for('unset');
|
||||
export type UNSET = typeof UNSET;
|
||||
|
||||
export interface IPropParent {
|
||||
delete(prop: Prop): void;
|
||||
readonly props: Props;
|
||||
}
|
||||
|
||||
export default class Prop implements IPropParent {
|
||||
@ -18,11 +21,11 @@ export default class Prop implements IPropParent {
|
||||
|
||||
readonly id = uniqueId('prop$');
|
||||
|
||||
private _type: 'unset' | 'literal' | 'map' | 'list' | 'expression' = 'unset';
|
||||
@obx.ref private _type: 'unset' | 'literal' | 'map' | 'list' | 'expression' | 'slot' = 'unset';
|
||||
/**
|
||||
* 属性类型
|
||||
*/
|
||||
get type(): 'unset' | 'literal' | 'map' | 'list' | 'expression' {
|
||||
get type(): 'unset' | 'literal' | 'map' | 'list' | 'expression' | 'slot' {
|
||||
return this._type;
|
||||
}
|
||||
|
||||
@ -31,23 +34,38 @@ export default class Prop implements IPropParent {
|
||||
/**
|
||||
* 属性值
|
||||
*/
|
||||
@computed get value(): CompositeValue {
|
||||
if (this._type === 'unset') {
|
||||
return null;
|
||||
@computed get value(): CompositeValue | UNSET {
|
||||
return this.export(true);
|
||||
}
|
||||
|
||||
export(serialize = false): CompositeValue | UNSET {
|
||||
const type = this._type;
|
||||
|
||||
if (type === 'unset') {
|
||||
return UNSET;
|
||||
}
|
||||
|
||||
const type = this._type;
|
||||
if (type === 'literal' || type === 'expression') {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
if (type === 'slot') {
|
||||
return {
|
||||
type: 'JSSlot',
|
||||
value: this._slotNode!.export(serialize),
|
||||
};
|
||||
}
|
||||
|
||||
if (type === 'map') {
|
||||
if (!this._items) {
|
||||
return this._value;
|
||||
}
|
||||
const maps: any = {};
|
||||
this.items!.forEach((prop, key) => {
|
||||
maps[key] = prop.value;
|
||||
const v = prop.export(serialize);
|
||||
if (v !== UNSET) {
|
||||
maps[key] = v;
|
||||
}
|
||||
});
|
||||
return maps;
|
||||
}
|
||||
@ -56,12 +74,31 @@ export default class Prop implements IPropParent {
|
||||
if (!this._items) {
|
||||
return this._items;
|
||||
}
|
||||
return this.items!.map(prop => prop.value);
|
||||
return this.items!.map(prop => {
|
||||
const v = prop.export(serialize);
|
||||
return v === UNSET ? null : v
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得表达式值
|
||||
*/
|
||||
@computed get code() {
|
||||
if (isJSExpression(this.value)) {
|
||||
return this.value.value;
|
||||
}
|
||||
if (this.type === 'slot') {
|
||||
return JSON.stringify(this._slotNode!.export(false));
|
||||
}
|
||||
return JSON.stringify(this.value);
|
||||
}
|
||||
set code(val) {
|
||||
|
||||
}
|
||||
|
||||
@computed getAsString(): string {
|
||||
if (this.type === 'literal') {
|
||||
return this._value ? String(this._value) : '';
|
||||
@ -72,18 +109,21 @@ export default class Prop implements IPropParent {
|
||||
/**
|
||||
* set value, val should be JSON Object
|
||||
*/
|
||||
set value(val: CompositeValue) {
|
||||
setValue(val: CompositeValue) {
|
||||
this._value = val;
|
||||
const t = typeof val;
|
||||
if (val == null) {
|
||||
this._value = null;
|
||||
this._type = 'literal';
|
||||
} else if (t === 'string' || t === 'number' || t === 'boolean') {
|
||||
this._value = val;
|
||||
this._type = 'literal';
|
||||
} else if (Array.isArray(val)) {
|
||||
this._type = 'list';
|
||||
} else if (isPlainObject(val)) {
|
||||
if (isJSSlot(val)) {
|
||||
this.setAsSlot(val.value);
|
||||
return;
|
||||
}
|
||||
if (isJSExpression(val)) {
|
||||
this._type = 'expression';
|
||||
} else {
|
||||
@ -97,14 +137,46 @@ export default class Prop implements IPropParent {
|
||||
value: valueToSource(val),
|
||||
};
|
||||
}
|
||||
if (untracked(() => this._items)) {
|
||||
this._items!.forEach(prop => prop.purge());
|
||||
this._items = null;
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
getValue(serialize = true) {
|
||||
// todo:
|
||||
}
|
||||
|
||||
private dispose() {
|
||||
const items = untracked(() => this._items);
|
||||
if (items) {
|
||||
items.forEach(prop => prop.purge());
|
||||
}
|
||||
this._items = null;
|
||||
this._maps = null;
|
||||
if (this.stash) {
|
||||
this.stash.clear();
|
||||
}
|
||||
if (this._type !== 'slot' && this._slotNode) {
|
||||
this._slotNode.purge();
|
||||
this._slotNode = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _slotNode?: Node;
|
||||
setAsSlot(data: NodeData) {
|
||||
this._type = 'slot';
|
||||
if (
|
||||
this._slotNode &&
|
||||
isNodeSchema(data) &&
|
||||
(!data.id || this._slotNode.id === data.id) &&
|
||||
this._slotNode.componentName === data.componentName
|
||||
) {
|
||||
this._slotNode.import(data);
|
||||
} else {
|
||||
this._slotNode?.internalSetParent(null);
|
||||
const owner = this.props.owner;
|
||||
this._slotNode = owner.document.createNode(data, this);
|
||||
this._slotNode.internalSetParent(owner);
|
||||
}
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -121,33 +193,24 @@ export default class Prop implements IPropParent {
|
||||
return this._type === 'unset';
|
||||
}
|
||||
|
||||
/**
|
||||
* 值是否包含表达式
|
||||
* 包含 JSExpresion | JSSlot 等值
|
||||
*/
|
||||
@computed isContainJSExpression(): boolean {
|
||||
const type = this._type;
|
||||
if (type === 'expression') {
|
||||
return true;
|
||||
// TODO: improve this logic
|
||||
compare(other: Prop | null): number {
|
||||
if (!other || other.isUnset()) {
|
||||
return this.isUnset() ? 0 : 2;
|
||||
}
|
||||
if (type === 'literal' || type === 'unset') {
|
||||
return false;
|
||||
if (other.type !== this.type) {
|
||||
return 2;
|
||||
}
|
||||
if ((type === 'list' || type === 'map') && this.items) {
|
||||
return this.items.some(item => item.isContainJSExpression());
|
||||
// list
|
||||
if (this.type === 'list') {
|
||||
return this.size === other.size ? 1 : 2;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否简单 JSON 数据
|
||||
*/
|
||||
@computed isJSON() {
|
||||
return !this.isContainJSExpression();
|
||||
// 'literal' | 'map' | 'expression' | 'slot'
|
||||
return this.code === other.code ? 0 : 2;
|
||||
}
|
||||
|
||||
@obx.val private _items: Prop[] | null = null;
|
||||
@obx.val private _maps: Map<string, Prop> | null = null;
|
||||
@obx.val private _maps: Map<string | number, Prop> | null = null;
|
||||
@computed private get items(): Prop[] | null {
|
||||
let _items: any;
|
||||
untracked(() => {
|
||||
@ -182,14 +245,14 @@ export default class Prop implements IPropParent {
|
||||
}
|
||||
return _items;
|
||||
}
|
||||
@computed private get maps(): Map<string, Prop> | null {
|
||||
if (!this.items || this.items.length < 1) {
|
||||
@computed private get maps(): Map<string | number, Prop> | null {
|
||||
if (!this.items) {
|
||||
return null;
|
||||
}
|
||||
return this._maps;
|
||||
}
|
||||
|
||||
private stash: StashSpace | undefined;
|
||||
private stash: PropStash | undefined;
|
||||
|
||||
/**
|
||||
* 键值
|
||||
@ -200,14 +263,17 @@ export default class Prop implements IPropParent {
|
||||
*/
|
||||
@obx spread: boolean;
|
||||
|
||||
readonly props: Props;
|
||||
|
||||
constructor(
|
||||
public parent: IPropParent,
|
||||
value: CompositeValue | UNSET = UNSET,
|
||||
key?: string | number,
|
||||
spread = false,
|
||||
) {
|
||||
this.props = parent.props;
|
||||
if (value !== UNSET) {
|
||||
this.value = value;
|
||||
this.setValue(value);
|
||||
}
|
||||
this.key = key;
|
||||
this.spread = spread;
|
||||
@ -215,58 +281,63 @@ export default class Prop implements IPropParent {
|
||||
|
||||
/**
|
||||
* 获取某个属性
|
||||
* @param stash 强制
|
||||
* @param stash 如果不存在,临时获取一个待写入
|
||||
*/
|
||||
get(path: string, stash: false): Prop | null;
|
||||
/**
|
||||
* 获取某个属性, 如果不存在,临时获取一个待写入
|
||||
* @param stash 强制
|
||||
*/
|
||||
get(path: string, stash: true): Prop;
|
||||
/**
|
||||
* 获取某个属性, 如果不存在,临时获取一个待写入
|
||||
*/
|
||||
get(path: string): Prop;
|
||||
get(path: string, stash = true) {
|
||||
get(path: string | number, stash = true): Prop | null {
|
||||
const type = this._type;
|
||||
if (type !== 'map' && type !== 'unset' && !stash) {
|
||||
// todo: support list get
|
||||
if (type !== 'map' && type !== 'list' && type !== 'unset' && !stash) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const maps = type === 'map' ? this.maps : null;
|
||||
|
||||
let prop: any = maps ? maps.get(path) : null;
|
||||
const items = type === 'list' ? this.items : null;
|
||||
let prop: any;
|
||||
if (type === 'list') {
|
||||
if (isValidArrayIndex(path, this.size)) {
|
||||
prop = items![path];
|
||||
}
|
||||
} else if (type === 'map') {
|
||||
prop = maps?.get(path);
|
||||
}
|
||||
|
||||
if (prop) {
|
||||
return prop;
|
||||
}
|
||||
|
||||
const i = path.indexOf('.');
|
||||
let entry = path;
|
||||
let nest = '';
|
||||
if (i > 0) {
|
||||
nest = path.slice(i + 1);
|
||||
if (nest) {
|
||||
entry = path.slice(0, i);
|
||||
prop = maps ? maps.get(entry) : null;
|
||||
if (prop) {
|
||||
return prop.get(nest, stash);
|
||||
if (typeof path !== 'number') {
|
||||
const i = path.indexOf('.');
|
||||
if (i > 0) {
|
||||
nest = path.slice(i + 1);
|
||||
if (nest) {
|
||||
entry = path.slice(0, i);
|
||||
|
||||
if (type === 'list') {
|
||||
if (isValidArrayIndex(entry, this.size)) {
|
||||
prop = items![entry];
|
||||
}
|
||||
} else if (type === 'map') {
|
||||
prop = maps?.get(entry);
|
||||
}
|
||||
|
||||
if (prop) {
|
||||
return prop.get(nest, stash);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (stash) {
|
||||
if (!this.stash) {
|
||||
this.stash = new StashSpace(
|
||||
item => {
|
||||
// item take effect
|
||||
this.set(String(item.key), item);
|
||||
item.parent = this;
|
||||
},
|
||||
() => {
|
||||
return true;
|
||||
},
|
||||
);
|
||||
this.stash = new PropStash(this.props, item => {
|
||||
// item take effect
|
||||
if (item.key) {
|
||||
this.set(item.key, item, true);
|
||||
}
|
||||
item.parent = this;
|
||||
});
|
||||
}
|
||||
prop = this.stash.get(entry);
|
||||
if (nest) {
|
||||
@ -317,7 +388,7 @@ export default class Prop implements IPropParent {
|
||||
/**
|
||||
* 元素个数
|
||||
*/
|
||||
size(): number {
|
||||
get size(): number {
|
||||
return this.items?.length || 0;
|
||||
}
|
||||
|
||||
@ -332,7 +403,7 @@ export default class Prop implements IPropParent {
|
||||
return null;
|
||||
}
|
||||
if (type === 'unset' || (force && type !== 'list')) {
|
||||
this.value = [];
|
||||
this.setValue([]);
|
||||
}
|
||||
const prop = new Prop(this, value);
|
||||
this.items!.push(prop);
|
||||
@ -344,29 +415,44 @@ export default class Prop implements IPropParent {
|
||||
*
|
||||
* @param force 强制
|
||||
*/
|
||||
set(key: string, value: CompositeValue | Prop, force = false) {
|
||||
set(key: string | number, value: CompositeValue | Prop, force = false) {
|
||||
const type = this._type;
|
||||
if (type !== 'map' && type !== 'unset' && !force) {
|
||||
if (type !== 'map' && type !== 'list' && type !== 'unset' && !force) {
|
||||
return null;
|
||||
}
|
||||
if (type === 'unset' || (force && type !== 'map')) {
|
||||
this.value = {};
|
||||
if (isValidArrayIndex(key)) {
|
||||
if (type !== 'list') {
|
||||
this.setValue([]);
|
||||
}
|
||||
} else {
|
||||
this.setValue({});
|
||||
}
|
||||
}
|
||||
const prop = isProp(value) ? value : new Prop(this, value, key);
|
||||
const items = this.items!;
|
||||
const maps = this.maps!;
|
||||
const orig = maps.get(key);
|
||||
if (orig) {
|
||||
// replace
|
||||
const i = items.indexOf(orig);
|
||||
if (i > -1) {
|
||||
items.splice(i, 1, prop)[0].purge();
|
||||
if (this.type === 'list') {
|
||||
if (!isValidArrayIndex(key)) {
|
||||
return null;
|
||||
}
|
||||
items[key] = prop;
|
||||
} else if (this.maps) {
|
||||
const maps = this.maps;
|
||||
const orig = maps.get(key);
|
||||
if (orig) {
|
||||
// replace
|
||||
const i = items.indexOf(orig);
|
||||
if (i > -1) {
|
||||
items.splice(i, 1, prop)[0].purge();
|
||||
}
|
||||
maps.set(key, prop);
|
||||
} else {
|
||||
// push
|
||||
items.push(prop);
|
||||
maps.set(key, prop);
|
||||
}
|
||||
maps.set(key, prop);
|
||||
} else {
|
||||
// push
|
||||
items.push(prop);
|
||||
maps.set(key, prop);
|
||||
return null;
|
||||
}
|
||||
|
||||
return prop;
|
||||
@ -401,6 +487,9 @@ export default class Prop implements IPropParent {
|
||||
this._items.forEach(item => item.purge());
|
||||
}
|
||||
this._maps = null;
|
||||
if (this._slotNode && this._slotNode.slotFor === this) {
|
||||
this._slotNode.purge();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -458,3 +547,8 @@ export default class Prop implements IPropParent {
|
||||
export function isProp(obj: any): obj is Prop {
|
||||
return obj && obj.isProp;
|
||||
}
|
||||
|
||||
function isValidArrayIndex(key: any, limit: number = -1): key is number {
|
||||
const n = parseFloat(String(key));
|
||||
return n >= 0 && Math.floor(n) === n && isFinite(n) && (limit < 0 || n < limit);
|
||||
}
|
||||
|
||||
@ -1,14 +1,13 @@
|
||||
import { computed, obx } from '@recore/obx';
|
||||
import { uniqueId } from '../../../../utils/unique-id';
|
||||
import { CompositeValue, PropsList, PropsMap } from '../../../schema';
|
||||
import StashSpace from './stash-space';
|
||||
import Prop, { IPropParent } from './prop';
|
||||
import { uniqueId } from '../../../../../../utils/unique-id';
|
||||
import { CompositeValue, PropsList, PropsMap, CompositeObject } from '../../../schema';
|
||||
import PropStash from './prop-stash';
|
||||
import Prop, { IPropParent, UNSET } from './prop';
|
||||
import { NodeParent } from '../node';
|
||||
|
||||
export const UNSET = Symbol.for('unset');
|
||||
export type UNSET = typeof UNSET;
|
||||
export const EXTRA_KEY_PREFIX = '__';
|
||||
|
||||
|
||||
export default class Props<O = any> implements IPropParent {
|
||||
export default class Props implements IPropParent {
|
||||
readonly id = uniqueId('props');
|
||||
@obx.val private items: Prop[] = [];
|
||||
@computed private get maps(): Map<string, Prop> {
|
||||
@ -23,15 +22,14 @@ export default class Props<O = any> implements IPropParent {
|
||||
return maps;
|
||||
}
|
||||
|
||||
private stash = new StashSpace(
|
||||
prop => {
|
||||
this.items.push(prop);
|
||||
prop.parent = this;
|
||||
},
|
||||
() => {
|
||||
return true;
|
||||
},
|
||||
);
|
||||
get props(): Props {
|
||||
return this;
|
||||
}
|
||||
|
||||
private stash = new PropStash(this, prop => {
|
||||
this.items.push(prop);
|
||||
prop.parent = this;
|
||||
});
|
||||
|
||||
/**
|
||||
* 元素个数
|
||||
@ -40,58 +38,103 @@ export default class Props<O = any> implements IPropParent {
|
||||
return this.items.length;
|
||||
}
|
||||
|
||||
@computed get value(): PropsMap | PropsList | null {
|
||||
if (this.items.length < 1) {
|
||||
return null;
|
||||
}
|
||||
if (this.type === 'list') {
|
||||
return this.items.map(item => ({
|
||||
spread: item.spread,
|
||||
name: item.key as string,
|
||||
value: item.value,
|
||||
}));
|
||||
}
|
||||
const maps: any = {};
|
||||
this.items.forEach(prop => {
|
||||
if (prop.key) {
|
||||
maps[prop.key] = prop.value;
|
||||
}
|
||||
});
|
||||
return maps;
|
||||
}
|
||||
|
||||
@obx type: 'map' | 'list' = 'map';
|
||||
|
||||
constructor(readonly owner: O, value?: PropsMap | PropsList | null) {
|
||||
constructor(readonly owner: NodeParent, value?: PropsMap | PropsList | null, extras?: object) {
|
||||
if (Array.isArray(value)) {
|
||||
this.type = 'list';
|
||||
this.items = value.map(item => new Prop(this, item.value, item.name, item.spread));
|
||||
} else if (value != null) {
|
||||
this.items = Object.keys(value).map(key => new Prop(this, value[key], key));
|
||||
}
|
||||
if (extras) {
|
||||
Object.keys(extras).forEach(key => {
|
||||
this.items.push(new Prop(this, (extras as any)[key], EXTRA_KEY_PREFIX + key));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
import(value?: PropsMap | PropsList | null, extras?: object) {
|
||||
this.stash.clear();
|
||||
const originItems = this.items;
|
||||
if (Array.isArray(value)) {
|
||||
this.type = 'list';
|
||||
this.items = value.map(item => new Prop(this, item.value, item.name, item.spread));
|
||||
} else if (value != null) {
|
||||
this.type = 'map';
|
||||
this.items = Object.keys(value).map(key => new Prop(this, value[key], key));
|
||||
} else {
|
||||
this.type = 'map';
|
||||
this.items = [];
|
||||
}
|
||||
if (extras) {
|
||||
Object.keys(extras).forEach(key => {
|
||||
this.items.push(new Prop(this, (extras as any)[key], EXTRA_KEY_PREFIX + key));
|
||||
});
|
||||
}
|
||||
originItems.forEach(item => item.purge());
|
||||
}
|
||||
|
||||
merge(value: PropsMap) {
|
||||
Object.keys(value).forEach(key => {
|
||||
this.query(key, true)!.setValue(value[key]);
|
||||
});
|
||||
}
|
||||
|
||||
export(serialize = false): { props?: PropsMap | PropsList; extras?: object} {
|
||||
if (this.items.length < 1) {
|
||||
return {};
|
||||
}
|
||||
let props: any = {};
|
||||
const extras: any = {};
|
||||
if (this.type === 'list') {
|
||||
props = [];
|
||||
this.items.forEach(item => {
|
||||
let value = item.export(serialize);
|
||||
if (value === UNSET) {
|
||||
value = null;
|
||||
}
|
||||
let name = item.key as string;
|
||||
if (name && typeof name === 'string' && name.startsWith(EXTRA_KEY_PREFIX)) {
|
||||
name = name.substr(EXTRA_KEY_PREFIX.length);
|
||||
extras[name] = value;
|
||||
} else {
|
||||
props.push({
|
||||
spread: item.spread,
|
||||
name,
|
||||
value,
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.items.forEach(item => {
|
||||
let name = item.key as string;
|
||||
if (name == null) {
|
||||
// todo ...spread
|
||||
return;
|
||||
}
|
||||
let value = item.export(serialize);
|
||||
if (value === UNSET) {
|
||||
value = null;
|
||||
}
|
||||
if (typeof name === 'string' && name.startsWith(EXTRA_KEY_PREFIX)) {
|
||||
name = name.substr(EXTRA_KEY_PREFIX.length);
|
||||
extras[name] = value;
|
||||
} else {
|
||||
props[name] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return { props, extras };
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 path 路径查询属性,如果没有则临时生成一个
|
||||
*/
|
||||
query(path: string): Prop;
|
||||
/**
|
||||
* 根据 path 路径查询属性
|
||||
*
|
||||
* @useStash 如果没有则临时生成一个
|
||||
*/
|
||||
query(path: string, useStash: true): Prop;
|
||||
/**
|
||||
* 根据 path 路径查询属性
|
||||
*/
|
||||
query(path: string, useStash: false): Prop | null;
|
||||
/**
|
||||
* 根据 path 路径查询属性
|
||||
*
|
||||
* @useStash 如果没有则临时生成一个
|
||||
*/
|
||||
query(path: string, useStash: boolean = true) {
|
||||
query(path: string, useStash: boolean = true): Prop | null {
|
||||
let matchedLength = 0;
|
||||
let firstMatched = null;
|
||||
if (this.items) {
|
||||
@ -133,17 +176,7 @@ export default class Props<O = any> implements IPropParent {
|
||||
* 获取某个属性, 如果不存在,临时获取一个待写入
|
||||
* @param useStash 强制
|
||||
*/
|
||||
get(path: string, useStash: true): Prop;
|
||||
/**
|
||||
* 获取某个属性
|
||||
* @param useStash 强制
|
||||
*/
|
||||
get(path: string, useStash: false): Prop | null;
|
||||
/**
|
||||
* 获取某个属性
|
||||
*/
|
||||
get(path: string): Prop | null;
|
||||
get(name: string, useStash = false) {
|
||||
get(name: string, useStash = false): Prop | null {
|
||||
return this.maps.get(name) || (useStash && this.stash.get(name)) || null;
|
||||
}
|
||||
|
||||
|
||||
@ -56,15 +56,9 @@ export default class RootNode extends Node implements NodeParent {
|
||||
get children(): NodeChildren {
|
||||
return this._children as NodeChildren;
|
||||
}
|
||||
get props(): Props<RootNode> {
|
||||
get props(): Props {
|
||||
return this._props as any;
|
||||
}
|
||||
get extras(): Props<RootNode> {
|
||||
return this._extras as any;
|
||||
}
|
||||
get directives(): Props<RootNode> {
|
||||
return this._directives as any;
|
||||
}
|
||||
internalSetParent(parent: null) {}
|
||||
|
||||
constructor(readonly document: DocumentModel, rootSchema: RootSchema) {
|
||||
|
||||
@ -1,78 +1,96 @@
|
||||
import Node, { comparePosition } from './node/node';
|
||||
import { obx } from '@recore/obx';
|
||||
import DocumentModel from './document-model';
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
export class Selection {
|
||||
@obx.val private selected: string[] = [];
|
||||
private emitter = new EventEmitter();
|
||||
@obx.val private _selected: string[] = [];
|
||||
/**
|
||||
* 选中的节点 id
|
||||
*/
|
||||
get selected(): string[] {
|
||||
return this._selected;
|
||||
}
|
||||
|
||||
constructor(private doc: DocumentModel) {}
|
||||
constructor(readonly doc: DocumentModel) {}
|
||||
|
||||
/**
|
||||
* 选中
|
||||
*/
|
||||
select(id: string) {
|
||||
if (this.selected.length === 1 && this.selected.indexOf(id) > -1) {
|
||||
if (this._selected.length === 1 && this._selected.indexOf(id) > -1) {
|
||||
// avoid cause reaction
|
||||
return;
|
||||
}
|
||||
|
||||
this.selected = [id];
|
||||
this._selected = [id];
|
||||
this.emitter.emit('selectionchange', this._selected);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量选中
|
||||
*/
|
||||
selectAll(ids: string[]) {
|
||||
this.selected = ids;
|
||||
this._selected = ids;
|
||||
this.emitter.emit('selectionchange', this._selected);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除选中
|
||||
*/
|
||||
clear() {
|
||||
this.selected = [];
|
||||
if (this._selected.length < 1) {
|
||||
return;
|
||||
}
|
||||
this._selected = [];
|
||||
this.emitter.emit('selectionchange', this._selected);
|
||||
}
|
||||
|
||||
/**
|
||||
* 整理选中
|
||||
*/
|
||||
dispose() {
|
||||
let i = this.selected.length;
|
||||
const l = this._selected.length;
|
||||
let i = l;
|
||||
while (i-- > 0) {
|
||||
const id = this.selected[i];
|
||||
const id = this._selected[i];
|
||||
if (!this.doc.hasNode(id)) {
|
||||
this.selected.splice(i, 1);
|
||||
} else {
|
||||
this.selected[i] = id;
|
||||
this._selected.splice(i, 1);
|
||||
}
|
||||
}
|
||||
if (this._selected.length !== l) {
|
||||
this.emitter.emit('selectionchange', this._selected);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加选中
|
||||
*/
|
||||
add(id: string) {
|
||||
if (this.selected.indexOf(id) > -1) {
|
||||
if (this._selected.indexOf(id) > -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.selected.push(id);
|
||||
this._selected.push(id);
|
||||
this.emitter.emit('selectionchange', this._selected);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否选中
|
||||
*/
|
||||
has(id: string) {
|
||||
return this.selected.indexOf(id) > -1;
|
||||
return this._selected.indexOf(id) > -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除选中
|
||||
*/
|
||||
remove(id: string) {
|
||||
let i = this.selected.indexOf(id);
|
||||
let i = this._selected.indexOf(id);
|
||||
if (i > -1) {
|
||||
this.selected.splice(i, 1);
|
||||
this._selected.splice(i, 1);
|
||||
this.emitter.emit('selectionchange', this._selected);
|
||||
}
|
||||
}
|
||||
|
||||
@ -80,7 +98,7 @@ export class Selection {
|
||||
* 选区是否包含节点
|
||||
*/
|
||||
containsNode(node: Node) {
|
||||
for (const id of this.selected) {
|
||||
for (const id of this._selected) {
|
||||
const parent = this.doc.getNode(id);
|
||||
if (parent?.contains(node)) {
|
||||
return true;
|
||||
@ -94,7 +112,7 @@ export class Selection {
|
||||
*/
|
||||
getNodes() {
|
||||
const nodes = [];
|
||||
for (const id of this.selected) {
|
||||
for (const id of this._selected) {
|
||||
const node = this.doc.getNode(id);
|
||||
if (node) {
|
||||
nodes.push(node);
|
||||
@ -108,7 +126,7 @@ export class Selection {
|
||||
*/
|
||||
getTopNodes() {
|
||||
const nodes = [];
|
||||
for (const id of this.selected) {
|
||||
for (const id of this._selected) {
|
||||
const node = this.doc.getNode(id);
|
||||
if (!node) {
|
||||
continue;
|
||||
@ -134,4 +152,11 @@ export class Selection {
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
|
||||
onSelectionChange(fn: () => void): () => void {
|
||||
this.emitter.addListener('selectionchange', fn);
|
||||
return () => {
|
||||
this.emitter.removeListener('selectionchange', fn);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -204,7 +204,8 @@ export default class Dragon {
|
||||
const listenSimulators = !sourceDoc || sourceDoc === doc ? makeSimulatorListener(masterSensors) : null;
|
||||
const alwaysListen = listenSimulators ? doc : sourceDoc!;
|
||||
const designer = this.designer;
|
||||
const newBie = dragObject.type !== DragObjectType.Node;
|
||||
const newBie = !isDragNodeObject(dragObject);
|
||||
const forceCopyState = isDragNodeObject(dragObject) && dragObject.nodes.some(node => node.isSlotRoot);
|
||||
let lastSensor: ISensor | undefined;
|
||||
|
||||
this._dragging = false;
|
||||
@ -219,14 +220,19 @@ export default class Dragon {
|
||||
}
|
||||
};
|
||||
|
||||
let copy = false;
|
||||
const checkcopy = (e: MouseEvent) => {
|
||||
if (newBie) {
|
||||
return;
|
||||
}
|
||||
if (e.altKey || e.ctrlKey) {
|
||||
copy = true;
|
||||
this.setCopyState(true);
|
||||
} else {
|
||||
this.setCopyState(false);
|
||||
copy = false;
|
||||
if (!forceCopyState) {
|
||||
this.setCopyState(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -246,7 +252,7 @@ export default class Dragon {
|
||||
|
||||
const dragstart = () => {
|
||||
const locateEvent = createLocateEvent(boostEvent);
|
||||
if (newBie) {
|
||||
if (newBie || forceCopyState) {
|
||||
this.setCopyState(true);
|
||||
} else {
|
||||
chooseSensor(locateEvent);
|
||||
@ -282,7 +288,6 @@ export default class Dragon {
|
||||
lastSensor.deactiveSensor();
|
||||
}
|
||||
this.setNativeSelection(true);
|
||||
const copy = !newBie && this.isCopyState();
|
||||
this.clearState();
|
||||
|
||||
let exception;
|
||||
@ -414,6 +419,7 @@ export default class Dragon {
|
||||
private getMasterSensors(): ISimulator[] {
|
||||
return this.designer.project.documents
|
||||
.map(doc => {
|
||||
// TODO: not use actived,
|
||||
if (doc.actived && doc.simulator?.sensorAvailable) {
|
||||
return doc.simulator;
|
||||
}
|
||||
|
||||
@ -1 +1,175 @@
|
||||
// todo
|
||||
import { EventEmitter } from 'events';
|
||||
import Session from './session';
|
||||
import { autorun, Reaction, untracked } from '@recore/obx';
|
||||
import { NodeSchema } from '../schema';
|
||||
|
||||
// TODO: cache to localStorage
|
||||
|
||||
export interface Serialization<T = any> {
|
||||
serialize(data: NodeSchema): T;
|
||||
unserialize(data: T): NodeSchema;
|
||||
}
|
||||
|
||||
let currentSerializion: Serialization<any> = {
|
||||
serialize(data: NodeSchema): string {
|
||||
return JSON.stringify(data);
|
||||
},
|
||||
unserialize(data: string) {
|
||||
return JSON.parse(data);
|
||||
},
|
||||
};
|
||||
|
||||
export function setSerialization(serializion: Serialization) {
|
||||
currentSerializion = serializion;
|
||||
}
|
||||
|
||||
export default class History {
|
||||
private session: Session;
|
||||
private records: Session[];
|
||||
private point: number = 0;
|
||||
private emitter = new EventEmitter();
|
||||
private obx: Reaction;
|
||||
private justWokeup: boolean = false;
|
||||
|
||||
constructor(
|
||||
logger: () => any,
|
||||
private redoer: (data: NodeSchema) => void,
|
||||
private timeGap: number = 1000,
|
||||
) {
|
||||
this.session = new Session(0, null, this.timeGap);
|
||||
this.records = [this.session];
|
||||
|
||||
this.obx = autorun(() => {
|
||||
const data = logger();
|
||||
console.info('log');
|
||||
if (this.justWokeup) {
|
||||
this.justWokeup = false;
|
||||
return;
|
||||
}
|
||||
untracked(() => {
|
||||
const log = currentSerializion.serialize(data);
|
||||
if (this.session.cursor === 0 && this.session.isActive()) {
|
||||
this.session.log(log);
|
||||
this.session.end();
|
||||
} else if (this.session) {
|
||||
if (this.session.isActive()) {
|
||||
this.session.log(log);
|
||||
} else {
|
||||
this.session.end();
|
||||
const cursor = this.session.cursor + 1;
|
||||
const session = new Session(cursor, log, this.timeGap);
|
||||
this.session = session;
|
||||
this.records.splice(cursor, this.records.length - cursor, session);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, true).$obx;
|
||||
}
|
||||
|
||||
get hotData() {
|
||||
return this.session.data;
|
||||
}
|
||||
|
||||
isSavePoint(): boolean {
|
||||
return this.point !== this.session.cursor;
|
||||
}
|
||||
|
||||
go(cursor: number) {
|
||||
this.session.end();
|
||||
|
||||
const currentCursor = this.session.cursor;
|
||||
cursor = +cursor;
|
||||
if (cursor < 0) {
|
||||
cursor = 0;
|
||||
} else if (cursor >= this.records.length) {
|
||||
cursor = this.records.length - 1;
|
||||
}
|
||||
if (cursor === currentCursor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const session = this.records[cursor];
|
||||
const hotData = session.data;
|
||||
|
||||
this.obx.sleep();
|
||||
try {
|
||||
this.redoer(currentSerializion.unserialize(hotData));
|
||||
this.emitter.emit('cursor', hotData);
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
|
||||
this.justWokeup = true;
|
||||
this.obx.wakeup();
|
||||
this.session = session;
|
||||
|
||||
this.emitter.emit('statechange', this.getState());
|
||||
}
|
||||
|
||||
back() {
|
||||
if (!this.session) {
|
||||
return;
|
||||
}
|
||||
const cursor = this.session.cursor - 1;
|
||||
this.go(cursor);
|
||||
}
|
||||
|
||||
forward() {
|
||||
if (!this.session) {
|
||||
return;
|
||||
}
|
||||
const cursor = this.session.cursor + 1;
|
||||
this.go(cursor);
|
||||
}
|
||||
|
||||
savePoint() {
|
||||
if (!this.session) {
|
||||
return;
|
||||
}
|
||||
this.session.end();
|
||||
this.point = this.session.cursor;
|
||||
this.emitter.emit('statechange', this.getState());
|
||||
}
|
||||
|
||||
/**
|
||||
* | 1 | 1 | 1 |
|
||||
* | -------- | -------- | -------- |
|
||||
* | modified | redoable | undoable |
|
||||
*/
|
||||
getState(): number {
|
||||
const cursor = this.session.cursor;
|
||||
let state = 7;
|
||||
// undoable ?
|
||||
if (cursor <= 0) {
|
||||
state -= 1;
|
||||
}
|
||||
// redoable ?
|
||||
if (cursor >= this.records.length - 1) {
|
||||
state -= 2;
|
||||
}
|
||||
// modified ?
|
||||
if (this.point === cursor) {
|
||||
state -= 4;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
onStateChange(func: () => any) {
|
||||
this.emitter.on('statechange', func);
|
||||
return () => {
|
||||
this.emitter.removeListener('statechange', func);
|
||||
};
|
||||
}
|
||||
|
||||
onCursor(func: () => any) {
|
||||
this.emitter.on('cursor', func);
|
||||
return () => {
|
||||
this.emitter.removeListener('cursor', func);
|
||||
};
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.emitter.removeAllListeners();
|
||||
this.records = [];
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,6 +24,12 @@ export default class Hovering {
|
||||
this._current = node;
|
||||
}
|
||||
|
||||
unhover(node: Node) {
|
||||
if (this._current === node) {
|
||||
this._current = null;
|
||||
}
|
||||
}
|
||||
|
||||
leave(document: DocumentModel) {
|
||||
if (this.current && this.current.document === document) {
|
||||
this._current = null;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { obx, computed } from '@recore/obx';
|
||||
import { INodeSelector, IViewport } from '../simulator';
|
||||
import { uniqueId } from '../../utils/unique-id';
|
||||
import { uniqueId } from '../../../../utils/unique-id';
|
||||
|
||||
export default class OffsetObserver {
|
||||
readonly id = uniqueId('oobx');
|
||||
|
||||
44
packages/designer/src/designer/helper/session.ts
Normal file
44
packages/designer/src/designer/helper/session.ts
Normal file
@ -0,0 +1,44 @@
|
||||
export default class Session {
|
||||
private _data: any;
|
||||
private activedTimer: any;
|
||||
|
||||
get data() {
|
||||
return this._data;
|
||||
}
|
||||
|
||||
constructor(readonly cursor: number, data: any, private timeGap: number = 1000) {
|
||||
this.setTimer();
|
||||
this.log(data);
|
||||
}
|
||||
|
||||
log(data: any) {
|
||||
if (!this.isActive()) {
|
||||
return;
|
||||
}
|
||||
this._data = data;
|
||||
this.setTimer();
|
||||
}
|
||||
|
||||
isActive() {
|
||||
return this.activedTimer != null;
|
||||
}
|
||||
|
||||
end() {
|
||||
if (this.isActive()) {
|
||||
this.clearTimer();
|
||||
console.info('session end');
|
||||
}
|
||||
}
|
||||
|
||||
private setTimer() {
|
||||
this.clearTimer();
|
||||
this.activedTimer = setTimeout(() => this.end(), this.timeGap);
|
||||
}
|
||||
|
||||
private clearTimer() {
|
||||
if (this.activedTimer) {
|
||||
clearTimeout(this.activedTimer);
|
||||
}
|
||||
this.activedTimer = null;
|
||||
}
|
||||
}
|
||||
@ -8,7 +8,6 @@ export default class ProjectView extends Component<{ designer: Designer }> {
|
||||
render() {
|
||||
const { designer } = this.props;
|
||||
// TODO: support splitview
|
||||
console.info(designer.project.documents);
|
||||
return (
|
||||
<div className="lc-project">
|
||||
{designer.project.documents.map(doc => {
|
||||
|
||||
@ -27,8 +27,8 @@ export default class Project {
|
||||
});
|
||||
}
|
||||
|
||||
@computed get activedDocuments() {
|
||||
return this.documents.filter(doc => doc.actived);
|
||||
@computed get currentDocument() {
|
||||
return this.documents.find(doc => doc.actived);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -106,14 +106,12 @@ export default class Project {
|
||||
}
|
||||
|
||||
checkExclusive(actived: DocumentModel) {
|
||||
if (this.canvasDisplayMode !== 'exclusive') {
|
||||
return;
|
||||
}
|
||||
this.documents.forEach((doc) => {
|
||||
if (doc !== actived) {
|
||||
doc.suspense();
|
||||
}
|
||||
});
|
||||
this.emitter.emit('current-document-change', actived);
|
||||
}
|
||||
|
||||
closeOthers(opened: DocumentModel) {
|
||||
@ -124,7 +122,14 @@ export default class Project {
|
||||
});
|
||||
}
|
||||
|
||||
onCurrentDocumentChange(fn: (doc: DocumentModel) => void): () => void {
|
||||
this.emitter.on('current-document-change', fn);
|
||||
return () => {
|
||||
this.emitter.removeListener('current-document-change', fn);
|
||||
};
|
||||
}
|
||||
// 通知标记删除,需要告知服务端
|
||||
// 项目角度编辑不是全量打开所有文档,是按需加载,哪个更新就通知更新谁,
|
||||
// 哪个删除就
|
||||
|
||||
}
|
||||
|
||||
@ -90,15 +90,14 @@ export type PropsList = Array<{
|
||||
|
||||
export type NodeData = NodeSchema | JSExpression | DOMText;
|
||||
|
||||
export interface JSExpression {
|
||||
type: 'JSExpression';
|
||||
value: string;
|
||||
}
|
||||
|
||||
export function isJSExpression(data: any): data is JSExpression {
|
||||
return data && data.type === 'JSExpression';
|
||||
}
|
||||
|
||||
export function isJSSlot(data: any): data is JSSlot {
|
||||
return data && data.type === 'JSSlot';
|
||||
}
|
||||
|
||||
export function isDOMText(data: any): data is DOMText {
|
||||
return typeof data === 'string';
|
||||
}
|
||||
@ -106,7 +105,7 @@ export function isDOMText(data: any): data is DOMText {
|
||||
export type DOMText = string;
|
||||
|
||||
export interface RootSchema extends NodeSchema {
|
||||
componentName: 'Block' | 'Page' | 'Component';
|
||||
componentName: string; // 'Block' | 'Page' | 'Component';
|
||||
fileName: string;
|
||||
meta?: object;
|
||||
state?: {
|
||||
@ -121,7 +120,7 @@ export interface RootSchema extends NodeSchema {
|
||||
css?: string;
|
||||
dataSource?: {
|
||||
items: DataSourceConfig[];
|
||||
};
|
||||
} | any;
|
||||
defaultProps?: CompositeObject;
|
||||
}
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ import { LocateEvent, ISensor } from './helper/dragon';
|
||||
import { Point } from './helper/location';
|
||||
import Node from './document/node/node';
|
||||
import { ScrollTarget, IScrollable } from './helper/scroller';
|
||||
import { ComponentDescriptionSpec } from './component-config';
|
||||
import { ComponentDescription } from './component-type';
|
||||
|
||||
export type AutoFit = '100%';
|
||||
export const AutoFit = '100%';
|
||||
@ -117,7 +117,7 @@ export interface ISimulator<P = object> extends ISensor {
|
||||
/**
|
||||
* 描述组件
|
||||
*/
|
||||
describeComponent(component: Component): ComponentDescriptionSpec;
|
||||
describeComponent(component: Component): ComponentDescription;
|
||||
/**
|
||||
* 根据组件信息获取组件类
|
||||
*/
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { isPlainObject } from './is-plain-object';
|
||||
import { isPlainObject } from '../../../utils/is-plain-object';
|
||||
|
||||
export function cloneDeep(src: any): any {
|
||||
const type = typeof src;
|
||||
|
||||
@ -4,6 +4,6 @@
|
||||
"experimentalDecorators": true
|
||||
},
|
||||
"include": [
|
||||
"./src/"
|
||||
"./src/", "../utils/unique-id.ts", "../utils/is-plain-object.ts", "../utils/is-object.ts", "../utils/is-function.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@ -1 +0,0 @@
|
||||
事件面板
|
||||
3
packages/plugin-setters/number-setter.tsx
Normal file
3
packages/plugin-setters/number-setter.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
import { NumberPicker } from '@alifd/next';
|
||||
|
||||
export default NumberPicker;
|
||||
5
packages/plugin-setters/package.json
Normal file
5
packages/plugin-setters/package.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@alifd/next": "^1.19.16"
|
||||
}
|
||||
}
|
||||
6
packages/plugin-settings-pane/.eslintignore
Normal file
6
packages/plugin-settings-pane/.eslintignore
Normal file
@ -0,0 +1,6 @@
|
||||
.idea/
|
||||
.vscode/
|
||||
build/
|
||||
.*
|
||||
~*
|
||||
node_modules
|
||||
3
packages/plugin-settings-pane/.eslintrc
Normal file
3
packages/plugin-settings-pane/.eslintrc
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "./node_modules/@recore/config/.eslintrc"
|
||||
}
|
||||
6
packages/plugin-settings-pane/.prettierrc
Normal file
6
packages/plugin-settings-pane/.prettierrc
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"printWidth": 120,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
43
packages/plugin-settings-pane/package.json
Normal file
43
packages/plugin-settings-pane/package.json
Normal file
@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "@ali/lowcode-plugin-settings-pane",
|
||||
"version": "0.0.0",
|
||||
"description": "xxx for Ali lowCode engine",
|
||||
"main": "src/index.tsx",
|
||||
"files": [
|
||||
"lib"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"test": "ava",
|
||||
"test:snapshot": "ava --update-snapshots"
|
||||
},
|
||||
"dependencies": {
|
||||
"@alifd/next": "^1.19.16",
|
||||
"classnames": "^2.2.6",
|
||||
"react": "^16",
|
||||
"react-dom": "^16.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@recore/config": "^2.0.0",
|
||||
"@types/classnames": "^2.2.7",
|
||||
"@types/node": "^13.7.1",
|
||||
"@types/react": "^16",
|
||||
"@types/react-dom": "^16",
|
||||
"eslint": "^6.5.1",
|
||||
"prettier": "^1.18.2",
|
||||
"tslib": "^1.9.3",
|
||||
"typescript": "^3.1.3",
|
||||
"ts-node": "^8.0.1"
|
||||
},
|
||||
"ava": {
|
||||
"compileEnhancements": false,
|
||||
"snapshotDir": "test/fixtures/__snapshots__",
|
||||
"extensions": [
|
||||
"ts"
|
||||
],
|
||||
"require": [
|
||||
"ts-node/register"
|
||||
]
|
||||
},
|
||||
"license": "MIT"
|
||||
}
|
||||
@ -0,0 +1,233 @@
|
||||
import { Component } from 'react';
|
||||
import { Icon, Button, Message } from '@alifd/next';
|
||||
import Sortable from './sortable';
|
||||
import { SettingField, SetterType } from '../../main';
|
||||
import './style.less';
|
||||
import { createSettingFieldView } from '../../settings-pane';
|
||||
|
||||
interface ArraySetterState {
|
||||
items: SettingField[];
|
||||
itemsMap: Map<string | number, SettingField>;
|
||||
prevLength: number;
|
||||
}
|
||||
export class ListSetter extends Component<
|
||||
{
|
||||
value: any[];
|
||||
field: SettingField;
|
||||
itemConfig?: {
|
||||
setter?: SetterType;
|
||||
defaultValue?: any | ((field: SettingField, editor: any) => any);
|
||||
required?: boolean;
|
||||
};
|
||||
multiValue?: boolean;
|
||||
},
|
||||
ArraySetterState
|
||||
> {
|
||||
static getDerivedStateFromProps(props: any, state: ArraySetterState) {
|
||||
const { value, field } = props;
|
||||
const newLength = value && Array.isArray(value) ? value.length : 0;
|
||||
if (state && state.prevLength === newLength) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// props value length change will go here
|
||||
const originLength = state ? state.items.length : 0;
|
||||
if (state && originLength === newLength) {
|
||||
return {
|
||||
prevLength: newLength,
|
||||
};
|
||||
}
|
||||
|
||||
const itemsMap = state ? state.itemsMap : new Map<string | number, SettingField>();
|
||||
let items = state ? state.items.slice() : [];
|
||||
if (newLength > originLength) {
|
||||
for (let i = originLength; i < newLength; i++) {
|
||||
const item = field.createField({
|
||||
...props.itemConfig,
|
||||
name: i,
|
||||
forceInline: 1,
|
||||
});
|
||||
items[i] = item;
|
||||
itemsMap.set(item.id, item);
|
||||
}
|
||||
} else if (newLength < originLength) {
|
||||
const deletes = items.splice(newLength);
|
||||
deletes.forEach(item => {
|
||||
itemsMap.delete(item.id);
|
||||
});
|
||||
}
|
||||
return {
|
||||
items,
|
||||
itemsMap,
|
||||
prevLength: newLength,
|
||||
};
|
||||
}
|
||||
|
||||
onSort(sortedIds: Array<string | number>) {
|
||||
const { itemsMap } = this.state;
|
||||
const items = sortedIds.map((id, index) => {
|
||||
const item = itemsMap.get(id)!;
|
||||
item.setKey(index);
|
||||
return item;
|
||||
});
|
||||
this.setState({
|
||||
items,
|
||||
});
|
||||
}
|
||||
|
||||
private scrollToLast: boolean = false;
|
||||
onAdd() {
|
||||
const { items, itemsMap } = this.state;
|
||||
const { itemConfig } = this.props;
|
||||
const defaultValue = itemConfig ? itemConfig.defaultValue : null;
|
||||
const item = this.props.field.createField({
|
||||
...itemConfig,
|
||||
name: items.length,
|
||||
forceInline: 1,
|
||||
});
|
||||
items.push(item);
|
||||
itemsMap.set(item.id, item);
|
||||
item.setValue(typeof defaultValue === 'function' ? defaultValue(item, item.editor) : defaultValue);
|
||||
this.scrollToLast = true;
|
||||
this.setState({
|
||||
items: items.slice(),
|
||||
});
|
||||
}
|
||||
|
||||
onRemove(field: SettingField) {
|
||||
const { items } = this.state;
|
||||
let i = items.indexOf(field);
|
||||
if (i < 0) {
|
||||
return;
|
||||
}
|
||||
items.splice(i, 1);
|
||||
const l = items.length;
|
||||
while (i < l) {
|
||||
items[i].setKey(i);
|
||||
i++;
|
||||
}
|
||||
field.remove();
|
||||
this.setState({ items: items.slice() });
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.state.items.forEach(field => {
|
||||
field.purge();
|
||||
});
|
||||
}
|
||||
|
||||
shouldComponentUpdate(_: any, nextState: ArraySetterState) {
|
||||
if (nextState.items !== this.state.items) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
render() {
|
||||
// mini Button: depends popup
|
||||
if (this.props.itemConfig) {
|
||||
// check is ObjectSetter then check if show columns
|
||||
}
|
||||
|
||||
const { items } = this.state;
|
||||
const scrollToLast = this.scrollToLast;
|
||||
this.scrollToLast = false;
|
||||
const lastIndex = items.length - 1;
|
||||
|
||||
return (
|
||||
<div className="lc-setter-list lc-block-setter">
|
||||
<div className="lc-block-setter-actions">
|
||||
<Button size="medium" onClick={this.onAdd.bind(this)}>
|
||||
<Icon type="add" />
|
||||
<span>添加</span>
|
||||
</Button>
|
||||
</div>
|
||||
{this.props.multiValue && <Message type="warning">当前选择多个节点,且值不一致</Message>}
|
||||
{items.length > 0 ? (
|
||||
<div className="lc-setter-list-scroll-body">
|
||||
<Sortable itemClassName="lc-setter-list-card" onSort={this.onSort.bind(this)}>
|
||||
{items.map((field, index) => (
|
||||
<ArrayItem
|
||||
key={field.id}
|
||||
scrollIntoView={scrollToLast && index === lastIndex}
|
||||
field={field}
|
||||
onRemove={this.onRemove.bind(this, field)}
|
||||
/>
|
||||
))}
|
||||
</Sortable>
|
||||
</div>
|
||||
) : (
|
||||
<div className="lc-setter-list-empty">
|
||||
列表为空
|
||||
<Button size="small" onClick={this.onAdd.bind(this)}>
|
||||
<Icon type="add" />
|
||||
<span>添加</span>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ArrayItem extends Component<{
|
||||
field: SettingField;
|
||||
onRemove: () => void;
|
||||
scrollIntoView: boolean;
|
||||
}> {
|
||||
shouldComponentUpdate() {
|
||||
return false;
|
||||
}
|
||||
private shell?: HTMLDivElement | null;
|
||||
componentDidMount() {
|
||||
if (this.props.scrollIntoView && this.shell) {
|
||||
this.shell.parentElement!.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
}
|
||||
}
|
||||
render() {
|
||||
const { onRemove, field } = this.props;
|
||||
return (
|
||||
<div className="lc-listitem" ref={ref => (this.shell = ref)}>
|
||||
<div draggable className="lc-listitem-handler">
|
||||
<Icon type="ellipsis" size="small" />
|
||||
</div>
|
||||
<div className="lc-listitem-body">{createSettingFieldView(field, field.parent)}</div>
|
||||
<div className="lc-listitem-actions">
|
||||
<div className="lc-listitem-action" onClick={onRemove}>
|
||||
<Icon type="ashbin" size="small" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TableSetter extends ListSetter {
|
||||
|
||||
}
|
||||
|
||||
export default class ArraySetter extends Component<
|
||||
{
|
||||
value: any[];
|
||||
field: SettingField;
|
||||
itemConfig?: {
|
||||
setter?: SetterType;
|
||||
defaultValue?: any | ((field: SettingField, editor: any) => any);
|
||||
required?: boolean;
|
||||
};
|
||||
mode?: 'popup' | 'list' | 'table';
|
||||
forceInline?: boolean;
|
||||
multiValue?: boolean;
|
||||
}> {
|
||||
render() {
|
||||
const { mode, forceInline, ...props } = this.props;
|
||||
if (mode === 'popup' || forceInline) {
|
||||
// todo popup
|
||||
return <Button>编辑数组</Button>;
|
||||
} else if (mode === 'table') {
|
||||
return <TableSetter {...props} />;
|
||||
} else {
|
||||
return <ListSetter {...props} />;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
.lc-sortable {
|
||||
position: relative;
|
||||
|
||||
.lc-sortable-card {
|
||||
box-sizing: border-box;
|
||||
&:after, &:before {
|
||||
content: "";
|
||||
display: table;
|
||||
}
|
||||
&:after {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
&.lc-dragging {
|
||||
outline: 2px dashed var(--color-brand);
|
||||
outline-offset: -2px;
|
||||
> * {
|
||||
visibility: hidden;
|
||||
}
|
||||
border-color: transparent !important;
|
||||
box-shadow: none !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
}
|
||||
|
||||
[draggable] {
|
||||
cursor: ns-resize;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,220 @@
|
||||
import { Component, Children, ReactElement } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import './sortable.less';
|
||||
|
||||
class Sortable extends Component<{
|
||||
className?: string;
|
||||
itemClassName?: string;
|
||||
onSort?: (sortedIds: Array<string | number>) => void;
|
||||
dragImageSourceHandler?: (elem: Element) => Element;
|
||||
children: ReactElement[];
|
||||
}> {
|
||||
private shell?: HTMLDivElement | null;
|
||||
private items?: Array<string | number>;
|
||||
private willDetach?: () => void;
|
||||
componentDidMount() {
|
||||
const box = this.shell!;
|
||||
|
||||
let isDragEnd: boolean = false;
|
||||
|
||||
/**
|
||||
* target node to be dragged
|
||||
*/
|
||||
let source: Element | null;
|
||||
|
||||
/**
|
||||
* node to be placed
|
||||
*/
|
||||
let ref: Element | null;
|
||||
|
||||
/**
|
||||
* next sibling of the source node
|
||||
*/
|
||||
let origRef: Element | null;
|
||||
|
||||
/**
|
||||
* accurately locate the node from event
|
||||
*/
|
||||
const locate = (e: DragEvent) => {
|
||||
let y = e.clientY;
|
||||
if (e.view !== window && e.view!.frameElement) {
|
||||
y += e.view!.frameElement.getBoundingClientRect().top;
|
||||
}
|
||||
let node = box.firstElementChild as HTMLDivElement;
|
||||
while (node) {
|
||||
if (node !== source && node.dataset.id) {
|
||||
const rect = node.getBoundingClientRect();
|
||||
|
||||
if (rect.height <= 0) continue;
|
||||
if (y < rect.top + rect.height / 2) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
node = node.nextElementSibling as HTMLDivElement;
|
||||
}
|
||||
return node;
|
||||
};
|
||||
|
||||
/**
|
||||
* find the source node
|
||||
*/
|
||||
const getSource = (e: DragEvent) => {
|
||||
const target = e.target as Element;
|
||||
if (!target || !box.contains(target) || target === box) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let node = box.firstElementChild;
|
||||
while (node) {
|
||||
if (node.contains(target)) {
|
||||
return node;
|
||||
}
|
||||
node = node.nextElementSibling;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const sort = (beforeId: string | number | null | undefined) => {
|
||||
if (!source) return;
|
||||
|
||||
const sourceId = (source as HTMLDivElement).dataset.id;
|
||||
const items = this.items!;
|
||||
const origIndex = items.findIndex(id => id == sourceId);
|
||||
|
||||
let newIndex = beforeId ? items.findIndex(id => id == beforeId) : items.length;
|
||||
|
||||
if (origIndex < 0 || newIndex < 0) return;
|
||||
if (this.props.onSort) {
|
||||
if (newIndex > origIndex) {
|
||||
newIndex -= 1;
|
||||
}
|
||||
if (origIndex === newIndex) return;
|
||||
const item = items.splice(origIndex, 1);
|
||||
items.splice(newIndex, 0, item[0]);
|
||||
|
||||
this.props.onSort(items);
|
||||
}
|
||||
};
|
||||
|
||||
const dragstart = (e: DragEvent) => {
|
||||
isDragEnd = false;
|
||||
source = getSource(e);
|
||||
if (!source) {
|
||||
return false;
|
||||
}
|
||||
origRef = source.nextElementSibling;
|
||||
const rect = source.getBoundingClientRect();
|
||||
let dragSource = source;
|
||||
if (this.props.dragImageSourceHandler) {
|
||||
dragSource = this.props.dragImageSourceHandler(source);
|
||||
}
|
||||
if (e.dataTransfer) {
|
||||
e.dataTransfer.setDragImage(dragSource, e.clientX - rect.left, e.clientY - rect.top);
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
try {
|
||||
e.dataTransfer.setData('application/json', {} as any);
|
||||
} catch (ex) {
|
||||
// eslint-disable-line
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
source!.classList.add('lc-dragging');
|
||||
}, 0);
|
||||
return true;
|
||||
};
|
||||
|
||||
const placeAt = (beforeRef: Element | null) => {
|
||||
if (beforeRef) {
|
||||
if (beforeRef !== source) {
|
||||
box.insertBefore(source!, beforeRef);
|
||||
}
|
||||
} else {
|
||||
box.appendChild(source!);
|
||||
}
|
||||
};
|
||||
|
||||
const adjust = (e: DragEvent) => {
|
||||
if (isDragEnd) return;
|
||||
ref = locate(e);
|
||||
placeAt(ref);
|
||||
};
|
||||
|
||||
let lastDragEvent: DragEvent | null;
|
||||
const drag = (e: DragEvent) => {
|
||||
if (!source) return;
|
||||
e.preventDefault();
|
||||
if (lastDragEvent) {
|
||||
if (lastDragEvent.clientX === e.clientX && lastDragEvent.clientY === e.clientY) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
lastDragEvent = e;
|
||||
if (e.dataTransfer) {
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
}
|
||||
adjust(e);
|
||||
};
|
||||
|
||||
const dragend = (e: DragEvent) => {
|
||||
isDragEnd = true;
|
||||
if (!source) return;
|
||||
e.preventDefault();
|
||||
source.classList.remove('lc-dragging');
|
||||
placeAt(origRef);
|
||||
sort(ref ? (ref as HTMLDivElement).dataset.id : null);
|
||||
source = null;
|
||||
ref = null;
|
||||
origRef = null;
|
||||
lastDragEvent = null;
|
||||
};
|
||||
|
||||
box.addEventListener('dragstart', dragstart);
|
||||
document.addEventListener('dragover', drag);
|
||||
document.addEventListener('drag', drag);
|
||||
document.addEventListener('dragend', dragend);
|
||||
|
||||
this.willDetach = () => {
|
||||
box.removeEventListener('dragstart', dragstart);
|
||||
document.removeEventListener('dragover', drag);
|
||||
document.removeEventListener('drag', drag);
|
||||
document.removeEventListener('dragend', dragend);
|
||||
};
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.willDetach) {
|
||||
this.willDetach();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className, itemClassName, children } = this.props;
|
||||
const items: Array<string | number> = [];
|
||||
const cards = Children.map(children, child => {
|
||||
const id = child.key!;
|
||||
items.push(id);
|
||||
return (
|
||||
<div key={id} data-id={id} className={classNames('lc-sortable-card', itemClassName)}>
|
||||
{child}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
this.items = items;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('lc-sortable', className)}
|
||||
ref={ref => {
|
||||
this.shell = ref;
|
||||
}}
|
||||
>
|
||||
{cards}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Sortable;
|
||||
@ -0,0 +1,72 @@
|
||||
.lc-setter-list {
|
||||
[draggable] {
|
||||
cursor: move;
|
||||
}
|
||||
color: var(--color-text);
|
||||
|
||||
.next-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
line-height: 1 !important;
|
||||
}
|
||||
.lc-setter-list-empty {
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
.next-btn {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.lc-setter-list-scroll-body {
|
||||
margin: -8px -5px;
|
||||
padding: 8px 10px;
|
||||
overflow-y: auto;
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
.lc-setter-list-card {
|
||||
border: 1px solid rgba(31,56,88,.2);
|
||||
background-color: var(--color-block-background-light);
|
||||
border-radius: 3px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.lc-listitem {
|
||||
position: relative;
|
||||
outline: none;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
height: 32px;
|
||||
|
||||
.lc-listitem-actions {
|
||||
margin: 0 3px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
.lc-listitem-action {
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
.lc-listitem-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.lc-listitem-handler {
|
||||
margin-left: 2px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
.next-icon-ellipsis {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
import { Component } from "react";
|
||||
import { FieldConfig, SettingField } from '../../main';
|
||||
|
||||
class ObjectSetter extends Component<{
|
||||
mode?: 'popup' | 'row' | 'form';
|
||||
forceInline?: number;
|
||||
}> {
|
||||
render() {
|
||||
const { mode, forceInline = 0 } = this.props;
|
||||
if (forceInline || (mode === 'popup' || mode === 'row')) {
|
||||
if (forceInline < 2 || mode === 'row') {
|
||||
// row
|
||||
} else {
|
||||
// popup
|
||||
}
|
||||
} else {
|
||||
// form
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface ObjectSetterConfig {
|
||||
items?: FieldConfig[];
|
||||
extraConfig?: {
|
||||
setter?: SetterType;
|
||||
defaultValue?: any | ((field: SettingField, editor: any) => any);
|
||||
};
|
||||
}
|
||||
|
||||
// for table|list row
|
||||
class RowSetter extends Component<{
|
||||
decriptor?: string | ((rowField: SettingField) => string);
|
||||
config: ObjectSetterConfig;
|
||||
columnsLimit?: number;
|
||||
}> {
|
||||
render() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// form-field setter
|
||||
class FormSetter extends Component<{}> {
|
||||
|
||||
}
|
||||
137
packages/plugin-settings-pane/src/field/index.less
Normal file
137
packages/plugin-settings-pane/src/field/index.less
Normal file
@ -0,0 +1,137 @@
|
||||
@import '../variables.less';
|
||||
|
||||
@x-gap: 10px;
|
||||
@y-gap: 8px;
|
||||
|
||||
.lc-field {
|
||||
// head
|
||||
.lc-field-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.lc-field-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.lc-field-icon {
|
||||
margin-right: @x-gap;
|
||||
transform-origin: center;
|
||||
transition: transform 0.1s;
|
||||
}
|
||||
}
|
||||
|
||||
&.lc-inline-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
// for top-level style
|
||||
padding: 8px 10px;
|
||||
|
||||
> .lc-field-head {
|
||||
width: 70px;
|
||||
margin-right: 1px;
|
||||
.lc-title-label {
|
||||
width: 70px;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
> .lc-field-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
&.lc-block-field, &.lc-accordion-field {
|
||||
display: block;
|
||||
&:not(:first-child) {
|
||||
border-top: 1px solid var(--color-line-normal, @dark-alpha-2);
|
||||
}
|
||||
> .lc-field-head {
|
||||
padding-left: @x-gap;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: 500;
|
||||
background: var(--color-block-background-shallow, rgba(31,56,88,.06));
|
||||
border-bottom: 1px solid var(--color-line-normal, @dark-alpha-2);
|
||||
color: var(--color-title, @white-alpha-2);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
> .lc-field-body {
|
||||
padding: @y-gap @x-gap/2;
|
||||
}
|
||||
|
||||
+ .lc-inline-field {
|
||||
border-top: 1px solid var(--color-line-normal, @dark-alpha-2);
|
||||
}
|
||||
}
|
||||
|
||||
&.lc-block-field {
|
||||
position: relative;
|
||||
>.lc-field-body>.lc-block-setter>.lc-block-setter-actions {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 0;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
&.lc-accordion-field {
|
||||
// collapsed
|
||||
&.lc-field-is-collapsed {
|
||||
> .lc-field-head .lc-field-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
> .lc-field-body {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// 邻近的保持上下距离
|
||||
+ .lc-field {
|
||||
margin-top: @y-gap;
|
||||
}
|
||||
}
|
||||
|
||||
// 2rd level reset
|
||||
.lc-field-body {
|
||||
.lc-inline-field {
|
||||
padding: @y-gap @x-gap/2 0 @x-gap/2;
|
||||
&:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
+ .lc-accordion-field, +.lc-block-field {
|
||||
margin-top: @y-gap;
|
||||
}
|
||||
}
|
||||
|
||||
.lc-field {
|
||||
border-top: none !important;
|
||||
}
|
||||
|
||||
.lc-accordion-field, .lc-block-field {
|
||||
> .lc-field-head {
|
||||
padding-left: @x-gap/2;
|
||||
background: var(--color-block-background-light);
|
||||
border-bottom-color: var(--color-line-light);
|
||||
> .lc-field-icon {
|
||||
margin-right: @x-gap/2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3rd level field title width should short
|
||||
.lc-field-body .lc-inline-field {
|
||||
> .lc-field-head {
|
||||
width: 50px;
|
||||
.lc-title-label {
|
||||
width: 50px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
94
packages/plugin-settings-pane/src/field/index.tsx
Normal file
94
packages/plugin-settings-pane/src/field/index.tsx
Normal file
@ -0,0 +1,94 @@
|
||||
import { Component } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { Icon } from '@alifd/next';
|
||||
import Title, { TitleContent } from '../title';
|
||||
import './index.less';
|
||||
|
||||
export interface FieldProps {
|
||||
className?: string;
|
||||
// span
|
||||
title?: TitleContent | null;
|
||||
}
|
||||
|
||||
export class Field extends Component<FieldProps> {
|
||||
private shell: HTMLDivElement | null = null;
|
||||
|
||||
private checkIsBlockField() {
|
||||
if (this.shell) {
|
||||
const setter = this.shell.lastElementChild!.firstElementChild;
|
||||
if (setter && setter.classList.contains('lc-block-setter')) {
|
||||
this.shell.classList.add('lc-block-field');
|
||||
this.shell.classList.remove('lc-inline-field');
|
||||
} else {
|
||||
this.shell.classList.remove('lc-block-field');
|
||||
this.shell.classList.add('lc-inline-field');
|
||||
}
|
||||
}
|
||||
}
|
||||
componentDidUpdate() {
|
||||
this.checkIsBlockField();
|
||||
}
|
||||
componentDidMount() {
|
||||
this.checkIsBlockField();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className, children, title } = this.props;
|
||||
|
||||
return (
|
||||
<div ref={shell => (this.shell = shell)} className={classNames('lc-field lc-inline-field', className)}>
|
||||
{title && (
|
||||
<div className="lc-field-head">
|
||||
<div className="lc-field-title">
|
||||
<Title title={title} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="lc-field-body">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export interface FieldGroupProps extends FieldProps {
|
||||
defaultCollapsed?: boolean;
|
||||
// gap?: number;
|
||||
onExpandChange?: (collapsed: boolean) => void;
|
||||
}
|
||||
|
||||
export class FieldGroup extends Component<FieldGroupProps> {
|
||||
state = {
|
||||
collapsed: this.props.defaultCollapsed,
|
||||
};
|
||||
|
||||
toggleExpand() {
|
||||
const { onExpandChange } = this.props;
|
||||
const collapsed = !this.state.collapsed;
|
||||
this.setState({
|
||||
collapsed,
|
||||
});
|
||||
onExpandChange && onExpandChange(collapsed);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className, children, title } = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('lc-field lc-accordion-field', className, {
|
||||
'lc-field-is-collapsed': this.state.collapsed,
|
||||
})}
|
||||
>
|
||||
{title && (
|
||||
<div className="lc-field-head" onClick={this.toggleExpand.bind(this)}>
|
||||
<div className="lc-field-title">
|
||||
<Title title={title} />
|
||||
</div>
|
||||
<Icon className="lc-field-icon" type="arrow-up" size="xs" />
|
||||
</div>
|
||||
)}
|
||||
<div className="lc-field-body">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
128
packages/plugin-settings-pane/src/index.tsx
Normal file
128
packages/plugin-settings-pane/src/index.tsx
Normal file
@ -0,0 +1,128 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Tab, Breadcrumb, Icon } from '@alifd/next';
|
||||
import { SettingsMain, SettingField, isSettingField } from './main';
|
||||
import './style.less';
|
||||
import Title from './title';
|
||||
import SettingsTab, { registerSetter, createSetterContent, getSetter, createSettingFieldView } from './settings-pane';
|
||||
import Node from '../../designer/src/designer/document/node/node';
|
||||
import ArraySetter from './builtin-setters/array-setter';
|
||||
|
||||
export default class SettingsMainView extends Component {
|
||||
private main: SettingsMain;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
this.main = new SettingsMain(props.editor);
|
||||
this.main.onNodesChange(() => {
|
||||
this.forceUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
shouldComponentUpdate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.main.purge();
|
||||
}
|
||||
|
||||
renderBreadcrumb() {
|
||||
if (this.main.isMulti) {
|
||||
return (
|
||||
<div className="lc-settings-navigator">
|
||||
{this.main.componentType!.icon || <Icon type="ellipsis" size="small" />}
|
||||
<span>
|
||||
{this.main.componentType!.title} x {this.main.nodes.length}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let node: Node | null = this.main.nodes[0]!;
|
||||
const items = [];
|
||||
let l = 4;
|
||||
while (l-- > 0 && node) {
|
||||
const props =
|
||||
l === 3
|
||||
? {}
|
||||
: {
|
||||
onMouseOver: hoverNode.bind(null, node, true),
|
||||
onMouseOut: hoverNode.bind(null, node, false),
|
||||
onClick: selectNode.bind(null, node),
|
||||
};
|
||||
items.unshift(<Breadcrumb.Item {...props}>{node.title}</Breadcrumb.Item>);
|
||||
node = node.parent;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="lc-settings-navigator">
|
||||
{this.main.componentType!.icon || <Icon type="ellipsis" size="small" />}
|
||||
<Breadcrumb className="lc-settings-node-breadcrumb">{items}</Breadcrumb>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.main.isNone) {
|
||||
// 未选中节点,提示选中 或者 显示根节点设置
|
||||
return (
|
||||
<div className="lc-settings-main">
|
||||
<div className="lc-settings-notice">
|
||||
<p>请在左侧画布选中节点</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.main.isSame) {
|
||||
// todo: future support 获取设置项交集编辑
|
||||
return (
|
||||
<div className="lc-settings-main">
|
||||
<div className="lc-settings-notice">
|
||||
<p>请选中同一类型节点编辑</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const { items } = this.main;
|
||||
if (items.length > 5 || items.some(item => !isSettingField(item) || !item.isGroup)) {
|
||||
return (
|
||||
<div className="lc-settings-main">
|
||||
{this.renderBreadcrumb()}
|
||||
<div className="lc-settings-body">
|
||||
<SettingsTab target={this.main} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="lc-settings-main">
|
||||
<Tab
|
||||
navClassName="lc-settings-tabs"
|
||||
animation={false}
|
||||
contentClassName="lc-settings-tabs-content"
|
||||
extra={this.renderBreadcrumb()}
|
||||
>
|
||||
{(items as SettingField[]).map(field => (
|
||||
<Tab.Item className="lc-settings-tab-item" title={<Title title={field.title} />} key={field.name}>
|
||||
<SettingsTab target={field} key={field.id} />
|
||||
</Tab.Item>
|
||||
))}
|
||||
</Tab>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function hoverNode(node: Node, flag: boolean) {
|
||||
node.hover(flag);
|
||||
}
|
||||
function selectNode(node: Node) {
|
||||
node.select();
|
||||
}
|
||||
|
||||
registerSetter('ArraySetter', ArraySetter);
|
||||
|
||||
export { registerSetter, createSetterContent, getSetter, createSettingFieldView };
|
||||
541
packages/plugin-settings-pane/src/main.ts
Normal file
541
packages/plugin-settings-pane/src/main.ts
Normal file
@ -0,0 +1,541 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import { uniqueId } from '../../utils/unique-id';
|
||||
import { ComponentType } from '../../designer/src/designer/component-type';
|
||||
import Node from '../../designer/src/designer/document/node/node';
|
||||
import { TitleContent } from './title';
|
||||
import { ReactElement, ComponentType as ReactComponentType, isValidElement } from 'react';
|
||||
import { isReactComponent } from '../../utils/is-react';
|
||||
import Designer from '../../designer/src/designer/designer';
|
||||
import { Selection } from '../../designer/src/designer/document/selection';
|
||||
|
||||
export interface SettingTarget {
|
||||
// 所设置的节点集,至少一个
|
||||
readonly nodes: Node[];
|
||||
|
||||
readonly componentType: ComponentType | null;
|
||||
|
||||
readonly items: Array<SettingField | CustomView>;
|
||||
|
||||
/**
|
||||
* 同样的
|
||||
*/
|
||||
readonly isSame: boolean;
|
||||
|
||||
/**
|
||||
* 一个
|
||||
*/
|
||||
readonly isOne: boolean;
|
||||
|
||||
/**
|
||||
* 多个
|
||||
*/
|
||||
readonly isMulti: boolean;
|
||||
|
||||
/**
|
||||
* 无
|
||||
*/
|
||||
readonly isNone: boolean;
|
||||
|
||||
/**
|
||||
* 编辑器引用
|
||||
*/
|
||||
readonly editor: object;
|
||||
|
||||
readonly designer?: Designer;
|
||||
|
||||
readonly path: string[];
|
||||
|
||||
/**
|
||||
* 响应式自动运行
|
||||
*/
|
||||
onEffect(action: () => void): () => void;
|
||||
|
||||
// 获取属性值
|
||||
getPropValue(propName: string | number): any;
|
||||
|
||||
// 设置属性值
|
||||
setPropValue(propName: string | number, value: any): void;
|
||||
|
||||
/*
|
||||
// 所有属性值数据
|
||||
readonly props: object;
|
||||
// 设置多个属性值,替换原有值
|
||||
setProps(data: object): void;
|
||||
// 设置多个属性值,和原有值合并
|
||||
mergeProps(data: object): void;
|
||||
// 绑定属性值发生变化时
|
||||
onPropsChange(fn: () => void): () => void;
|
||||
*/
|
||||
}
|
||||
|
||||
export type CustomView = ReactElement | ReactComponentType<any>;
|
||||
|
||||
export function isCustomView(obj: any): obj is CustomView {
|
||||
return obj && (isValidElement(obj) || isReactComponent(obj));
|
||||
}
|
||||
|
||||
export type DynamicProps = (field: SettingField, editor: any) => object;
|
||||
|
||||
export interface SetterConfig {
|
||||
/**
|
||||
* if *string* passed must be a registered Setter Name
|
||||
*/
|
||||
componentName: string | CustomView;
|
||||
/**
|
||||
* the props pass to Setter Component
|
||||
*/
|
||||
props?: object | DynamicProps;
|
||||
children?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* if *string* passed must be a registered Setter Name, future support blockSchema
|
||||
*/
|
||||
export type SetterType = SetterConfig | string | CustomView;
|
||||
|
||||
export interface FieldExtraProps {
|
||||
/**
|
||||
* 是否必填参数
|
||||
*/
|
||||
required?: boolean;
|
||||
/**
|
||||
* default value of target prop for setter use
|
||||
*/
|
||||
defaultValue?: any;
|
||||
onChange?: (val: any, field: SettingField, editor: any) => void;
|
||||
getValue?: (field: SettingField, editor: any) => any;
|
||||
/**
|
||||
* the field conditional show, is not set always true
|
||||
* @default undefined
|
||||
*/
|
||||
condition?: (field: SettingField, editor: any) => boolean;
|
||||
/**
|
||||
* default collapsed when display accordion
|
||||
*/
|
||||
defaultCollapsed?: boolean;
|
||||
/**
|
||||
* important field
|
||||
*/
|
||||
important?: boolean;
|
||||
/**
|
||||
* internal use
|
||||
*/
|
||||
forceInline?: number;
|
||||
}
|
||||
|
||||
export interface FieldConfig extends FieldExtraProps {
|
||||
type?: 'field' | 'group';
|
||||
/**
|
||||
* the name of this setting field, which used in quickEditor
|
||||
*/
|
||||
name: string | number;
|
||||
/**
|
||||
* the field title
|
||||
* @default sameas .name
|
||||
*/
|
||||
title?: TitleContent;
|
||||
/**
|
||||
* the field body contains when .type = 'field'
|
||||
*/
|
||||
setter?: SetterType;
|
||||
/**
|
||||
* the setting items which group body contains when .type = 'group'
|
||||
*/
|
||||
items?: FieldConfig[];
|
||||
/**
|
||||
* extra props for field
|
||||
*/
|
||||
extraProps?: FieldExtraProps;
|
||||
}
|
||||
|
||||
export class SettingField implements SettingTarget {
|
||||
readonly isSettingField = true;
|
||||
readonly id = uniqueId('field');
|
||||
readonly type: 'field' | 'virtual-field' | 'group';
|
||||
readonly isGroup: boolean;
|
||||
private _name: string | number;
|
||||
get name() {
|
||||
return this._name;
|
||||
}
|
||||
readonly title: TitleContent;
|
||||
readonly editor: any;
|
||||
readonly extraProps: FieldExtraProps;
|
||||
readonly setter?: SetterType;
|
||||
readonly isSame: boolean;
|
||||
readonly isMulti: boolean;
|
||||
readonly isOne: boolean;
|
||||
readonly isNone: boolean;
|
||||
readonly nodes: Node[];
|
||||
readonly componentType: ComponentType | null;
|
||||
readonly designer: Designer;
|
||||
get path() {
|
||||
const path = this.parent.path.slice();
|
||||
if (this.type === 'field') {
|
||||
path.push(String(this.name));
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
constructor(readonly parent: SettingTarget, config: FieldConfig) {
|
||||
const { type, title, name, items, setter, extraProps, ...rest } = config;
|
||||
|
||||
if (type == null) {
|
||||
const c = typeof name === 'string' ? name.substr(0, 1) : '';
|
||||
if (c === '#') {
|
||||
this.type = 'group';
|
||||
} else if (c === '!') {
|
||||
this.type = 'virtual-field';
|
||||
} else {
|
||||
this.type = 'field';
|
||||
}
|
||||
} else {
|
||||
this.type = type;
|
||||
}
|
||||
// initial self properties
|
||||
this._name = name;
|
||||
this.title = title || String(name);
|
||||
this.setter = setter;
|
||||
this.extraProps = {
|
||||
...rest,
|
||||
...extraProps,
|
||||
};
|
||||
this.isGroup = this.type === 'group';
|
||||
|
||||
// copy parent properties
|
||||
this.editor = parent.editor;
|
||||
this.nodes = parent.nodes;
|
||||
this.componentType = parent.componentType;
|
||||
this.isSame = parent.isSame;
|
||||
this.isMulti = parent.isMulti;
|
||||
this.isOne = parent.isOne;
|
||||
this.isNone = parent.isNone;
|
||||
this.designer = parent.designer!;
|
||||
|
||||
// initial items
|
||||
if (this.type === 'group' && items) {
|
||||
this.initItems(items);
|
||||
}
|
||||
}
|
||||
|
||||
onEffect(action: () => void): () => void {
|
||||
return this.designer.autorun(action, true);
|
||||
}
|
||||
|
||||
private _items: Array<SettingField | CustomView> = [];
|
||||
private initItems(items: Array<FieldConfig | CustomView>) {
|
||||
this._items = items.map((item) => {
|
||||
if (isCustomView(item)) {
|
||||
return item;
|
||||
}
|
||||
return new SettingField(this, item);
|
||||
});
|
||||
}
|
||||
|
||||
private disposeItems() {
|
||||
this._items.forEach(item => isSettingField(item) && item.purge());
|
||||
this._items = [];
|
||||
}
|
||||
|
||||
createField(config: FieldConfig): SettingField {
|
||||
return new SettingField(this, config);
|
||||
}
|
||||
|
||||
get items() {
|
||||
return this._items;
|
||||
}
|
||||
|
||||
// ====== 当前属性读写 =====
|
||||
|
||||
/**
|
||||
* 判断当前属性值是否一致
|
||||
* 0 无值/多种值
|
||||
* 1 类似值,比如数组长度一样
|
||||
* 2 单一植
|
||||
*/
|
||||
get valueState(): number {
|
||||
if (this.type !== 'field') {
|
||||
return 0;
|
||||
}
|
||||
const propName = this.path.join('.');
|
||||
const first = this.nodes[0].getProp(propName)!;
|
||||
let l = this.nodes.length;
|
||||
let state = 2;
|
||||
while (l-- > 1) {
|
||||
const next = this.nodes[l].getProp(propName, false);
|
||||
const s = first.compare(next);
|
||||
if (s > 1) {
|
||||
return 0;
|
||||
}
|
||||
if (s === 1) {
|
||||
state = 1;
|
||||
}
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前属性值
|
||||
*/
|
||||
getValue(): any {
|
||||
if (this.type !== 'field') {
|
||||
return null;
|
||||
}
|
||||
// todo: use getValue
|
||||
const { getValue } = this.extraProps;
|
||||
if (getValue) {
|
||||
return getValue(this, this.editor);
|
||||
}
|
||||
return this.parent.getPropValue(this.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前属性值
|
||||
*/
|
||||
setValue(val: any) {
|
||||
if (this.type !== 'field') {
|
||||
return;
|
||||
}
|
||||
// todo: use onChange
|
||||
this.parent.setPropValue(this.name, val);
|
||||
}
|
||||
|
||||
setKey(key: string | number) {
|
||||
if (this.type !== 'field') {
|
||||
return;
|
||||
}
|
||||
const propName = this.path.join('.');
|
||||
let l = this.nodes.length;
|
||||
while (l-- > 1) {
|
||||
this.nodes[l].getProp(propName, true)!.key = key;
|
||||
}
|
||||
this._name = key;
|
||||
}
|
||||
|
||||
remove() {
|
||||
if (this.type !== 'field') {
|
||||
return;
|
||||
}
|
||||
const propName = this.path.join('.');
|
||||
let l = this.nodes.length;
|
||||
while (l-- > 1) {
|
||||
this.nodes[l].getProp(propName)?.remove()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置子级属性值
|
||||
*/
|
||||
setPropValue(propName: string | number, value: any) {
|
||||
const path = this.type === 'field' ? `${this.name}.${propName}` : propName;
|
||||
this.parent.setPropValue(path, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取子级属性值
|
||||
*/
|
||||
getPropValue(propName: string | number): any {
|
||||
const path = this.type === 'field' ? `${this.name}.${propName}` : propName;
|
||||
return this.parent.getPropValue(path);
|
||||
}
|
||||
|
||||
purge() {
|
||||
this.disposeItems();
|
||||
}
|
||||
}
|
||||
|
||||
export function isSettingField(obj: any): obj is SettingField {
|
||||
return obj && obj.isSettingField;
|
||||
}
|
||||
|
||||
export class SettingsMain implements SettingTarget {
|
||||
private emitter = new EventEmitter();
|
||||
|
||||
private _nodes: Node[] = [];
|
||||
private _items: Array<SettingField | CustomView> = [];
|
||||
private _sessionId = '';
|
||||
private _componentType: ComponentType | null = null;
|
||||
private _isSame: boolean = true;
|
||||
readonly path = [];
|
||||
|
||||
get nodes(): Node[] {
|
||||
return this._nodes;
|
||||
}
|
||||
|
||||
get componentType() {
|
||||
return this._componentType;
|
||||
}
|
||||
|
||||
get items() {
|
||||
return this._items;
|
||||
}
|
||||
|
||||
/**
|
||||
* 同样的
|
||||
*/
|
||||
get isSame(): boolean {
|
||||
return this._isSame;
|
||||
}
|
||||
|
||||
/**
|
||||
* 一个
|
||||
*/
|
||||
get isOne(): boolean {
|
||||
return this.nodes.length === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 多个
|
||||
*/
|
||||
get isMulti(): boolean {
|
||||
return this.nodes.length > 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 无
|
||||
*/
|
||||
get isNone() {
|
||||
return this.nodes.length < 1;
|
||||
}
|
||||
|
||||
private disposeListener: () => void;
|
||||
|
||||
private _designer?: Designer;
|
||||
get designer() {
|
||||
return this._designer || this.editor.designer;
|
||||
}
|
||||
|
||||
constructor(readonly editor: any) {
|
||||
const setupSelection = (selection?: Selection) => {
|
||||
if (selection) {
|
||||
if (!this._designer) {
|
||||
this._designer = selection.doc.designer;
|
||||
}
|
||||
this.setup(selection.getNodes());
|
||||
} else {
|
||||
this.setup([]);
|
||||
}
|
||||
};
|
||||
editor.on('designer.current-selection-change', setupSelection);
|
||||
if (editor.designer) {
|
||||
setupSelection(editor.designer.currentSelection);
|
||||
}
|
||||
this.disposeListener = () => {
|
||||
editor.removeListener('designer.current-selection-change', setupSelection);
|
||||
};
|
||||
}
|
||||
|
||||
onEffect(action: () => void): () => void {
|
||||
action();
|
||||
return this.onNodesChange(action);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取属性值
|
||||
*/
|
||||
getPropValue(propName: string): any {
|
||||
return this.nodes[0].getProp(propName, false)?.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置属性值
|
||||
*/
|
||||
setPropValue(propName: string, value: any) {
|
||||
this.nodes.forEach(node => {
|
||||
node.setPropValue(propName, value);
|
||||
});
|
||||
}
|
||||
|
||||
// 设置多个属性值,替换原有值
|
||||
setProps(data: object) {
|
||||
this.nodes.forEach(node => {
|
||||
node.setProps(data as any);
|
||||
});
|
||||
}
|
||||
|
||||
// 设置多个属性值,和原有值合并
|
||||
mergeProps(data: object) {
|
||||
this.nodes.forEach(node => {
|
||||
node.mergeProps(data as any);
|
||||
});
|
||||
}
|
||||
|
||||
private setup(nodes: Node[]) {
|
||||
this._nodes = nodes;
|
||||
|
||||
// check nodes change
|
||||
const sessionId = this.nodes
|
||||
.map(node => node.id)
|
||||
.sort()
|
||||
.join(',');
|
||||
if (sessionId === this._sessionId) {
|
||||
return;
|
||||
}
|
||||
this._sessionId = sessionId;
|
||||
|
||||
// setups
|
||||
this.setupComponentType();
|
||||
|
||||
// todo: enhance when componentType not changed do merge
|
||||
// clear fields
|
||||
this.setupItems();
|
||||
|
||||
// emit change
|
||||
this.emitter.emit('nodeschange');
|
||||
}
|
||||
|
||||
private disposeItems() {
|
||||
this._items.forEach(item => isSettingField(item) && item.purge());
|
||||
this._items = [];
|
||||
}
|
||||
|
||||
private setupComponentType() {
|
||||
if (this.nodes.length < 1) {
|
||||
this._isSame = false;
|
||||
this._componentType = null;
|
||||
return;
|
||||
}
|
||||
const first = this.nodes[0];
|
||||
const type = first.componentType;
|
||||
const l = this.nodes.length;
|
||||
let theSame = true;
|
||||
for (let i = 1; i < l; i++) {
|
||||
const other = this.nodes[i];
|
||||
if ((other as any).componentType !== type) {
|
||||
theSame = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (theSame) {
|
||||
this._isSame = true;
|
||||
this._componentType = type;
|
||||
} else {
|
||||
this._isSame = false;
|
||||
this._componentType = null;
|
||||
}
|
||||
}
|
||||
|
||||
private setupItems() {
|
||||
this.disposeItems();
|
||||
if (this.componentType) {
|
||||
this._items = this.componentType.configure.map(item => {
|
||||
if (isCustomView(item)) {
|
||||
return item;
|
||||
}
|
||||
return new SettingField(this, item as any);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onNodesChange(fn: () => void): () => void {
|
||||
this.emitter.on('nodeschange', fn);
|
||||
return () => {
|
||||
this.emitter.removeListener('nodeschange', fn);
|
||||
};
|
||||
}
|
||||
|
||||
purge() {
|
||||
this.disposeListener();
|
||||
this.disposeItems();
|
||||
this.emitter.removeAllListeners();
|
||||
}
|
||||
}
|
||||
270
packages/plugin-settings-pane/src/settings-pane.tsx
Normal file
270
packages/plugin-settings-pane/src/settings-pane.tsx
Normal file
@ -0,0 +1,270 @@
|
||||
import { Component, ReactNode } from 'react';
|
||||
import { createContent } from '../../utils/create-content';
|
||||
import { shallowEqual } from '../../utils/shallow-equal';
|
||||
import {
|
||||
SettingField,
|
||||
CustomView,
|
||||
isSettingField,
|
||||
SettingTarget,
|
||||
SetterConfig,
|
||||
isCustomView,
|
||||
DynamicProps,
|
||||
} from './main';
|
||||
import { Field, FieldGroup } from './field';
|
||||
|
||||
export type RegisteredSetter = CustomView | {
|
||||
component: CustomView;
|
||||
props?: object;
|
||||
};
|
||||
|
||||
const settersMap = new Map<string, RegisteredSetter>();
|
||||
export function registerSetter(type: string, setter: RegisteredSetter) {
|
||||
settersMap.set(type, setter);
|
||||
}
|
||||
|
||||
export function getSetter(type: string): RegisteredSetter | null {
|
||||
return settersMap.get(type) || null;
|
||||
}
|
||||
|
||||
export function createSetterContent(setter: any, props: object): ReactNode {
|
||||
if (typeof setter === 'string') {
|
||||
setter = getSetter(setter);
|
||||
if (!isCustomView(setter)) {
|
||||
if (setter.props) {
|
||||
props = {
|
||||
...setter.props,
|
||||
...props,
|
||||
};
|
||||
}
|
||||
setter = setter.component;
|
||||
}
|
||||
}
|
||||
|
||||
return createContent(setter, props);
|
||||
}
|
||||
|
||||
function isSetterConfig(obj: any): obj is SetterConfig {
|
||||
return obj && typeof obj === 'object' && 'componentName' in obj && !isCustomView(obj);
|
||||
}
|
||||
|
||||
class SettingFieldView extends Component<{ field: SettingField }> {
|
||||
state = {
|
||||
visible: false,
|
||||
value: null,
|
||||
setterProps: {},
|
||||
};
|
||||
private dispose: () => void;
|
||||
private setterType?: string | CustomView;
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const { field } = this.props;
|
||||
const { setter } = field;
|
||||
let setterProps: object | DynamicProps = {};
|
||||
if (isSetterConfig(setter)) {
|
||||
this.setterType = setter.componentName;
|
||||
if (setter.props) {
|
||||
setterProps = setter.props;
|
||||
}
|
||||
} else if (setter) {
|
||||
this.setterType = setter;
|
||||
}
|
||||
let firstRun: boolean = true;
|
||||
this.dispose = field.onEffect(() => {
|
||||
const state: any = {};
|
||||
const { extraProps, editor } = field;
|
||||
const { condition, defaultValue } = extraProps;
|
||||
state.visible = field.isOne && typeof condition === 'function' ? !condition(field, editor) : true;
|
||||
if (state.visible) {
|
||||
state.setterProps = {
|
||||
...(typeof setterProps === 'function' ? setterProps(field, editor) : setterProps),
|
||||
};
|
||||
if (field.type === 'field') {
|
||||
if (defaultValue != null && !('defaultValue' in state.setterProps)) {
|
||||
state.setterProps.defaultValue = defaultValue;
|
||||
}
|
||||
if (field.valueState > 0) {
|
||||
state.value = field.getValue();
|
||||
} else {
|
||||
state.value = null;
|
||||
state.setterProps.multiValue = true;
|
||||
if (!('placeholder' in props)) {
|
||||
state.setterProps.placeholder = '多种值';
|
||||
}
|
||||
}
|
||||
// TODO: error handling
|
||||
}
|
||||
}
|
||||
if (firstRun) {
|
||||
firstRun = false;
|
||||
this.state = state;
|
||||
} else {
|
||||
this.setState(state);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
shouldComponentUpdate(_: any, nextState: any) {
|
||||
const { state } = this;
|
||||
if (
|
||||
nextState.value !== state.value ||
|
||||
nextState.visible !== state.visible ||
|
||||
!shallowEqual(state.setterProps, nextState.setterProps)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { visible, value, setterProps } = this.state;
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
const { field } = this.props;
|
||||
const { title, extraProps } = field;
|
||||
|
||||
// todo: error handling
|
||||
|
||||
return (
|
||||
<Field title={extraProps.forceInline ? null : title}>
|
||||
{createSetterContent(this.setterType, {
|
||||
...setterProps,
|
||||
forceInline: extraProps.forceInline,
|
||||
key: field.id,
|
||||
// === injection
|
||||
prop: field,
|
||||
field,
|
||||
// === IO
|
||||
value, // reaction point
|
||||
onChange: (value: any) => {
|
||||
this.setState({
|
||||
value,
|
||||
});
|
||||
field.setValue(value);
|
||||
},
|
||||
})}
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SettingGroupView extends Component<{ field: SettingField }> {
|
||||
state = {
|
||||
visible: false,
|
||||
items: [],
|
||||
};
|
||||
private dispose: () => void;
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const { field } = this.props;
|
||||
const { condition } = field.extraProps;
|
||||
let firstRun: boolean = true;
|
||||
this.dispose = field.onEffect(() => {
|
||||
const state: any = {};
|
||||
state.visible = field.isOne && typeof condition === 'function' ? !condition(field, field.editor) : true;
|
||||
if (state.visible) {
|
||||
state.items = field.items.slice();
|
||||
}
|
||||
if (firstRun) {
|
||||
firstRun = false;
|
||||
this.state = state;
|
||||
} else {
|
||||
this.setState(state);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
shouldComponentUpdate(_: any, nextState: any) {
|
||||
// todo: shallowEqual ?
|
||||
if (nextState.items !== this.state.items || nextState.visible !== this.state.visible) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { field } = this.props;
|
||||
const { title, extraProps } = field;
|
||||
const { defaultCollapsed } = extraProps;
|
||||
const { visible, items } = this.state;
|
||||
// reaction point
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<FieldGroup title={title} defaultCollapsed={defaultCollapsed}>
|
||||
{items.map((item, index) => createSettingFieldView(item, field, index))}
|
||||
</FieldGroup>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function createSettingFieldView(item: SettingField | CustomView, field: SettingTarget, index?: number) {
|
||||
if (isSettingField(item)) {
|
||||
if (item.isGroup) {
|
||||
return <SettingGroupView field={item} key={item.id} />;
|
||||
} else {
|
||||
return <SettingFieldView field={item} key={item.id} />;
|
||||
}
|
||||
} else {
|
||||
return createContent(item, { key: index, field, editor: field.editor });
|
||||
}
|
||||
}
|
||||
|
||||
export function showPopup() {
|
||||
|
||||
}
|
||||
|
||||
export default class SettingsPane extends Component<{ target: SettingTarget }> {
|
||||
state: { items: Array<SettingField | CustomView> } = {
|
||||
items: [],
|
||||
};
|
||||
private dispose: () => void;
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
const { target } = this.props;
|
||||
let firstRun: boolean = true;
|
||||
this.dispose = target.onEffect(() => {
|
||||
const state = {
|
||||
items: target.items.slice(),
|
||||
};
|
||||
if (firstRun) {
|
||||
firstRun = false;
|
||||
this.state = state;
|
||||
} else {
|
||||
this.setState(state);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
shouldComponentUpdate(_: any, nextState: any) {
|
||||
if (nextState.items !== this.state.items) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { items } = this.state;
|
||||
const { target } = this.props;
|
||||
return (
|
||||
<div className="lc-settings-pane">
|
||||
{items.map((item, index) => createSettingFieldView(item, target, index))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
126
packages/plugin-settings-pane/src/style.less
Normal file
126
packages/plugin-settings-pane/src/style.less
Normal file
@ -0,0 +1,126 @@
|
||||
:root {
|
||||
--color-brand: #006cff;
|
||||
--color-brand-light: #197aff;
|
||||
--color-brand-dark: #0060e5;
|
||||
--color-icon-normal: rgba(31, 56, 88, 0.4);
|
||||
--color-icon-hover: rgba(31, 56, 88, 0.6);
|
||||
--color-icon-active: #006cff;
|
||||
--color-icon-reverse: #ffffff;
|
||||
--color-line-light: rgba(31, 56, 88, 0.05);
|
||||
--color-line-normal: rgba(31, 56, 88, 0.1);
|
||||
--color-line-darken: rgba(18, 32, 50, 0.1);
|
||||
--color-title: rgba(0, 0, 0, 0.8);
|
||||
--color-text: rgba(0, 0, 0, 0.6);
|
||||
--color-text-dark: rgba(0, 0, 0, 0.6);
|
||||
--color-text-light: rgba(26, 26, 26, 0.6);
|
||||
--color-text-reverse: rgba(255, 255, 255, 0.8);
|
||||
--color-text-regular: rgba(31, 56, 88, 0.8);
|
||||
--color-field-label: rgba(0, 0, 0, 0.4);
|
||||
--color-field-text: rgba(0, 0, 0, 0.6);
|
||||
--color-field-placeholder: rgba(31, 56, 88, 0.3);
|
||||
--color-field-border: rgba(31, 56, 88, 0.3);
|
||||
--color-field-border-hover: rgba(31, 56, 88, 0.4);
|
||||
--color-field-border-active: rgba(31, 56, 88, 0.6);
|
||||
--color-field-background: #ffffff;
|
||||
--color-pane-background: #ffffff;
|
||||
--color-block-background-normal: #ffffff;
|
||||
--color-block-background-light: rgba(31, 56, 88, 0.03);
|
||||
--color-block-background-shallow: rgba(31, 56, 88, 0.06);
|
||||
--color-block-background-dark: rgba(31, 56, 88, 0.1);
|
||||
--color-block-background-disabled: rgba(31, 56, 88, 0.2);
|
||||
--color-block-background-deep-dark: #BAC3CC;
|
||||
}
|
||||
|
||||
.lc-settings-main {
|
||||
position: relative;
|
||||
|
||||
.lc-settings-notice {
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
font-family: PingFang SC,Hiragino Sans GB,Microsoft YaHei,Helvetica,Arial,sans-serif;
|
||||
color: var(--color-text ,rgba(0,0,0,.6));
|
||||
margin: 50px 15px 0;
|
||||
overflow: hidden;
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
.lc-settings-navigator {
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 5px;
|
||||
border-bottom: 1px solid var(--color-line-normal);
|
||||
.lc-settings-node-breadcrumb {
|
||||
margin-left: 5px;
|
||||
.next-breadcrumb {
|
||||
display: inline-flex;
|
||||
align-items: stretch;
|
||||
height: 24px;
|
||||
}
|
||||
.next-breadcrumb-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
cursor: default;
|
||||
&:not(:last-child):hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lc-settings-body {
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
right: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.lc-settings-pane {
|
||||
padding-bottom: 50px;
|
||||
}
|
||||
|
||||
// ====== reset fusion-tabs =====
|
||||
.lc-settings-tabs {
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
> .next-tabs-nav-extra {
|
||||
position: absolute !important;
|
||||
top: 40px !important;
|
||||
left: 0 !important;
|
||||
height: 30px;
|
||||
right: 0;
|
||||
transform: none !important;
|
||||
|
||||
}
|
||||
.next-tabs-nav-container {
|
||||
.next-tabs-nav {
|
||||
display: flex;
|
||||
.next-tabs-tab.lc-settings-tab-item {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
.next-tabs-tab-inner {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lc-settings-tabs-content {
|
||||
position: absolute;
|
||||
top: 70px;
|
||||
left:0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
.next-tabs-tabpane {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
23
packages/plugin-settings-pane/src/tip/embed-tip.tsx
Normal file
23
packages/plugin-settings-pane/src/tip/embed-tip.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { uniqueId } from '../../../utils/unique-id';
|
||||
import { Component, ReactNode } from 'react';
|
||||
import { saveTips } from './tip-handler';
|
||||
|
||||
export interface TipConfig {
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
theme?: string;
|
||||
direction?: string; // 'n|s|w|e|top|bottom|left|right';
|
||||
}
|
||||
|
||||
export default class EmbedTip extends Component<TipConfig> {
|
||||
private id = uniqueId('tips$');
|
||||
|
||||
componentWillUnmount() {
|
||||
saveTips(this.id, null);
|
||||
}
|
||||
|
||||
render() {
|
||||
saveTips(this.id, this.props);
|
||||
return <meta data-role="tip" data-tip-id={this.id} />;
|
||||
}
|
||||
}
|
||||
4
packages/plugin-settings-pane/src/tip/index.ts
Normal file
4
packages/plugin-settings-pane/src/tip/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import './style.less';
|
||||
|
||||
export { default as EmbedTip } from './embed-tip';
|
||||
export { default as TipContainer } from './tip-container';
|
||||
215
packages/plugin-settings-pane/src/tip/style.less
Normal file
215
packages/plugin-settings-pane/src/tip/style.less
Normal file
@ -0,0 +1,215 @@
|
||||
@keyframes shake {
|
||||
from,
|
||||
to {
|
||||
margin: 0;
|
||||
}
|
||||
20%,
|
||||
60% {
|
||||
margin: 0 10px 0 -10px;
|
||||
}
|
||||
40%,
|
||||
80% {
|
||||
margin: 0 -10px 0 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes drop {
|
||||
from {
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes appear-left {
|
||||
from {
|
||||
transform: translateX(8px);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes appear-right {
|
||||
from {
|
||||
transform: translateX(-8px);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes appear-top {
|
||||
from {
|
||||
transform: translateY(8px);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes appear-bottom {
|
||||
from {
|
||||
transform: translateY(-8px);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scale {
|
||||
from {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spining {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
from,
|
||||
to {
|
||||
transform: scale(1);
|
||||
opacity: 0.7;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.02);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.lc-arrow {
|
||||
position: absolute;
|
||||
width: 36px;
|
||||
height: 10px;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
&:after {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
margin: 0 auto;
|
||||
border: 8px solid transparent;
|
||||
border-top-color: var(--color-pane-background, rgb(255, 255, 255));
|
||||
}
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
|
||||
.lc-align-top > .lc-arrow {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
.lc-align-right > .lc-arrow {
|
||||
left: 0;
|
||||
top: 0;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.lc-align-left > .lc-arrow {
|
||||
right: 0;
|
||||
top: 0;
|
||||
transform-origin: right top;
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.lc-align-bottom > .lc-arrow {
|
||||
top: 0;
|
||||
left: 0;
|
||||
transform: scaleY(-1);
|
||||
}
|
||||
|
||||
|
||||
.lc-tip {
|
||||
z-index: 2;
|
||||
position: fixed;
|
||||
box-sizing: border-box;
|
||||
background: #57a672;
|
||||
max-height: 400px;
|
||||
color: var(--color-text-reverse, rgba(255, 255, 255, 0.8));
|
||||
left: 0;
|
||||
top: 0;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
border-radius: 3px;
|
||||
padding: 6px 8px;
|
||||
text-shadow: 0 -1px rgba(0, 0, 0, 0.3);
|
||||
font-size: var(--font-size-text);
|
||||
line-height: 14px;
|
||||
max-width: 200px;
|
||||
pointer-events: none;
|
||||
&.lc-align-top {
|
||||
transform: translateY(8px);
|
||||
}
|
||||
&.lc-align-bottom {
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
&.lc-align-left {
|
||||
transform: translateX(8px);
|
||||
}
|
||||
&.lc-align-right {
|
||||
transform: translateX(-8px);
|
||||
}
|
||||
.lc-arrow {
|
||||
width: 24px;
|
||||
height: 8px;
|
||||
&:after {
|
||||
border: 6px solid transparent;
|
||||
border-top-color: #57a672;
|
||||
}
|
||||
}
|
||||
&.lc-theme-black {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
.lc-arrow:after {
|
||||
border-top-color: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
}
|
||||
&.lc-theme-green {
|
||||
background: #57a672;
|
||||
.lc-arrow:after {
|
||||
border-top-color: #57a672;
|
||||
}
|
||||
}
|
||||
&.lc-visible {
|
||||
visibility: visible;
|
||||
}
|
||||
&.lc-visible-animate {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transition: transform ease-out 200ms, opacity ease-out 200ms;
|
||||
}
|
||||
|
||||
will-change: transform, width, height, opacity, left, top;
|
||||
}
|
||||
|
||||
.lc-tips-container {
|
||||
pointer-events: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
overflow: visible;
|
||||
z-index: 2000;
|
||||
}
|
||||
36
packages/plugin-settings-pane/src/tip/tip-container.tsx
Normal file
36
packages/plugin-settings-pane/src/tip/tip-container.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import { Component } from 'react';
|
||||
import Tip from './tip';
|
||||
import tipHandler from './tip-handler';
|
||||
|
||||
export default class TipContainer extends Component {
|
||||
shouldComponentUpdate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
private dispose?: () => void;
|
||||
|
||||
componentDidMount() {
|
||||
const over = (e: MouseEvent) => tipHandler.setTarget(e.target as any);
|
||||
const down = () => tipHandler.hideImmediately();
|
||||
document.addEventListener('mouseover', over, false);
|
||||
document.addEventListener('mousedown', down, true);
|
||||
this.dispose = () => {
|
||||
document.removeEventListener('mouseover', over, false);
|
||||
document.removeEventListener('mousedown', down, true);
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
if (this.dispose) {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="lc-tips-container">
|
||||
<Tip />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
139
packages/plugin-settings-pane/src/tip/tip-handler.ts
Normal file
139
packages/plugin-settings-pane/src/tip/tip-handler.ts
Normal file
@ -0,0 +1,139 @@
|
||||
import { TipConfig } from './embed-tip';
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
export interface TipOptions extends TipConfig {
|
||||
target: HTMLElement;
|
||||
}
|
||||
|
||||
function findTip(target: HTMLElement | null): TipOptions | null {
|
||||
if (!target) {
|
||||
return null;
|
||||
}
|
||||
// optimize deep finding on mouseover
|
||||
let loopupLimit = 10;
|
||||
while (target && loopupLimit-- > 0) {
|
||||
// get tip from target node
|
||||
if (target.dataset && target.dataset.tip) {
|
||||
return {
|
||||
children: target.dataset.tip,
|
||||
direction: target.dataset.direction || target.dataset.dir,
|
||||
theme: target.dataset.theme,
|
||||
target,
|
||||
};
|
||||
}
|
||||
|
||||
// or get tip from child nodes
|
||||
let child: HTMLElement | null = target.lastElementChild as HTMLElement;
|
||||
|
||||
while (child) {
|
||||
if (child.dataset && child.dataset.role === 'tip') {
|
||||
const tipId = child.dataset.tipId;
|
||||
if (!tipId) {
|
||||
return null;
|
||||
}
|
||||
const tipProps = tipsMap.get(tipId);
|
||||
if (!tipProps) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...tipProps,
|
||||
target,
|
||||
};
|
||||
}
|
||||
child = child.previousElementSibling as HTMLElement;
|
||||
}
|
||||
|
||||
target = target.parentNode as HTMLElement;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
class TipHandler {
|
||||
tip: TipOptions | null = null;
|
||||
private showDelay: number | null = null;
|
||||
private hideDelay: number | null = null;
|
||||
private emitter = new EventEmitter();
|
||||
|
||||
setTarget(target: HTMLElement) {
|
||||
const tip = findTip(target);
|
||||
if (tip) {
|
||||
if (this.tip) {
|
||||
// the some target should return
|
||||
if (this.tip.target === tip.target) {
|
||||
this.tip = tip;
|
||||
return;
|
||||
}
|
||||
// not show already, reset show delay
|
||||
if (this.showDelay) {
|
||||
clearTimeout(this.showDelay);
|
||||
this.showDelay = null;
|
||||
this.tip = null;
|
||||
} else {
|
||||
if (this.hideDelay) {
|
||||
clearTimeout(this.hideDelay);
|
||||
this.hideDelay = null;
|
||||
}
|
||||
this.tip = tip;
|
||||
this.emitter.emit('tipchange');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.tip = tip;
|
||||
if (this.hideDelay) {
|
||||
clearTimeout(this.hideDelay);
|
||||
this.hideDelay = null;
|
||||
this.emitter.emit('tipchange');
|
||||
} else {
|
||||
this.showDelay = setTimeout(() => {
|
||||
this.showDelay = null;
|
||||
this.emitter.emit('tipchange');
|
||||
}, 350) as any;
|
||||
}
|
||||
} else {
|
||||
if (this.showDelay) {
|
||||
clearTimeout(this.showDelay);
|
||||
this.showDelay = null;
|
||||
} else {
|
||||
this.hideDelay = setTimeout(() => {
|
||||
this.hideDelay = null;
|
||||
}, 100) as any;
|
||||
}
|
||||
this.tip = null;
|
||||
|
||||
this.emitter.emit('tipchange');
|
||||
}
|
||||
}
|
||||
|
||||
hideImmediately() {
|
||||
if (this.hideDelay) {
|
||||
clearTimeout(this.hideDelay);
|
||||
this.hideDelay = null;
|
||||
}
|
||||
if (this.showDelay) {
|
||||
clearTimeout(this.showDelay);
|
||||
this.showDelay = null;
|
||||
}
|
||||
this.tip = null;
|
||||
this.emitter.emit('tipchange');
|
||||
}
|
||||
|
||||
onChange(func: () => void) {
|
||||
this.emitter.on('tipchange', func);
|
||||
return () => {
|
||||
this.emitter.removeListener('tipchange', func);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const tipsMap = new Map<string, TipConfig>();
|
||||
export function saveTips(id: string, props: TipConfig | null) {
|
||||
if (props) {
|
||||
tipsMap.set(id, props);
|
||||
} else {
|
||||
tipsMap.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
export default new TipHandler();
|
||||
120
packages/plugin-settings-pane/src/tip/tip.tsx
Normal file
120
packages/plugin-settings-pane/src/tip/tip.tsx
Normal file
@ -0,0 +1,120 @@
|
||||
import { Component } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { resolvePosition } from './utils';
|
||||
import tipHandler from './tip-handler';
|
||||
|
||||
export default class Tip extends Component {
|
||||
private dispose?: () => void;
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
this.dispose = tipHandler.onChange(() => this.forceUpdate());
|
||||
}
|
||||
|
||||
shouldComponentUpdate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.updateTip();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.updateTip();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.dispose) {
|
||||
this.dispose();
|
||||
}
|
||||
this.clearTimer();
|
||||
}
|
||||
|
||||
private timer: number | null = null;
|
||||
clearTimer() {
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private shell: HTMLDivElement | null = null;
|
||||
private originClassName: string = '';
|
||||
|
||||
updateTip() {
|
||||
if (!this.shell) {
|
||||
return;
|
||||
}
|
||||
const shell = this.shell;
|
||||
const arrow = shell.querySelector('.lc-arrow') as HTMLElement;
|
||||
|
||||
// reset
|
||||
shell.className = this.originClassName;
|
||||
shell.style.cssText = '';
|
||||
arrow.style.cssText = '';
|
||||
this.clearTimer();
|
||||
|
||||
const tip = tipHandler.tip;
|
||||
if (!tip) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { target, direction } = tip;
|
||||
const targetRect = target.getBoundingClientRect();
|
||||
|
||||
if (targetRect.width === 0 || targetRect.height === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const shellRect = shell.getBoundingClientRect();
|
||||
const bounds = {
|
||||
left: 1,
|
||||
top: 1,
|
||||
right: document.documentElement.clientWidth - 1,
|
||||
bottom: document.documentElement.clientHeight - 1,
|
||||
};
|
||||
|
||||
const arrowRect = arrow.getBoundingClientRect();
|
||||
const { dir, left, top, arrowLeft, arrowTop } = resolvePosition(
|
||||
shellRect,
|
||||
targetRect,
|
||||
arrowRect,
|
||||
bounds,
|
||||
direction,
|
||||
);
|
||||
|
||||
shell.classList.add(`lc-align-${dir}`);
|
||||
shell.style.top = `${top}px`;
|
||||
shell.style.left = `${left}px`;
|
||||
shell.style.width = `${shellRect.width}px`;
|
||||
shell.style.height = `${shellRect.height}px`;
|
||||
|
||||
if (dir === 'top' || dir === 'bottom') {
|
||||
arrow.style.left = `${arrowLeft}px`;
|
||||
} else {
|
||||
arrow.style.top = `${arrowTop}px`;
|
||||
}
|
||||
this.timer = window.setTimeout(() => {
|
||||
shell.classList.add('lc-visible-animate');
|
||||
shell.style.transform = 'none';
|
||||
}, 10); /**/
|
||||
}
|
||||
|
||||
render() {
|
||||
const tip: any = tipHandler.tip || {};
|
||||
const className = classNames('lc-tip', tip.className, tip && tip.theme ? `lc-theme-${tip.theme}` : null);
|
||||
|
||||
this.originClassName = className;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
ref={ref => {
|
||||
this.shell = ref;
|
||||
}}
|
||||
>
|
||||
<i className="lc-arrow" />
|
||||
<div className="lc-tip-content">{tip.children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
234
packages/plugin-settings-pane/src/tip/utils.ts
Normal file
234
packages/plugin-settings-pane/src/tip/utils.ts
Normal file
@ -0,0 +1,234 @@
|
||||
function resolveEdge(popup: any, target: any, arrow: any, bounds: any) {
|
||||
const sx = arrow.width > target.width ? (arrow.width - target.width) / 2 : 0;
|
||||
const sy = arrow.width > target.height ? (arrow.width - target.height) / 2 : 0;
|
||||
|
||||
const top = Math.max(target.top - popup.height + arrow.width - sy, bounds.top);
|
||||
const right = Math.min(target.right + popup.width - arrow.width + sx, bounds.right);
|
||||
const bottom = Math.min(target.bottom + popup.height - arrow.width + sy, bounds.bottom);
|
||||
const left = Math.max(target.left - popup.width + arrow.width - sx, bounds.left);
|
||||
|
||||
return { top, right, bottom, left };
|
||||
}
|
||||
|
||||
function resolveDirection(popup: any, target: any, edge: any, bounds: any, prefers: any) {
|
||||
if (prefers.forceDirection) {
|
||||
return prefers.dir;
|
||||
}
|
||||
const extendWidth = popup.width + popup.extraOffset;
|
||||
const extendHeight = popup.height + popup.extraOffset;
|
||||
const SY = popup.width * extendHeight;
|
||||
const SX = popup.height * extendWidth;
|
||||
const mw = Math.min(edge.right - edge.left, popup.width);
|
||||
const mh = Math.min(edge.bottom - edge.top, popup.height);
|
||||
|
||||
const mat: any = {
|
||||
top: () => {
|
||||
const s = mw * Math.min(target.top - bounds.top, extendHeight);
|
||||
return { s, enough: s >= SY };
|
||||
},
|
||||
bottom: () => {
|
||||
const s = mw * Math.min(bounds.bottom - target.bottom, extendHeight);
|
||||
return { s, enough: s >= SY };
|
||||
},
|
||||
left: () => {
|
||||
const s = mh * Math.min(target.left - bounds.left, extendWidth);
|
||||
return { s, enough: s >= SX };
|
||||
},
|
||||
right: () => {
|
||||
const s = mh * Math.min(bounds.right - target.right, extendWidth);
|
||||
return { s, enough: s >= SX };
|
||||
},
|
||||
};
|
||||
|
||||
const orders = ['top', 'right', 'bottom', 'left'];
|
||||
if (prefers.dir) {
|
||||
const i = orders.indexOf(prefers.dir);
|
||||
if (i > -1) {
|
||||
orders.splice(i, 1);
|
||||
orders.unshift(prefers.dir);
|
||||
}
|
||||
}
|
||||
let ms = 0;
|
||||
let prefer = orders[0];
|
||||
for (let i = 0, l = orders.length; i < l; i++) {
|
||||
const dir = orders[i];
|
||||
const { s, enough } = mat[dir]();
|
||||
if (enough) {
|
||||
return dir;
|
||||
}
|
||||
if (s > ms) {
|
||||
ms = s;
|
||||
prefer = dir;
|
||||
}
|
||||
}
|
||||
return prefer;
|
||||
}
|
||||
|
||||
function resolvePrefer(prefer: any) {
|
||||
if (!prefer) {
|
||||
return {};
|
||||
}
|
||||
const force = prefer[0] === '!';
|
||||
if (force) {
|
||||
prefer = prefer.substr(1);
|
||||
}
|
||||
let [dir, offset] = prefer.split(/\s+/);
|
||||
let forceDirection = false;
|
||||
let forceOffset = false;
|
||||
if (dir === 'center') {
|
||||
dir = 'auto';
|
||||
if (!offset) {
|
||||
offset = 'center';
|
||||
}
|
||||
}
|
||||
|
||||
if (force) {
|
||||
if (dir && dir !== 'auto') {
|
||||
forceDirection = true;
|
||||
}
|
||||
if (offset && offset !== 'auto') {
|
||||
forceOffset = true;
|
||||
}
|
||||
}
|
||||
|
||||
return { dir, offset, forceDirection, forceOffset };
|
||||
}
|
||||
|
||||
export function resolvePosition(popup: any, target: any, arrow: any, bounds: any, prefer: any) {
|
||||
popup = {
|
||||
extraOffset: arrow.height,
|
||||
top: popup.top,
|
||||
right: popup.right,
|
||||
left: popup.left,
|
||||
bottom: popup.bottom,
|
||||
height: popup.height,
|
||||
width: popup.width,
|
||||
};
|
||||
|
||||
const prefers = resolvePrefer(prefer);
|
||||
|
||||
const edge = resolveEdge(popup, target, arrow, bounds);
|
||||
|
||||
// 选择方向
|
||||
const dir = resolveDirection(popup, target, edge, bounds, prefers);
|
||||
|
||||
let top;
|
||||
let left;
|
||||
let arrowTop;
|
||||
let arrowLeft;
|
||||
|
||||
// 或得该方位上横向 或 纵向的 偏移
|
||||
if (dir === 'top' || dir === 'bottom') {
|
||||
if (dir === 'top') {
|
||||
top = target.top - popup.extraOffset - popup.height;
|
||||
} else {
|
||||
top = target.bottom + popup.extraOffset;
|
||||
}
|
||||
|
||||
// 解决横向偏移
|
||||
const offset = arrow.width > target.width ? (arrow.width - target.width) / 2 : 0;
|
||||
const minLeft = target.left + arrow.width - offset - popup.width;
|
||||
const maxLeft = target.right - arrow.width + offset;
|
||||
const centerLeft = target.left - (popup.width - target.width) / 2;
|
||||
|
||||
if (prefers.offset === 'left') {
|
||||
left = minLeft;
|
||||
} else if (prefers.offset === 'right') {
|
||||
left = maxLeft;
|
||||
} else {
|
||||
left = centerLeft;
|
||||
}
|
||||
|
||||
if (!prefers.forceOffset) {
|
||||
left = Math.max(Math.min(edge.right - popup.width, left), minLeft);
|
||||
left = Math.min(Math.max(edge.left, left), maxLeft);
|
||||
}
|
||||
|
||||
arrowLeft = Math.min(popup.width - arrow.width, Math.max(target.left - (arrow.width - target.width) / 2 - left, 0));
|
||||
} else {
|
||||
if (dir === 'left') {
|
||||
left = target.left - popup.extraOffset - popup.width;
|
||||
} else {
|
||||
left = target.right + popup.extraOffset;
|
||||
}
|
||||
|
||||
// 解决纵向偏移
|
||||
const offset = arrow.width > target.height ? (arrow.width - target.height) / 2 : 0;
|
||||
const minTop = target.top + arrow.width - offset - popup.height;
|
||||
const maxTop = target.bottom - arrow.width + offset;
|
||||
const centerTop = target.top - (popup.height - target.height) / 2;
|
||||
|
||||
if (prefers.offset === 'top') {
|
||||
top = minTop;
|
||||
} else if (prefers.offset === 'bottom') {
|
||||
top = maxTop;
|
||||
} else {
|
||||
top = centerTop;
|
||||
}
|
||||
|
||||
if (!prefers.forceOffset) {
|
||||
top = Math.max(Math.min(edge.bottom - popup.height, top), minTop);
|
||||
top = Math.min(Math.max(edge.top, top), maxTop);
|
||||
}
|
||||
|
||||
arrowTop = Math.min(popup.height - arrow.height, Math.max(target.top - (arrow.width - target.height) / 2 - top, 0));
|
||||
}
|
||||
|
||||
return { dir, left, top, arrowLeft, arrowTop };
|
||||
}
|
||||
|
||||
const percentPresets: any = {
|
||||
right: 1,
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 1,
|
||||
center: 0.5,
|
||||
};
|
||||
|
||||
function isPercent(val: any) {
|
||||
return /^[\d.]+%$/.test(val);
|
||||
}
|
||||
|
||||
function resolveRelativeValue(val: any, offset: any, total: any) {
|
||||
if (!val) {
|
||||
val = 0;
|
||||
} else if (isPercent(val)) {
|
||||
val = (parseFloat(val) / 100) * total;
|
||||
} else if (percentPresets.hasOwnProperty(val)) {
|
||||
val = percentPresets[val] * total;
|
||||
} else {
|
||||
val = parseFloat(val);
|
||||
if (isNaN(val)) {
|
||||
val = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return `${val + offset}px`;
|
||||
}
|
||||
|
||||
export function resolveRelativePosition(align: any, popup: any, bounds: any) {
|
||||
if (!align) {
|
||||
// return default position
|
||||
return {
|
||||
top: '38.2%',
|
||||
left: 'calc(50% - 110px)',
|
||||
};
|
||||
}
|
||||
|
||||
let [xAlign, yAlign] = align.trim().split(/\s+/);
|
||||
|
||||
if (xAlign === 'top' || xAlign === 'bottom' || yAlign === 'left' || yAlign === 'right') {
|
||||
const tmp = xAlign;
|
||||
xAlign = yAlign;
|
||||
yAlign = tmp;
|
||||
}
|
||||
|
||||
if (xAlign === 'center' && !yAlign) {
|
||||
yAlign = 'center';
|
||||
}
|
||||
|
||||
return {
|
||||
left: resolveRelativeValue(xAlign, 0, bounds.right - bounds.left - popup.width),
|
||||
top: resolveRelativeValue(yAlign, 0, bounds.bottom - bounds.top - popup.height),
|
||||
};
|
||||
}
|
||||
61
packages/plugin-settings-pane/src/title/index.tsx
Normal file
61
packages/plugin-settings-pane/src/title/index.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
import { Component, isValidElement, ReactElement, ReactNode } from 'react';
|
||||
import { Icon } from '@alifd/next';
|
||||
import classNames from 'classnames';
|
||||
import EmbedTip, { TipConfig } from '../tip/embed-tip';
|
||||
import './title.less';
|
||||
|
||||
export interface IconConfig {
|
||||
type: string;
|
||||
size?: number | "small" | "xxs" | "xs" | "medium" | "large" | "xl" | "xxl" | "xxxl" | "inherit";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface TitleConfig {
|
||||
label?: ReactNode;
|
||||
tip?: string | ReactElement | TipConfig;
|
||||
icon?: string | ReactElement | IconConfig;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export type TitleContent = string | ReactElement | TitleConfig;
|
||||
|
||||
export default class Title extends Component<{ title: TitleContent; onClick?: () => void }> {
|
||||
render() {
|
||||
let { title } = this.props;
|
||||
if (isValidElement(title)) {
|
||||
return title;
|
||||
}
|
||||
if (typeof title === 'string') {
|
||||
title = { label: title }; // tslint:disable-line
|
||||
}
|
||||
|
||||
let icon = null;
|
||||
if (title.icon) {
|
||||
if (isValidElement(title.icon)) {
|
||||
icon = title.icon;
|
||||
} else {
|
||||
const iconProps = typeof title.icon === 'string' ? { type: title.icon } : title.icon;
|
||||
icon = <Icon {...iconProps} />;
|
||||
}
|
||||
}
|
||||
|
||||
let tip: any = null;
|
||||
if (title.tip) {
|
||||
if (isValidElement(title.tip) && title.tip.type === EmbedTip) {
|
||||
tip = title.tip;
|
||||
} else {
|
||||
const tipProps =
|
||||
typeof title.tip === 'object' && !isValidElement(title.tip) ? title.tip : { children: title.tip };
|
||||
tip = <EmbedTip direction="top" theme="black" {...tipProps} />;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames('lc-title', title.className)} onClick={this.props.onClick}>
|
||||
{icon ? <div className="lc-title-icon">{icon}</div> : null}
|
||||
{title.label ? <span className="lc-title-label">{title.label}</span> : null}
|
||||
{tip}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
14
packages/plugin-settings-pane/src/title/title.less
Normal file
14
packages/plugin-settings-pane/src/title/title.less
Normal file
@ -0,0 +1,14 @@
|
||||
.lc-title {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: var(--color-text);
|
||||
.lc-title-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.actived .lc-title {
|
||||
color: var(--color-actived);
|
||||
}
|
||||
170
packages/plugin-settings-pane/src/variables.less
Normal file
170
packages/plugin-settings-pane/src/variables.less
Normal file
@ -0,0 +1,170 @@
|
||||
/*
|
||||
* 基础的 DPL 定义使用了 kuma base 的定义,参考:
|
||||
* https://github.com/uxcore/kuma-base/tree/master/variables
|
||||
*/
|
||||
|
||||
/**
|
||||
* ===========================================================
|
||||
* ==================== Font Family ==========================
|
||||
* ===========================================================
|
||||
*/
|
||||
|
||||
/*
|
||||
* @font-family: "STHeiti", "Microsoft Yahei", "Lucida Grande", "Lucida Sans Unicode", Helvetica, Arial, Verdana, sans-serif;
|
||||
*/
|
||||
|
||||
@font-family: 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Helvetica, Arial, sans-serif;
|
||||
@font-family-code: Monaco, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Helvetica, Arial, sans-serif;
|
||||
|
||||
/**
|
||||
* ===========================================================
|
||||
* ===================== Color DPL ===========================
|
||||
* ===========================================================
|
||||
*/
|
||||
|
||||
@brand-color-1: rgba(0, 108, 255, 1);
|
||||
@brand-color-2: rgba(25, 122, 255, 1);
|
||||
@brand-color-3: rgba(0, 96, 229, 1);
|
||||
|
||||
@brand-color-1-3: rgba(0, 108, 255, 0.6);
|
||||
@brand-color-1-4: rgba(0, 108, 255, 0.4);
|
||||
@brand-color-1-5: rgba(0, 108, 255, 0.3);
|
||||
@brand-color-1-6: rgba(0, 108, 255, 0.2);
|
||||
@brand-color-1-7: rgba(0, 108, 255, 0.1);
|
||||
|
||||
@brand-color: @brand-color-1;
|
||||
|
||||
@white-alpha-1: rgb(255, 255, 255); // W-1
|
||||
@white-alpha-2: rgba(255, 255, 255, 0.8); // W-2 A80
|
||||
@white-alpha-3: rgba(255, 255, 255, 0.6); // W-3 A60
|
||||
@white-alpha-4: rgba(255, 255, 255, 0.4); // W-4 A40
|
||||
@white-alpha-5: rgba(255, 255, 255, 0.3); // W-5 A30
|
||||
@white-alpha-6: rgba(255, 255, 255, 0.2); // W-6 A20
|
||||
@white-alpha-7: rgba(255, 255, 255, 0.1); // W-7 A10
|
||||
@white-alpha-8: rgba(255, 255, 255, 0.06); // W-8 A6
|
||||
|
||||
@dark-alpha-1: rgba(0, 0, 0, 1); // D-1 A100
|
||||
@dark-alpha-2: rgba(0, 0, 0, 0.8); // D-2 A80
|
||||
@dark-alpha-3: rgba(0, 0, 0, 0.6); // D-3 A60
|
||||
@dark-alpha-4: rgba(0, 0, 0, 0.4); // D-4 A40
|
||||
@dark-alpha-5: rgba(0, 0, 0, 0.3); // D-5 A30
|
||||
@dark-alpha-6: rgba(0, 0, 0, 0.2); // D-6 A20
|
||||
@dark-alpha-7: rgba(0, 0, 0, 0.1); // D-7 A10
|
||||
@dark-alpha-8: rgba(0, 0, 0, 0.06); // D-8 A6
|
||||
@dark-alpha-9: rgba(0, 0, 0, 0.04); // D-9 A4
|
||||
|
||||
@normal-alpha-1: rgba(31, 56, 88, 1); // N-1 A100
|
||||
@normal-alpha-2: rgba(31, 56, 88, 0.8); // N-2 A80
|
||||
@normal-alpha-3: rgba(31, 56, 88, 0.6); // N-3 A60
|
||||
@normal-alpha-4: rgba(31, 56, 88, 0.4); // N-4 A40
|
||||
@normal-alpha-5: rgba(31, 56, 88, 0.3); // N-5 A30
|
||||
@normal-alpha-6: rgba(31, 56, 88, 0.2); // N-6 A20
|
||||
@normal-alpha-7: rgba(31, 56, 88, 0.1); // N-7 A10
|
||||
@normal-alpha-8: rgba(31, 56, 88, 0.06); // N-8 A6
|
||||
@normal-alpha-9: rgba(31, 56, 88, 0.04); // N-9 A4
|
||||
|
||||
@normal-3: #77879c;
|
||||
@normal-4: #a3aebd;
|
||||
@normal-5: #bac3cc;
|
||||
@normal-6: #d1d7de;
|
||||
|
||||
@gray-dark: #333; // N2_4
|
||||
@gray: #666; // N2_3
|
||||
@gray-light: #999; // N2_2
|
||||
@gray-lighter: #ccc; // N2_1
|
||||
|
||||
@brand-secondary: #2c2f33; // B2_3
|
||||
// 补色
|
||||
@brand-complement: #00b3e8; // B3_1
|
||||
// 复合
|
||||
@brand-comosite: #00c587; // B3_2
|
||||
// 浓度
|
||||
@brand-deep: #73461d; // B3_3
|
||||
|
||||
// F1-1
|
||||
@brand-danger: rgb(240, 70, 49);
|
||||
// F1-2 (10% white)
|
||||
@brand-danger-hover: rgba(240, 70, 49, 0.9);
|
||||
// F1-3 (5% black)
|
||||
@brand-danger-focus: rgba(240, 70, 49, 0.95);
|
||||
|
||||
// F2-1
|
||||
@brand-warning: rgb(250, 189, 14);
|
||||
// F3-1
|
||||
@brand-success: rgb(102, 188, 92);
|
||||
// F4-1
|
||||
@brand-link: rgb(102, 188, 92);
|
||||
// F4-2
|
||||
@brand-link-hover: #2e76a6;
|
||||
|
||||
// F1-1-7 A10
|
||||
@brand-danger-alpha-7: rgba(240, 70, 49, 0.9);
|
||||
// F1-1-8 A6
|
||||
@brand-danger-alpha-8: rgba(240, 70, 49, 0.8);
|
||||
// F2-1-2 A80
|
||||
@brand-warning-alpha-2: rgba(250, 189, 14, 0.8);
|
||||
// F2-1-7 A10
|
||||
@brand-warning-alpha-7: rgba(250, 189, 14, 0.9);
|
||||
// F3-1-2 A80
|
||||
@brand-success-alpha-2: rgba(102, 188, 92, 0.8);
|
||||
// F3-1-7 A10
|
||||
@brand-success-alpha-7: rgba(102, 188, 92, 0.9);
|
||||
// F4-1-7 A10
|
||||
@brand-link-alpha-7: rgba(102, 188, 92, 0.9);
|
||||
|
||||
// 文本色
|
||||
@text-primary-color: @dark-alpha-3;
|
||||
@text-secondary-color: @normal-alpha-3;
|
||||
@text-thirdary-color: @dark-alpha-4;
|
||||
@text-disabled-color: @normal-alpha-5;
|
||||
@text-helper-color: @dark-alpha-4;
|
||||
@text-danger-color: @brand-danger;
|
||||
@text-ali-color: #ec6c00;
|
||||
|
||||
/**
|
||||
* ===========================================================
|
||||
* =================== Shadow Box ============================
|
||||
* ===========================================================
|
||||
*/
|
||||
|
||||
@box-shadow-1: 0 1px 4px 0 rgba(31, 56, 88, 0.15); // 1 级阴影,物体由原来存在于底面的物体展开,物体和底面关联紧密
|
||||
@box-shadow-2: 0 2px 10px 0 rgba(31, 56, 88, 0.15); // 2 级阴影,hover状态,物体层级较高
|
||||
@box-shadow-3: 0 4px 15px 0 rgba(31, 56, 88, 0.15); // 3 级阴影,当物体层级高于所有界面元素,弹窗用
|
||||
|
||||
/**
|
||||
* ===========================================================
|
||||
* ================= FontSize of Level =======================
|
||||
* ===========================================================
|
||||
*/
|
||||
|
||||
@fontSize-1: 26px;
|
||||
@fontSize-2: 20px;
|
||||
@fontSize-3: 16px;
|
||||
@fontSize-4: 14px;
|
||||
@fontSize-5: 12px;
|
||||
|
||||
@fontLineHeight-1: 38px;
|
||||
@fontLineHeight-2: 30px;
|
||||
@fontLineHeight-3: 26px;
|
||||
@fontLineHeight-4: 24px;
|
||||
@fontLineHeight-5: 20px;
|
||||
|
||||
/**
|
||||
* ===========================================================
|
||||
* ================= FontSize of Level =======================
|
||||
* ===========================================================
|
||||
*/
|
||||
|
||||
@global-border-radius: 3px;
|
||||
@input-border-radius: 3px;
|
||||
@popup-border-radius: 6px;
|
||||
|
||||
/**
|
||||
* ===========================================================
|
||||
* ===================== Transistion =========================
|
||||
* ===========================================================
|
||||
*/
|
||||
|
||||
@transition-duration: 0.3s;
|
||||
@transition-ease: cubic-bezier(0.23, 1, 0.32, 1);
|
||||
@transition-delay: 0s;
|
||||
9
packages/plugin-settings-pane/tsconfig.json
Normal file
9
packages/plugin-settings-pane/tsconfig.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "./node_modules/@recore/config/tsconfig",
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true
|
||||
},
|
||||
"include": [
|
||||
"./src/"
|
||||
]
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
样式面板
|
||||
@ -1,17 +1,13 @@
|
||||
import { ReactNode, ComponentType, isValidElement, cloneElement, createElement, ReactElement } from 'react';
|
||||
import { isReactClass } from './is-react';
|
||||
import { ReactNode, ComponentType, isValidElement, cloneElement, createElement } from 'react';
|
||||
import { isReactComponent } from './is-react';
|
||||
|
||||
export function createContent(content: ReactNode | ComponentType<any>, props?: object): ReactNode {
|
||||
if (isValidElement(content)) {
|
||||
return props ? cloneElement(content, props) : content;
|
||||
}
|
||||
if (isReactClass(content)) {
|
||||
if (isReactComponent(content)) {
|
||||
return createElement(content, props);
|
||||
}
|
||||
|
||||
if (typeof content === 'function') {
|
||||
return content(props) as ReactElement;
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
4
packages/utils/has-own-property.ts
Normal file
4
packages/utils/has-own-property.ts
Normal file
@ -0,0 +1,4 @@
|
||||
const prototypeHasOwnProperty = Object.prototype.hasOwnProperty;
|
||||
export function hasOwnProperty(obj: any, key: string | number | symbol): boolean {
|
||||
return obj && prototypeHasOwnProperty.call(obj, key);
|
||||
}
|
||||
5
packages/utils/package.json
Normal file
5
packages/utils/package.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.8.7"
|
||||
}
|
||||
}
|
||||
27
packages/utils/shallow-equal.ts
Normal file
27
packages/utils/shallow-equal.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { hasOwnProperty } from './has-own-property';
|
||||
|
||||
export function shallowEqual(objA: any, objB: any): boolean {
|
||||
if (objA === objB) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const keysA = Object.keys(objA);
|
||||
const keysB = Object.keys(objB);
|
||||
|
||||
if (keysA.length !== keysB.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Test for A's keys different from B.
|
||||
for (let i = 0; i < keysA.length; i++) {
|
||||
if (!hasOwnProperty(objB, keysA[i]) || objA[keysA[i]] !== objB[keysA[i]]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user