├── src
├── index.d.ts
├── static
│ └── iconfont.css
├── canvas
│ ├── empty.js
│ ├── endpoint.js
│ ├── checkbox.less
│ ├── canvas.js
│ └── node.js
├── index.less
├── adaptor.js
└── index.tsx
├── .gitignore
├── tsconfig.json
├── example
├── tsconfig.json
├── index.html
├── index.less
├── mock_data
│ ├── single-point-limit.js
│ ├── single-with-header.js
│ ├── single-no-header.js
│ ├── diff-columns.js
│ └── mutiply-mapping.js
├── package.json
├── webpack.config.js
└── index.jsx
├── LICENSE
├── rollup.config.js
├── package.json
├── README.md
└── README.en-US.md
/src/index.d.ts:
--------------------------------------------------------------------------------
1 | declare let $: JQuery;
2 | export default $;
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | dist
4 | package-lock.json
5 | es
6 | pack
7 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "./dist/",
4 | "noImplicitAny": false,
5 | "module": "commonjs",
6 | "target": "es2015",
7 | "jsx": "react",
8 | "allowJs": true,
9 | "esModuleInterop": true
10 | }
11 | }
--------------------------------------------------------------------------------
/example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "./dist/",
4 | "noImplicitAny": false,
5 | "module": "commonjs",
6 | "target": "es5",
7 | "jsx": "react",
8 | "allowJs": true,
9 | "esModuleInterop": true
10 | }
11 | }
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | DTDesign-React数据映射组件
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/example/index.less:
--------------------------------------------------------------------------------
1 | html, body {
2 | padding: 0;
3 | margin: 0;
4 | width: 100%;
5 | height: 100%;
6 | }
7 |
8 | #main {
9 | width: 100%;
10 | .ant-layout, .ant-layout-content {
11 | height: 100%;
12 | }
13 |
14 | .menu {
15 | height: 100%;
16 | width: 200px;
17 | overflow-y: auto;
18 | }
19 |
20 | .header.ant-layout-header {
21 | background-color: #212528;
22 | color: #fff;
23 | height: 50px;
24 | line-height: 50px;
25 | }
26 |
27 | section.ant-layout {
28 | background: #2E2E2E;
29 | }
30 |
31 | .container {
32 | border: 1px solid rgba(255,255,255,0.3);
33 | margin: 5px;
34 | }
35 | .empty-content {
36 | height: 100px;
37 | padding-top: 20px;
38 | text-align: center;
39 | .desc {
40 | color: #474747;
41 | }
42 | .add-field {
43 | color: #0070cc;
44 | cursor: pointer;
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/example/mock_data/single-point-limit.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | export const columns4 = [{
4 | key: 'id',
5 | primaryKey: true,
6 | width: 30
7 | }, {
8 | key: 'name',
9 | width: 90
10 | }, {
11 | key: 'desc',
12 | width: 90
13 | }];
14 |
15 | export const sourceData4 = {
16 | fields: [{
17 | id: '1',
18 | name: '性别',
19 | desc: 'gender'
20 | }, {
21 | id: '2',
22 | name: '年龄',
23 | desc: 'age'
24 | }, {
25 | id: '3',
26 | name: '喜好',
27 | desc: 'hobby'
28 | }]
29 | };
30 |
31 | export const targetData4 = {
32 | fields: [{
33 | id: '1',
34 | name: '限制数量1',
35 | desc: 'point limit1'
36 | }, {
37 | id: '2',
38 | name: '限制数量1',
39 | desc: 'point limit1'
40 | }, {
41 | id: '3',
42 | name: '限制数量1',
43 | desc: 'point limit1'
44 | }]
45 | };
46 |
47 | export const mappingData4 = [{
48 | source: '1',
49 | target: '3'
50 | }, {
51 | source: '2',
52 | target: '1'
53 | }];
54 |
55 |
--------------------------------------------------------------------------------
/src/static/iconfont.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'data-mapping-icon'; /* project id 2354012 */
3 | src: url('//at.alicdn.com/t/font_2354012_zebv3q16c09.eot');
4 | src: url('//at.alicdn.com/t/font_2354012_zebv3q16c09.eot?#iefix') format('embedded-opentype'),
5 | url('//at.alicdn.com/t/font_2354012_zebv3q16c09.woff2') format('woff2'),
6 | url('//at.alicdn.com/t/font_2354012_zebv3q16c09.woff') format('woff'),
7 | url('//at.alicdn.com/t/font_2354012_zebv3q16c09.ttf') format('truetype'),
8 | url('//at.alicdn.com/t/font_2354012_zebv3q16c09.svg#data-mapping-icon') format('svg');
9 | }
10 |
11 | .data-mapping-icon {
12 | font-family: "data-mapping-icon" !important;
13 | font-size: 16px;
14 | font-style: normal;
15 | -webkit-font-smoothing: antialiased;
16 | -moz-osx-font-smoothing: grayscale;
17 | }
18 |
19 | .data-mapping-icon-kongshuju:before {
20 | content: "\e602";
21 | }
22 |
23 | .data-mapping-icon-paixu-top:before {
24 | content: "\e601";
25 | }
26 |
27 | .data-mapping-icon-paixu-bottom:before {
28 | content: "\e600";
29 | }
30 |
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Alibaba Cloud
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/canvas/empty.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery';
2 | import React from 'react';
3 | import ReactDOM from 'react-dom';
4 |
5 | const isReactEle = (HTMLElement) => {
6 | return React.isValidElement(HTMLElement);
7 | };
8 |
9 | /**
10 | * params {Object} config
11 | * params {JSX.Element | String} config.content
12 | * params {Number | String} config.width
13 | */
14 | export default (config, callback) => {
15 | const content = config.content;
16 | let width = config.width;
17 |
18 | if (!width) {
19 | width = '150px';
20 | }
21 |
22 | if (typeof config.width === 'number') {
23 | width = config.width + 'px';
24 | }
25 |
26 | let emptyDom = '';
27 |
28 | if (content) {
29 | if (isReactEle(content)) {
30 | emptyDom = ReactDOM.render(content, document.createElement('div'), callback);
31 | } else {
32 | emptyDom = $(content);
33 | }
34 | } else {
35 | emptyDom = $('');
36 | const iconDom = $('');
37 |
38 | emptyDom.append(iconDom);
39 | }
40 |
41 | return emptyDom;
42 | };
43 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "start": "webpack-dev-server"
9 | },
10 | "author": "jambin",
11 | "license": "MIT",
12 | "dependencies": {
13 | "antd": "~4.10.3",
14 | "jquery": "^3.4.1",
15 | "react": "~16.12.0",
16 | "react-dom": "^16.13.1",
17 | "react-router": "~5.1.2",
18 | "react-router-dom": "~5.1.2"
19 | },
20 | "devDependencies": {
21 | "@babel/core": "~7.8.3",
22 | "@babel/plugin-proposal-class-properties": "~7.8.3",
23 | "@babel/plugin-proposal-object-rest-spread": "~7.8.3",
24 | "@babel/plugin-transform-runtime": "^7.12.1",
25 | "@babel/preset-env": "~7.8.3",
26 | "@babel/preset-react": "~7.8.3",
27 | "babel-loader": "8.0.6",
28 | "babel-plugin-transform-es2015-modules-commonjs": "~6.26.2",
29 | "css-loader": "~1.0.0",
30 | "eslint": "~5.16.0",
31 | "eslint-config-aliyun": "~2.0.3",
32 | "eslint-plugin-react": "~7.13.0",
33 | "file-loader": "~2.0.0",
34 | "html-webpack-plugin": "^3.2.0",
35 | "less": "~3.7.0",
36 | "less-loader": "~4.1.0",
37 | "mini-css-extract-plugin": "~0.9.0",
38 | "style-loader": "~0.21.0",
39 | "ts-loader": "~8.0.14",
40 | "url-loader": "~1.0.1",
41 | "webpack": "~4.41.5",
42 | "webpack-cli": "~3.0.8",
43 | "webpack-dev-server": "~3.10.1"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/example/mock_data/single-with-header.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | export const columns2 = [{
4 | key: 'id',
5 | primaryKey: true,
6 | width: 30
7 | }, {
8 | key: 'name',
9 | }, {
10 | key: 'desc',
11 | width: 90
12 | }];
13 |
14 | export const sourceData2 = {
15 | title: '来源列',
16 | fields: [{
17 | id: '1',
18 | name: '性别',
19 | desc: 'gender'
20 | }, {
21 | id: '2',
22 | name: '年龄',
23 | desc: 'age'
24 | }, {
25 | id: '3',
26 | name: '喜好',
27 | desc: 'hobby'
28 | }, {
29 | id: '4',
30 | name: '身高',
31 | desc: 'height'
32 | }, {
33 | id: '5',
34 | name: '体重',
35 | desc: 'weight'
36 | }, {
37 | id: '6',
38 | name: '国籍',
39 | desc: 'nationality'
40 | }]
41 | };
42 |
43 | export const targetData2 = {
44 | title: '目标列',
45 | fields: [{
46 | id: '1',
47 | name: '字段1',
48 | desc: 'filed1'
49 | }, {
50 | id: '2',
51 | name: '字段2',
52 | desc: 'filed2'
53 | }, {
54 | id: '3',
55 | name: '字段3',
56 | desc: 'filed3'
57 | }, {
58 | id: '4',
59 | name: '字段4',
60 | desc: 'filed4'
61 | }, {
62 | id: '5',
63 | name: '字段5',
64 | desc: 'filed5'
65 | }, {
66 | id: '6',
67 | name: '字段6',
68 | desc: 'filed6'
69 | }, {
70 | id: '7',
71 | name: '字段7',
72 | desc: 'filed7'
73 | }, {
74 | id: '8',
75 | name: '字段8',
76 | desc: 'filed8'
77 | }]
78 | };
79 |
80 | export const mappingData2 = [{
81 | source: '1',
82 | target: '3'
83 | }, {
84 | source: '2',
85 | target: '4'
86 | }, {
87 | source: '4',
88 | target: '1'
89 | }];
90 |
91 |
--------------------------------------------------------------------------------
/src/canvas/endpoint.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import {Endpoint} from 'butterfly-dag';
4 | import $ from 'jquery';
5 | import _ from 'lodash';
6 |
7 | class NewEndPoint extends Endpoint {
8 | constructor(opts) {
9 | super(opts);
10 | if (!this.options._isNodeSelf) {
11 | this.originId = (this.id || '').replace('-left', '').replace('-right', '');
12 | }
13 | }
14 | attachEvent() {
15 | $(this.dom).on('mousedown', (e) => {
16 | const LEFT_KEY = 0;
17 | if (e.button !== LEFT_KEY) {
18 | return;
19 | }
20 |
21 | e.preventDefault();
22 | e.stopPropagation();
23 |
24 | if (this.options.disable) {
25 | return;
26 | }
27 |
28 | // 点击中了上移/下移的按钮,需要阻止
29 | let classname = e.target.className || '';
30 | if (!_.isString(classname) || classname.indexOf('move-up') !== -1 || classname.indexOf('move-down') !== -1) {
31 | return;
32 | }
33 |
34 | if (this.options._isNodeSelf && this.type === 'target') {
35 | this.emit('custom.endpoint.dragNode', {
36 | data: this
37 | });
38 | } else {
39 | this.emit('InnerEvents', {
40 | type: 'endpoint:drag',
41 | data: this
42 | });
43 | }
44 | });
45 |
46 | if (this.options._isNodeSelf) {
47 | $(this.dom).on('mouseover', (e) => {
48 | this.emit('custom.endpoint.focus', {
49 | point: this
50 | });
51 | });
52 |
53 | $(this.dom).on('mouseout', (e) => {
54 | this.emit('custom.endpoint.unFocus', {
55 | point: this
56 | });
57 | });
58 | }
59 | }
60 | }
61 |
62 | export default NewEndPoint;
63 |
--------------------------------------------------------------------------------
/example/mock_data/single-no-header.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | import React from 'react';
3 |
4 | export const columns1 = [{
5 | key: 'id',
6 | title: 'ID',
7 | primaryKey: true,
8 | width: 30
9 | }, {
10 | key: 'name',
11 | title: '名字',
12 | render: (val, row, index) => {
13 | return {val}
14 | }
15 | }, {
16 | key: 'desc',
17 | title: '描述',
18 | }];
19 |
20 | export const sourceData1 = {
21 | title: 'source标题',
22 | fields: [{
23 | id: '1',
24 | name: '性别',
25 | desc: 'gender'
26 | }, {
27 | id: '2',
28 | name: '年龄',
29 | desc: 'age'
30 | }, {
31 | id: '3',
32 | name: '喜好',
33 | desc: 'hobby'
34 | }, {
35 | id: '4',
36 | name: '身高',
37 | desc: 'height'
38 | }, {
39 | id: '5',
40 | name: '体重',
41 | desc: 'weight',
42 | checked: true
43 | }, {
44 | id: '6',
45 | name: '国籍',
46 | desc: 'nation',
47 | disable: true
48 | }]
49 | };
50 |
51 | export const targetData1 = {
52 | fields: [{
53 | id: '1',
54 | name: '字段1',
55 | desc: 'filed1'
56 | }, {
57 | id: '2',
58 | name: '字段2',
59 | desc: 'filed2'
60 | }, {
61 | id: '3',
62 | name: '字段3',
63 | desc: 'filed3'
64 | }, {
65 | id: '4',
66 | name: '字段4',
67 | desc: 'filed4'
68 | }, {
69 | id: '5',
70 | name: '字段5',
71 | desc: 'filed5'
72 | }, {
73 | id: '6',
74 | name: '字段6',
75 | desc: 'filed6'
76 | }, {
77 | id: '7',
78 | name: '字段7',
79 | desc: 'filed7'
80 | }, {
81 | id: '8',
82 | name: '字段8',
83 | desc: 'filed8',
84 | disable: true
85 | }]
86 | };
87 |
88 | export const mappingData1 = [{
89 | source: '1',
90 | target: '3'
91 | }, {
92 | source: '2',
93 | target: '4'
94 | }, {
95 | source: '4',
96 | target: '1'
97 | }];
98 |
99 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import json from 'rollup-plugin-json';
3 | import babel from 'rollup-plugin-babel';
4 | import postcss from 'rollup-plugin-postcss';
5 | import commonjs from 'rollup-plugin-commonjs';
6 | import extensions from 'rollup-plugin-extensions';
7 | import external from 'rollup-plugin-peer-deps-external';
8 | import url from "rollup-plugin-url";
9 | import typescript from 'rollup-plugin-typescript2';
10 |
11 | import pkg from './package.json';
12 |
13 | const config = {
14 | presets: [
15 | '@babel/preset-typescript',
16 | '@babel/preset-react',
17 | '@babel/preset-env'
18 | ],
19 | plugins: [
20 | '@babel/plugin-proposal-class-properties'
21 | ]
22 | };
23 |
24 |
25 | const plugins = [
26 | extensions({
27 | extensions: ['.js'],
28 | resolveIndex: true,
29 | }),
30 | external(),
31 | babel(Object.assign({
32 | exclude: [
33 | 'node_modules/**',
34 | ]
35 | }, config)),
36 | postcss({
37 | extract: true,
38 | modules: false,
39 | use: [
40 | [
41 | 'less',
42 | {
43 | javascriptEnabled: true,
44 | }
45 | ]
46 | ]
47 | }),
48 | commonjs(),
49 | json(),
50 | url({
51 | limit: 100 * 1024, // inline files < 100k, copy files > 100k
52 | include: ["**/*.svg", "**/*.eot", "**/*.tff", "**/*.woff", "**/*.woff2"], // defaults to .svg, .png, .jpg and .gif files
53 | emitFiles: true // defaults to true
54 | }),
55 | typescript({
56 | tsconfigOverride: {
57 | compilerOptions: {
58 | module: "es2015"
59 | }
60 | }
61 | })
62 | ];
63 |
64 | const rollupCfg = [];
65 |
66 | // all in one 构建
67 | rollupCfg.push({
68 | input: path.join(__dirname, 'src/index.tsx'),
69 | output: [
70 | {
71 | file: pkg.pack,
72 | format: 'cjs',
73 | exports: 'named',
74 | sourcemap: 'inline'
75 | },
76 | {
77 | file: pkg.main,
78 | format: 'es',
79 | sourcemap: 'inline'
80 | }
81 | ],
82 | plugins
83 | });
84 |
85 | export default rollupCfg;
86 |
--------------------------------------------------------------------------------
/example/mock_data/diff-columns.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | import React from 'react';
3 |
4 | export const sourceColumns = [{
5 | key: 'id',
6 | title: 'ID',
7 | primaryKey: true,
8 | width: 30
9 | }, {
10 | key: 'name',
11 | title: '名字',
12 | render: (val, row, index) => {
13 | return {val}
14 | }
15 | }, {
16 | key: 'desc',
17 | title: '描述',
18 | }];
19 |
20 | export const targetColumns = [{
21 | key: 'code',
22 | title: 'code',
23 | primaryKey: true,
24 | width: 30
25 | }, {
26 | key: 'gender',
27 | title: '性别',
28 | render: (val, row, index) => {
29 | return {val}
30 | }
31 | }, {
32 | key: 'desc',
33 | title: '简介',
34 | }];
35 |
36 | export const sourceData5 = {
37 | title: 'source标题',
38 | fields: [{
39 | id: '1',
40 | name: '性别',
41 | desc: 'gender'
42 | }, {
43 | id: '2',
44 | name: '年龄',
45 | desc: 'age'
46 | }, {
47 | id: '3',
48 | name: '喜好',
49 | desc: 'hobby'
50 | }, {
51 | id: '4',
52 | name: '身高',
53 | desc: 'height'
54 | }, {
55 | id: '5',
56 | name: '体重',
57 | desc: 'weight'
58 | }, {
59 | id: '6',
60 | name: '国籍',
61 | desc: 'nation'
62 | }]
63 | };
64 |
65 | export const targetData5 = {
66 | fields: [{
67 | code: '1',
68 | gender: '男',
69 | desc: 'filed1'
70 | }, {
71 | code: '2',
72 | gender: '女',
73 | desc: 'filed2'
74 | }, {
75 | code: '3',
76 | gender: '男',
77 | desc: 'filed3'
78 | }, {
79 | code: '4',
80 | gender: '女',
81 | desc: 'filed4'
82 | }, {
83 | code: '5',
84 | gender: '男',
85 | desc: 'filed5'
86 | }, {
87 | code: '6',
88 | gender: '女',
89 | desc: 'filed6'
90 | }, {
91 | code: '7',
92 | gender: '男',
93 | desc: 'filed7'
94 | }, {
95 | code: '8',
96 | gender: '中性',
97 | desc: 'filed8'
98 | }]
99 | };
100 |
101 | export const mappingData5 = [{
102 | source: '1',
103 | target: '3'
104 | }, {
105 | source: '2',
106 | target: '4'
107 | }, {
108 | source: '4',
109 | target: '1'
110 | }];
111 |
112 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-data-mapping",
3 | "version": "1.3.17",
4 | "description": "数据/字段映射React组件",
5 | "main": "dist/index.js",
6 | "pack": "pack/index.js",
7 | "scripts": {
8 | "test": "echo \"Error: no test specified\" && exit 1",
9 | "build": "rollup -c && cp -r ./src/static ./dist && cp -r ./src/static ./pack && tsc -d --emitDeclarationOnly --target esnext --removeComments false --allowJs false --declarationDir ./dist",
10 | "dev": "rollup -w -c"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git@gitlab.alibaba-inc.com:DataQ-FE-Components/butterfly-data-mapping.git"
15 | },
16 | "keywords": [
17 | "butterfly",
18 | "data-mapping",
19 | "field-mapping",
20 | "react-data-mapping",
21 | "react-field-mapping",
22 | "butterfly-data-mapping"
23 | ],
24 | "author": "无惟",
25 | "license": "MIT",
26 | "dependencies": {
27 | "butterfly-dag": "~4.1.0"
28 | },
29 | "peerDependencies": {
30 | "react": ">15.6.1",
31 | "react-dom": ">15.6.1",
32 | "lodash": "^4.17.20",
33 | "jquery": ">3.5.1"
34 | },
35 | "devDependencies": {
36 | "@babel/core": "~7.12.0",
37 | "@babel/plugin-proposal-class-properties": "~7.12.1",
38 | "@babel/plugin-proposal-object-rest-spread": "~7.12.0",
39 | "@babel/plugin-transform-modules-commonjs": "^7.12.1",
40 | "@babel/plugin-transform-runtime": "^7.12.1",
41 | "@babel/preset-env": "~7.12.0",
42 | "@babel/preset-react": "~7.12.1",
43 | "@babel/preset-typescript": "~7.12.7",
44 | "@types/lodash": "~4.14.167",
45 | "@types/react": "~17.0.0",
46 | "@types/react-dom": "^17.0.0",
47 | "babel-loader": "~8.2.0",
48 | "babel-plugin-transform-es2015-modules-commonjs": "~6.26.2",
49 | "less": "~3.12.2",
50 | "postcss": "~8.2.13",
51 | "rollup": "~2.38.0",
52 | "rollup-plugin-babel": "~4.4.0",
53 | "rollup-plugin-commonjs": "~10.1.0",
54 | "rollup-plugin-extensions": "~0.1.0",
55 | "rollup-plugin-json": "~4.0.0",
56 | "rollup-plugin-peer-deps-external": "~2.2.4",
57 | "rollup-plugin-postcss": "~4.0.0",
58 | "rollup-plugin-typescript2": "~0.29.0",
59 | "rollup-plugin-url": "~3.0.1",
60 | "typescript": "~4.1.3"
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/canvas/checkbox.less:
--------------------------------------------------------------------------------
1 | .butterfly-data-mapping {
2 | .dm-checkbox {
3 | box-sizing: border-box;
4 | padding: 0;
5 | color: #000000d9;
6 | font-size: 14px;
7 | font-variant: tabular-nums;
8 | list-style: none;
9 | font-feature-settings: "tnum";
10 | position: relative;
11 | line-height: 1;
12 | white-space: nowrap;
13 | outline: none;
14 | cursor: pointer;
15 | .dm-checkbox-inner {
16 | position: relative;
17 | top: 0;
18 | left: 0;
19 | display: block;
20 | width: 16px;
21 | height: 16px;
22 | direction: ltr;
23 | border: 1px solid #d9d9d9;
24 | border-radius: 2px;
25 | border-collapse: separate;
26 | transition: all .3s;
27 | &:after {
28 | position: absolute;
29 | top: 50%;
30 | left: 21.5%;
31 | display: table;
32 | width: 5.71428571px;
33 | height: 9.14285714px;
34 | border: 2px solid #fff;
35 | border-top: 0;
36 | border-left: 0;
37 | transform: rotate(45deg) scale(0) translate(-50%,-50%);
38 | opacity: 0;
39 | transition: all .1s cubic-bezier(.71,-.46,.88,.6),opacity .1s;
40 | content: " ";
41 | }
42 | }
43 | &.dm-checkbox-checked {
44 | .dm-checkbox-inner {
45 | background-color: #3b93f4;
46 | border-color: #3b93f4;
47 | &:after {
48 | position: absolute;
49 | display: table;
50 | border: 2px solid #fff;
51 | border-top: 0;
52 | border-left: 0;
53 | transform: rotate(45deg) scale(1) translate(-50%,-50%);
54 | opacity: 1;
55 | transition: all .2s cubic-bezier(.12,.4,.29,1.46) .1s;
56 | content: " ";
57 | }
58 | }
59 | &:after {
60 | position: absolute;
61 | top: 0;
62 | left: 0;
63 | width: 100%;
64 | height: 100%;
65 | border: 1px solid #3b93f4;
66 | border-radius: 2px;
67 | visibility: hidden;
68 | -webkit-animation: antCheckboxEffect .36s ease-in-out;
69 | animation: antCheckboxEffect .36s ease-in-out;
70 | -webkit-animation-fill-mode: backwards;
71 | animation-fill-mode: backwards;
72 | content: "";
73 | }
74 | }
75 | }
76 | }
--------------------------------------------------------------------------------
/example/mock_data/mutiply-mapping.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | export const columns3 = [{
4 | key: 'id',
5 | width: 30,
6 | primaryKey: true,
7 | }, {
8 | key: 'name',
9 | }, {
10 | key: 'desc',
11 | width: 90
12 | }];
13 |
14 | export const sourceData3 = [{
15 | id: 'source1',
16 | title: '来源列1',
17 | fields: [{
18 | id: '1',
19 | name: '性别',
20 | desc: 'gender'
21 | }, {
22 | id: '2',
23 | name: '年龄',
24 | desc: 'age'
25 | }, {
26 | id: '3',
27 | name: '喜好',
28 | desc: 'hobby'
29 | }, {
30 | id: '4',
31 | name: '身高',
32 | desc: 'height'
33 | }, {
34 | id: '5',
35 | name: '体重',
36 | desc: 'weight'
37 | }, {
38 | id: '6',
39 | name: '国籍',
40 | desc: 'nationality'
41 | }]
42 | }, {
43 | id: 'source2',
44 | title: '来源列2',
45 | fields: [{
46 | id: '1',
47 | name: '性别',
48 | desc: 'gender'
49 | }, {
50 | id: '2',
51 | name: '年龄',
52 | desc: 'age'
53 | }, {
54 | id: '3',
55 | name: '喜好',
56 | desc: 'hobby'
57 | }, {
58 | id: '4',
59 | name: '身高',
60 | desc: 'height'
61 | }, {
62 | id: '5',
63 | name: '体重',
64 | desc: 'weight'
65 | }, {
66 | id: '6',
67 | name: '国籍',
68 | desc: 'nationality'
69 | }]
70 | }];
71 |
72 | export const targetData3 = [{
73 | id: 'target1',
74 | title: '目标列1',
75 | fields: [{
76 | id: '1',
77 | name: '字段1',
78 | desc: 'filed1'
79 | }, {
80 | id: '2',
81 | name: '字段2',
82 | desc: 'filed2'
83 | }, {
84 | id: '3',
85 | name: '字段3',
86 | desc: 'filed3'
87 | }, {
88 | id: '4',
89 | name: '字段4',
90 | desc: 'filed4'
91 | }, {
92 | id: '5',
93 | name: '字段5',
94 | desc: 'filed5'
95 | }, {
96 | id: '6',
97 | name: '字段6',
98 | desc: 'filed6'
99 | }, {
100 | id: '7',
101 | name: '字段7',
102 | desc: 'filed7'
103 | }, {
104 | id: '8',
105 | name: '字段8',
106 | desc: 'filed8'
107 | }]
108 | }, {
109 | id: 'target2',
110 | title: '目标列2(空状态)',
111 | fields: []
112 | }];
113 |
114 | export const mappingData3 = [{
115 | source: '1',
116 | target: '3',
117 | sourceNode: 'source1',
118 | targetNode: 'target1'
119 | }, {
120 | source: '2',
121 | target: '4',
122 | sourceNode: 'source1',
123 | targetNode: 'target1'
124 | }, {
125 | source: '4',
126 | target: '1',
127 | sourceNode: 'source1',
128 | targetNode: 'target1'
129 | }];
130 |
131 |
--------------------------------------------------------------------------------
/src/index.less:
--------------------------------------------------------------------------------
1 | @import './static/iconfont.css';
2 |
3 | .butterfly-data-mapping {
4 | position: relative;
5 | height: 500px;
6 | width: 550px;
7 | min-height: 200px;
8 | min-width: 200px;
9 | .table-node {
10 | position: absolute;
11 | border: 1px solid #595959;
12 | color: #fff;
13 | border-radius: 5px;
14 | background: #2E2E2E;
15 | .title {
16 | background: rgba(255, 255, 255, 0.15);
17 | padding-left: 10px;
18 | overflow: hidden;
19 | text-overflow: ellipsis;
20 | white-space: nowrap;
21 | }
22 | .filed-title{
23 | background: #1d1d1d;
24 | .filed-title-item{
25 | display: inline-block;
26 | text-align: center;
27 | }
28 | }
29 | .field {
30 | position: relative;
31 | cursor: pointer;
32 | border-bottom: 1px solid rgba(255,255,255,0.3);
33 | &:last-child {
34 | border-bottom: none;
35 | }
36 | &:hover {
37 | background: rgba(255, 255, 255, 0.06);
38 | }
39 | &.link {
40 | background: rgba(255,255,255,0.12);
41 | }
42 | &.focus {
43 | background: #F66902;
44 | }
45 | .field-sort {
46 | overflow: hidden;
47 | display: inline-block;
48 | .move-up, .move-down {
49 | font-size: 14px;
50 | }
51 | }
52 | .field-checkbox {
53 | overflow: hidden;
54 | display: inline-block;
55 | padding: 4px 2px;
56 | }
57 | .field-item {
58 | display: inline-block;
59 | text-align: center;
60 | white-space: nowrap;
61 | overflow: hidden;
62 | text-overflow: ellipsis;
63 | padding-left: 4px;
64 | }
65 | .point {
66 | position: absolute;
67 | &.left-point {
68 | top: 50%;
69 | left: 0;
70 | }
71 | &.right-point{
72 | top: 50%;
73 | right: 0;
74 | }
75 | }
76 | }
77 | .no-data {
78 | margin: 10px 0;
79 | color: #474747;
80 | text-align: center;
81 | .no-data-icon {
82 | font-size: 36px;
83 | }
84 | }
85 | }
86 | .butterflies-link {
87 | &.focus {
88 | stroke: #F66902;
89 | }
90 | }
91 | .butterflies-arrow {
92 | &.focus {
93 | fill: #F66902;
94 | stroke: #F66902;
95 | }
96 | &:hover {
97 | stroke: #BFBFBF;
98 | fill: #BFBFBF;
99 | }
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/example/webpack.config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const HtmlWebpackPlugin = require('html-webpack-plugin');
4 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
5 | const path = require('path');
6 |
7 | module.exports = {
8 | devtool: 'cheap-module-source-map',
9 | entry: {
10 | app: './index.jsx'
11 | },
12 | output: {
13 | filename: '[name].js',
14 | chunkFilename: '[name].js'
15 | },
16 | resolve: {
17 | modules: [
18 | path.resolve(process.cwd(), 'node_modules'),
19 | path.resolve(process.cwd(), '../node_modules'),
20 | 'node_modules'
21 | ],
22 | extensions: ['.js', '.jsx']
23 | },
24 | module: {
25 | rules: [
26 | {
27 | test: /\.tsx?$/,
28 | use: 'ts-loader',
29 | exclude: /node_modules/,
30 | },
31 | {
32 | test: /\.(js|jsx)$/,
33 | exclude: /(node_modules|bower_components)/,
34 | use: {
35 | loader: 'babel-loader',
36 | options: {
37 | presets: [
38 | '@babel/preset-env',
39 | '@babel/preset-react'
40 | ],
41 | plugins: [
42 | '@babel/plugin-transform-runtime',
43 | '@babel/plugin-transform-modules-commonjs',
44 | '@babel/plugin-proposal-object-rest-spread',
45 | '@babel/plugin-proposal-class-properties',
46 | ]
47 | }
48 | }
49 | },
50 | {
51 | test: /\.(woff|woff2|eot|ttf|otf)$/,
52 | use: {
53 | loader: 'file-loader',
54 |
55 | options: {
56 | name: '[name][hash].[ext]',
57 | outputPath: 'fonts/'
58 | }
59 | }
60 | },
61 | {
62 | test: /\.(less|css)$/,
63 | use: [
64 | {
65 | loader: 'style-loader'
66 | },
67 | {
68 | loader: 'css-loader'
69 | },
70 | {
71 | loader: 'less-loader',
72 | options: {
73 | javascriptEnabled: true
74 | }
75 | }
76 | ]
77 | },
78 | {
79 | test: /\.(png|jpg|gif|svg)$/,
80 | use: [
81 | {
82 | loader: 'url-loader'
83 | }
84 | ]
85 | }
86 | ]
87 | },
88 | plugins: [
89 | new MiniCssExtractPlugin({
90 | filename: '[name].css'
91 | }),
92 | new HtmlWebpackPlugin({
93 | template: './index.html'
94 | })
95 | ],
96 | devServer: {
97 | contentBase: './dist', // 本地服务器所加载的页面所在的目录
98 | historyApiFallback: true, // 不跳转
99 | inline: true, // 实时刷新
100 | index: 'index.html',
101 | port: 8080,
102 | open: true
103 | }
104 | };
105 |
--------------------------------------------------------------------------------
/src/adaptor.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import TableNode from './canvas/node';
4 | import * as _ from 'lodash';
5 |
6 | export let transformInitData = (data) => {
7 | let {
8 | columns, sourceColumns, targetColumns,
9 | sourceData, targetData,
10 | mappingData, type, extraPos,
11 | sortable, checkable, emptyContent, emptyWidth,
12 | sourceClassName, targetClassName, readonly
13 | } = data;
14 |
15 | let _sourceColumns = [];
16 | let _targetColumns = [];
17 | _sourceColumns = sourceColumns ? sourceColumns : columns;
18 | _targetColumns = targetColumns ? targetColumns : columns;
19 |
20 | const _genNodes = (data, nodeType, comType) => {
21 | if (comType === 'single' && data.constructor === Object) {
22 | return [_.assign({
23 | id: nodeType,
24 | type: nodeType,
25 | _sourceColumns,
26 | _targetColumns,
27 | _extraPos: extraPos,
28 | Class: TableNode,
29 | _emptyContent: emptyContent,
30 | _emptyWidth: emptyWidth,
31 | _sourceClassName: sourceClassName,
32 | _targetClassName: targetClassName,
33 | sortable,
34 | checkable,
35 | readonly
36 | }, data)];
37 | } else if (comType === 'mutiply' && data.constructor === Array) {
38 | return data.map((item) => {
39 | return _.assign({
40 | type: nodeType,
41 | _sourceColumns,
42 | _targetColumns,
43 | _extraPos: extraPos,
44 | Class: TableNode,
45 | _emptyContent: emptyContent,
46 | _emptyWidth: emptyWidth,
47 | _sourceClassName: sourceClassName,
48 | _targetClassName: targetClassName,
49 | sortable,
50 | checkable,
51 | readonly
52 | }, item);
53 | });
54 | }
55 | }
56 | let sourceNodes = _genNodes(sourceData, 'source', type);
57 | let targetNodes = _genNodes(targetData, 'target', type);
58 | let edges = mappingData.map((item) => {
59 | return {
60 | id: `${item.source}-${item.target}`,
61 | type: 'endpoint',
62 | sourceNode: item.sourceNode || sourceNodes[0].id,
63 | source: item.source,
64 | targetNode: item.targetNode || targetNodes[0].id,
65 | target: item.target
66 | }
67 | });
68 | return {
69 | nodes: [].concat(sourceNodes).concat(targetNodes),
70 | edges: edges
71 | };
72 | };
73 |
74 | export let transformChangeData = (data, comType) => {
75 | let result = {
76 | mappingData: [],
77 | sourceData: [],
78 | targetData: []
79 | };
80 | let sourceNodes = data.nodes.filter((item) => {
81 | return item.options.type === 'source';
82 | });
83 | let targetNodes = data.nodes.filter((item) => {
84 | return item.options.type === 'target';
85 | });
86 | if (comType === 'single') {
87 | let _sourceNode = sourceNodes[0];
88 | let _targetNode = targetNodes[0];
89 | result.mappingData = data.edges.map((item) => {
90 | return {
91 | source: item.sourceEndpoint.originId,
92 | target: item.targetEndpoint.originId
93 | }
94 | });
95 | result.sourceData = {
96 | id: _sourceNode.id,
97 | title: _.get(_sourceNode, 'options.title'),
98 | fields: _.get(_sourceNode, 'options.fields')
99 | };
100 | result.targetData = {
101 | id: _targetNode.id,
102 | title: _.get(_targetNode, 'options.title'),
103 | fields: _.get(_targetNode, 'options.fields')
104 | };
105 | } else if (comType === 'mutiply') {
106 | result.mappingData = data.edges.map((item) => {
107 | return {
108 | sourceNode: item.sourceNode.id,
109 | targetNode: item.targetNode.id,
110 | source: item.sourceEndpoint.originId,
111 | target: item.targetEndpoint.originId
112 | }
113 | });
114 | result.sourceData = sourceNodes.map((item) => {
115 | return {
116 | id: item.id,
117 | title: _.get(item, 'options.title'),
118 | fields: _.get(item, 'options.fields')
119 | };
120 | });
121 | result.targetData = targetNodes.map((item) => {
122 | return {
123 | id: item.id,
124 | title: _.get(item, 'options.title'),
125 | fields: _.get(item, 'options.fields')
126 | };
127 | });
128 | }
129 | return _.cloneDeep(result);
130 | };
131 |
132 | const getPrimaryKey = (nodeData) => {
133 | let primaryKey = nodeData._sourceColumns.filter((obj) => {
134 | return obj.primaryKey === true;
135 | }).key;
136 |
137 | // fallback if no primary key set, use first entry
138 | if (!primaryKey) {
139 | primaryKey = nodeData._sourceColumns[0].key;
140 | }
141 | return primaryKey;
142 | };
143 |
144 | export let diffPropsData = (newData, oldData) => {
145 | const primaryKey = getPrimaryKey(newData.nodes[0]);
146 |
147 | const isSameId = (a, b) => a[primaryKey] === b[primaryKey];
148 | const isSameCheck = (a, b) => a.id === b.id && a.checked === b.checked;
149 |
150 | let addNodes = _.differenceWith(newData.nodes, oldData.nodes, isSameId);
151 | let rmNodes = _.differenceWith(oldData.nodes, newData.nodes, isSameId);
152 |
153 |
154 | let addFields = [];
155 | let rmFields = [];
156 | let checkedFields = [];
157 | newData.nodes.forEach((_newNode) => {
158 | let _oldNode = _.find(oldData.nodes, _node => _node.id === _newNode.id);
159 | if (_oldNode) {
160 |
161 | let addResult = _.differenceWith(_newNode.fields, _.get(_oldNode, 'options.fields'), isSameId);
162 | let checkResult = _.differenceWith(_newNode.fields, _.get(_oldNode, 'options.fields'), isSameCheck);
163 | if (checkResult.length > 0) {
164 | checkedFields.push({
165 | id: _newNode.id,
166 | type: _newNode.type,
167 | fields: checkResult
168 | });
169 | }
170 | if (addResult.length > 0) {
171 | addFields.push({
172 | id: _newNode.id,
173 | type: _newNode.type,
174 | fields: addResult
175 | });
176 | }
177 | }
178 | });
179 |
180 | oldData.nodes.forEach((_oldNode) => {
181 | let _newNode = _.find(newData.nodes, _node => _node.id === _oldNode.id);
182 | if (_newNode) {
183 | let result = _.differenceWith(_.get(_oldNode, 'options.fields'), _newNode.fields, isSameId);
184 | if (result.length > 0) {
185 | rmFields.push({
186 | id: _newNode.id,
187 | type: _newNode.type,
188 | fields: result
189 | });
190 | }
191 | }
192 | });
193 |
194 | const isSameEdge = (a, b) => {
195 | return (
196 | a.sourceNode === b.sourceNode &&
197 | a.targetNode === b.targetNode &&
198 | a.source === b.source &&
199 | a.target === b.target
200 | );
201 | }
202 |
203 | let addEdges = _.differenceWith(newData.edges, oldData.edges, isSameEdge);
204 | let rmEdges = _.differenceWith(oldData.edges, newData.edges, isSameEdge);
205 |
206 | let result = {
207 | addEdges,
208 | rmEdges,
209 | addNodes,
210 | rmNodes,
211 | addFields,
212 | rmFields,
213 | checkedFields
214 | };
215 |
216 | return result;
217 | };
--------------------------------------------------------------------------------
/example/index.jsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import React from 'react';
4 | import ReactDOM from 'react-dom';
5 | import {BrowserRouter as Router} from 'react-router-dom';
6 | import {Layout, Row, Col, Button} from 'antd';
7 | import * as _ from 'lodash';
8 | import DataMapping from '../src/index.tsx';
9 | import * as SingleNoHeaderData from './mock_data/single-no-header';
10 | import * as SingleWithHeaderData from './mock_data/single-with-header';
11 | import * as MutiplyMappingData from './mock_data/mutiply-mapping';
12 | import * as SinglePointLimit from './mock_data/single-point-limit';
13 | import * as DiffColumns from './mock_data/diff-columns';
14 |
15 | import 'antd/dist/antd.css';
16 | import './index.less';
17 |
18 | const {Header} = Layout;
19 | const {columns1, mappingData1, sourceData1, targetData1} = SingleNoHeaderData;
20 | const {columns2, mappingData2, sourceData2, targetData2} = SingleWithHeaderData;
21 | const {columns3, mappingData3, sourceData3, targetData3} = MutiplyMappingData;
22 | const {columns4, mappingData4, sourceData4, targetData4} = SinglePointLimit;
23 | const {sourceColumns, targetColumns, mappingData5, sourceData5, targetData5} = DiffColumns;
24 |
25 | class Com extends React.Component {
26 | constructor(props) {
27 | super(props);
28 | this.state = {
29 | sourceData1,
30 | targetData1,
31 | mappingData1
32 | }
33 | }
34 | componentDidMount() {
35 | this.setState({
36 | sourceData1: _.cloneDeep(sourceData1),
37 | targetData1: _.cloneDeep(targetData1),
38 | mappingData1: _.cloneDeep(mappingData1),
39 | });
40 | // setTimeout(() => {
41 | // let _sourceData1 = _.cloneDeep(this.state.sourceData1);
42 | // _sourceData1.fields[4].disable = true;
43 | // let _targetData1 = _.cloneDeep(this.state.targetData1);
44 | // _targetData1.fields[5].disable = true;
45 | // _targetData1.fields[6].disable = true;
46 | // _targetData1.fields[7].disable = true;
47 | // this.setState({
48 | // sourceData1: _sourceData1,
49 | // targetData1: _targetData1
50 | // });
51 | // }, 5000);
52 | }
53 | render() {
54 | return (
55 |
56 |
57 | {
71 | this.setState({
72 | sourceData1: _.cloneDeep(data.sourceData),
73 | targetData1: _.cloneDeep(data.targetData),
74 | mappingData1: _.cloneDeep(data.mappingData),
75 | });
76 | }}
77 | // sourceData={this.state.sourceData1}
78 | // targetData={this.state.targetData1}
79 | // mappingData={this.state.mappingData1}
80 | mappingData={this.state.mappingData1}
81 | width={600}
82 | height={600}
83 | onEdgeClick={(data) => {
84 | console.log(data);
85 | }}
86 | />
87 |
88 |
89 |
98 |
99 |
100 |
119 |
120 |
121 |
130 | 暂无数据
131 | {
134 | e.stopPropagation();
135 | console.log('自定义空状态');
136 | }}
137 | >+ 添加字段
138 |
139 | }
140 | width={600}
141 | height={600}
142 | config={{
143 | sortable: true,
144 | checkable: {
145 | source: true,
146 | target: false
147 | },
148 | extraPos: {
149 | paddingLeft: 10,
150 | paddingRight: 10,
151 | paddingTop: 10,
152 | paddingBottom: 10,
153 | paddingCenter: 130
154 | }
155 | }}
156 | />
157 |
158 |
159 |
181 |
182 |
183 |
195 |
196 |
197 | );
198 | }
199 | }
200 |
201 |
202 | ReactDOM.render((
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 | ), document.getElementById('main'));
212 |
--------------------------------------------------------------------------------
/src/canvas/canvas.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import {Canvas} from 'butterfly-dag';
4 | import $ from 'jquery';
5 |
6 | export default class MappingCanvas extends Canvas {
7 | constructor(opts) {
8 | super(opts);
9 | this.extraPos = opts.extraPos;
10 | }
11 | _calcPos() {
12 | let sourceTop = 0 + _.get(this, 'extraPos.paddingTop', 0);
13 | let sourceLeft = 0 + _.get(this, 'extraPos.paddingLeft', 0);
14 | let souceNodes = this.nodes.filter((item) => {
15 | return item.options.type === 'source';
16 | }).map((item) => {
17 | item.moveTo(sourceLeft, sourceTop);
18 | sourceTop += item.height + item.PADDING_VERTICAL;
19 | return item;
20 | });
21 |
22 | let targetTop = 0 + _.get(this, 'extraPos.paddingTop', 0);
23 | let targetLeft = _.get(souceNodes, '[0].width', 0) + _.get(souceNodes, '[0].PADDING_HORIZONTAL', 0) + _.get(this, 'extraPos.paddingLeft', 0);
24 | this.nodes.filter((item) => {
25 | return item.options.type === 'target';
26 | }).forEach((item) => {
27 | item.moveTo(targetLeft, targetTop);
28 | targetTop += item.height + item.PADDING_VERTICAL;
29 | return item;
30 | });
31 | }
32 | _autoResize(type) {
33 | let totalHeight = 0;
34 | let totleWidth = 0;
35 |
36 | let _sourceHeight = 0;
37 | let _targetHeight = 0;
38 |
39 | let souceNodes = this.nodes.filter((item) => {
40 | return item.options.type === 'source';
41 | });
42 | let targetNodes = this.nodes.filter((item) => {
43 | return item.options.type === 'target';
44 | });
45 |
46 | souceNodes.forEach((item) => {
47 | _sourceHeight += item.height + item.PADDING_VERTICAL;
48 | });
49 |
50 | targetNodes.forEach((item) => {
51 | _targetHeight += item.height + item.PADDING_VERTICAL;
52 | });
53 |
54 | // 计算所有节点大小总和
55 | totalHeight = _sourceHeight > _targetHeight ? _sourceHeight : _targetHeight;
56 | totleWidth = (_.get(souceNodes, '[0].width', 0) + _.get(souceNodes, '[0].PADDING_HORIZONTAL', 0) + _.get(targetNodes, '[0].width', 0)) || 200;
57 |
58 | // 计算边缘
59 | totalHeight += _.get(this, 'extraPos.paddingTop', 0) + _.get(this, 'extraPos.paddingBottom', 0);
60 | totleWidth +=_.get(this, 'extraPos.paddingLeft', 0) + _.get(this, 'extraPos.paddingRight', 0);
61 |
62 | if (type === 'width') {
63 | $(this.root).css('width', totleWidth);
64 | }
65 | if (type === 'height') {
66 | $(this.root).css('height', totalHeight);
67 | }
68 | this.updateRootResize();
69 | }
70 | // 纠正获取左右锚点
71 | _getEndpoint = (point) => {
72 | let _node = this.getNode(point.nodeId);
73 | let _point = undefined;
74 | if (_node && point.originId) {
75 | _point = _node.getEndpoint(point.originId);
76 | return _point;
77 | } else {
78 | return point;
79 | }
80 | };
81 | // 改变linked状态
82 | _linkedChain(links) {
83 | links.forEach((edge) => {
84 | let _sourceEndpoint = this._getEndpoint(edge.sourceEndpoint);
85 | let _targetEndpoint = this._getEndpoint(edge.targetEndpoint);
86 | _sourceEndpoint && $(_sourceEndpoint.dom).addClass('link');
87 | _targetEndpoint && $(_targetEndpoint.dom).addClass('link');
88 | });
89 | }
90 | _unLinkedChain(links) {
91 | links.forEach((edge) => {
92 | let _sourceEndpoint = this._getEndpoint(edge.sourceEndpoint);
93 | let _targetEndpoint = this._getEndpoint(edge.targetEndpoint);
94 | _sourceEndpoint && $(_sourceEndpoint.dom).removeClass('link').removeClass('focus');
95 | _targetEndpoint && $(_targetEndpoint.dom).removeClass('link').removeClass('focus');
96 | });
97 | }
98 | // 聚焦链路
99 | _focusChain(point) {
100 | let edges = this._findChain(point);
101 | edges.forEach((item) => {
102 | this._changeFoucsStatus(item, true);
103 | });
104 | }
105 | _unFocusChain(point) {
106 | let edges = this._findChain(point);
107 | edges.forEach((item) => {
108 | this._changeFoucsStatus(item, false);
109 | });
110 | }
111 | _changeFoucsStatus(edge, status) {
112 |
113 | let _sourceEndpoint = this._getEndpoint(edge.sourceEndpoint);
114 | let _targetEndpoint = this._getEndpoint(edge.targetEndpoint);
115 |
116 | if (status) {
117 | $(edge.dom).addClass('focus');
118 | $(edge.arrowDom).addClass('focus');
119 | _sourceEndpoint && $(_sourceEndpoint.dom).addClass('focus');
120 | _targetEndpoint && $(_targetEndpoint.dom).addClass('focus');
121 | } else {
122 | $(edge.dom).removeClass('focus');
123 | $(edge.arrowDom).removeClass('focus');
124 | _sourceEndpoint && $(_sourceEndpoint.dom).removeClass('focus');
125 | _targetEndpoint && $(_targetEndpoint.dom).removeClass('focus');
126 | }
127 | }
128 | _findChain(point) {
129 | let type = point.type;
130 | let neighborEdges = this.getNeighborEdges(point.nodeId);
131 | let targetsEdges = neighborEdges.filter((item) => {
132 | return item[type + 'Node'].id === point.nodeId && item[type + 'Endpoint'].originId === point.id;
133 | });
134 | return targetsEdges;
135 | }
136 |
137 | // 检查连接数量限制
138 | _checkLinkNum(point, targetEdge, type) {
139 | let _linkNums =
140 | this.edges.filter((_edge) => {
141 | return (
142 | _edge[`${type}Node`].id === point.nodeId &&
143 | _edge[`${type}Endpoint`].id === point.id
144 | );
145 | }).length + 1;
146 | let _isValidLink = true;
147 | let _pointLimitedNum = -1;
148 | if (point.limitNum && typeof point.limitNum === "number") {
149 | if (_linkNums > point.limitNum) {
150 | _pointLimitedNum = point.limitNum;
151 | _isValidLink = false;
152 | }
153 | }
154 | if (
155 | point.limitNum &&
156 | Object.prototype.toString.call(point.limitNum) === "[object Object]"
157 | ) {
158 | if (point.limitNum.source && type === "source") {
159 | if (_linkNums > point.limitNum.source) {
160 | _pointLimitedNum = point.limitNum.source;
161 | _isValidLink = false;
162 | }
163 | }
164 | if (point.limitNum.target && type === "target") {
165 | if (_linkNums > point.limitNum.target) {
166 | _pointLimitedNum= point.limitNum.target;
167 | _isValidLink = false;
168 | }
169 | }
170 | }
171 | if (!_isValidLink) {
172 | console.warn(
173 | `id为${point.id}的锚点限制了${_pointLimitedNum}条连线`
174 | );
175 | targetEdge && targetEdge.destroy();
176 | this._dragEdges = [];
177 | this._dragType = null;
178 | }
179 | return _isValidLink;
180 | }
181 | addFields(data) {
182 | data.forEach((item) => {
183 | let node = this.getNode(item.id);
184 | node && node.addFields(item.fields);
185 | });
186 | }
187 | removeFields(data) {
188 | data.forEach((item) => {
189 | let node = this.getNode(item.id);
190 | node && node.removeFields(item.fields);
191 | });
192 | }
193 | updateDisableStatus(newData) {
194 | (newData.nodes || []).forEach((newNode) => {
195 | let oldNode = _.find(this.nodes, (item) => {
196 | return item.id === newNode.id;
197 | });
198 |
199 | if (oldNode) {
200 | let oldFields = oldNode.options.fields;
201 | let newFields = newNode.fields;
202 | oldFields.forEach((oldField) => {
203 | let newField = _.find(newFields, (item) => {
204 | return item.id === oldField.id;
205 | });
206 | if (newField && newField.disable !== oldField.disable) {
207 | oldField.disable = newField.disable;
208 | let pos = oldNode.options.type === 'source' ? 'right' : 'left';
209 | oldNode.endpoints.filter((item) => {
210 | return item.id === oldField.id || item.id === `${oldField.id}-${pos}`;
211 | }).forEach((item) => {
212 | item.options.disable = newField.disable;
213 | if (newField.disable) {
214 | $(item.dom).addClass('disable');
215 | } else {
216 | $(item.dom).removeClass('disable');
217 | }
218 | });
219 | }
220 | });
221 | }
222 | });
223 | }
224 | updateCheckedStatus(checkFields) {
225 | checkFields.forEach((item) => {
226 | let node = this.getNode(item.id);
227 | node && node.updateCheckedStatus(item.fields);
228 | })
229 | }
230 | };
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | 一个基于React的数据/字段映射组件
3 |
4 |
5 | [](https://www.npmjs.com/package/react-data-mapping)
6 | [](https://www.npmjs.com/package/react-data-mapping)
7 | [](https://www.npmjs.com/package/react-data-mapping)
8 | [](https://github.com/aliyun/react-data-mapping/blob/master/LICENSE)
9 |
10 | [English](./README.en-US.md) | 简体中文
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | ## ✨ 特性
22 |
23 | * 支持定制字段属性
24 | * 支持表名定制
25 | * 支持字段连接数量限制
26 | * 支持字段排序
27 | * 支持延迟渲染,自动适配高宽,四周留白等配置
28 | * 支持空字段内容定制
29 |
30 | ## 🔨快速本地DEMO
31 |
32 | ```
33 |
34 | git clone git@github.com:aliyun/react-data-mapping.git
35 | npm install
36 | cd example
37 | npm install
38 | npm start
39 | ```
40 |
41 | ## 📦 安装
42 |
43 | ```
44 |
45 | npm install react-data-mapping
46 | ```
47 |
48 | ## API:
49 |
50 | ### DataMapping属性
51 |
52 | | 参数 | 说明 | 类型 | 默认值 |
53 | |-----------------|-------------------------------|-------------------------------------|--------------------------------------|
54 | | width | 组件宽度 | number | 默认500,自适应的话可以设置"auto" |
55 | | height | 组件高度 | number | 默认500,自适应的话可以设置"auto" |
56 | | type| 映射类型 | string | `single` | `mutiply` ,默认 `single` |
57 | | className | 组件类名 | string | - |
58 | | sourceClassName | 来源表类名 | string | - |
59 | | targetClassName | 目标表类名 | string | - |
60 | | columns | 每列的属性 | [Columns](#columns-type) | Array<Columns> | undefined |
61 | | sourceData | 来源表数据 | [SourceData](#source-data) | Object | Array<SourceData> | undefined |
62 | | targetData | 目标表数据 | [TargetData](#target-data) | Object | Array<TargetData> | undefined |
63 | | mappingData | 初始映射关系,见[mappingData Prop](#mapping-data) | array | [ ] |
64 | | emptyContent | 当表字段为空时显示内容 | string | ReactNode | - |
65 | | emptyWidth | 当表字段为空时,表容器的宽度 | string | number | 150 |
66 | | config | 组件的额外属性配置,见[config Prop](#config) | object | { } | |
67 | | isConnect | 每次连线前触发,判断是否可以连线 | function(edge): boolean |
68 | | onChange | 每次连线触发事件 | function |
69 | | onRowMouseOver | 鼠标移入某一行数据时触发 | function(row) |
70 | | onRowMouseOut | 鼠标移出某一行数据时触发 | function(row) |
71 | | onEdgeClick | 点击连线时触发 | function(row) |
72 | | readonly | 是否只读 | boolean | 默认false |
73 |
74 |
75 |
76 | ### Column
77 |
78 | 列描述数据对象,是Columns中的一项
79 |
80 | | 参数 | 说明 | 类型 | 默认值 |
81 | |------------|-------------------------|---------|------------------------|
82 | | key | 列数据在数据项中对应的路径 | string| - |
83 | | title | 列头显示文字 |string| - |
84 | | width | 列宽度 | number| - |
85 | | primaryKey | 此属性是否为该组数据唯一标识 | boolean| `必须且仅有一个属性为true` |
86 | | render | 自定义渲染函数,参数分别为当前行的值,当前行数据,行索引 | function(text, record, index) {}| - |
87 |
88 |
89 |
90 | ### sourceData
91 |
92 | 来源表数据,当[type](#data-mapping-type)为 `single` 时,sourceData的类型为{ };当[type](#data-mapping-type)为 `mutiply` 时,sourceData的类型为[ ]
93 |
94 | | 参数 | 说明 | 类型 | 默认值 |
95 | |--------------------------------------|--------------------------------------------------|---------|--------------------------|
96 | | id | 列标识, `single` 类型下,id可不填, `mutiply` 为必填 | string | - |
97 | | title | 列标题 | string | - |
98 | | fileds | 数据数组 | array | - |
99 | | checked | 勾选框是否已勾选 | boolean | false |
100 | | disable | 禁止连线 | boolean | false |
101 |
102 |
103 |
104 | ### targetData
105 |
106 | 目标表数据,当[type](#data-mapping-type)为 `single` 时,targetData的类型为{ };当[type](#data-mapping-type)为 `mutiply` 时,targetData的类型为[ ],属性详情见[sourceData](#source-data)
107 |
108 |
109 |
110 | ### mappingData
111 |
112 | | 参数 | 说明 | 类型 |
113 | |------------|-------------------------|---------|
114 | | source | 来源表当前行数据的唯一标识 | - |
115 | | target | 目标表当前行数据的唯一标识 | - |
116 | | sourceNode | 来源表的id,见[sourceData ID](#source-data-id) | string|
117 | | targetNode | 目标表的id,见[targetData ID](#target-data) | string |
118 |
119 |
120 |
121 | ### config
122 |
123 | 组件的额外属性配置
124 |
125 | | 参数 | 说明 | 类型 | 默认值 |
126 | |------------|-------------------------|---------|------------------------|
127 | | delayDraw | 延迟渲染,此组件一定要确保画布容器渲染(包括动画执行)完毕才能渲染, 否则坐标都产生偏移, 如:antd的modal的动画 | number | 0|
128 | | extraPos | 画布渲染的时候会留padding, | [extraPos Prop](#extraPos-prop) { } | - |
129 | | sortable | 排序支持 | boolean | object | - |
130 | | linkNumLimit | 连线数量支持 | number | object | - |
131 | | checkable | 来源表,目标表是否有勾选框 | [checkable Prop](#checkable-prop) { } | - |
132 |
133 |
134 |
135 | ### extraPos
136 |
137 | 画布渲染的时候预留padding
138 |
139 | | 参数 |说明 | 类型 | 默认值|
140 | |----------- |----------------|-------------|------|
141 | |paddingLeft | 左侧padding间距 | number | 0 |
142 | |paddingRight | 右侧padding间距 | number | 0 |
143 | |paddingTop | 顶部padding间距 | number | 0 |
144 | |paddingBottom | 底部padding间距 | number | 0 |
145 | |paddingCenter | 水平间距 | number | 150 |
146 | |nodeVerticalPadding | 节点垂直间距 | number | 10 |
147 | |rowHeight | 节点每行的高度 | number | 26 |
148 |
149 |
150 |
151 | ### checkable
152 |
153 | 来源表,目标表是否有勾选框
154 |
155 | | 参数 |说明 | 类型 | 默认值|
156 | |----------- |----------------|-------------|------|
157 | |source | 源表是否有勾选框 | boolean | false |
158 | |target | 目标表是否有勾选框 | boolean | false |
159 |
160 | ## 🔗API
161 |
162 | ``` javascript
163 | interface columns { // 设置每列的属性
164 | title ? : string; // 每列的title,类似thead的概念
165 | key: string; // 每列的唯一标志,对应数据上的key值
166 | width ? : number; // 每列宽度
167 | primaryKey: boolean // 这列的key对应的value是否作为键值对
168 | }
169 |
170 | interface config {
171 | delayDraw: number; // 延迟渲染,此组件一定要确保画布容器渲染(包括动画执行)完毕才能渲染,否则坐标都产生偏移,如:antd的modal的动画
172 | extraPos ? : { // 画布渲染的时候会留padding
173 | paddingLeft ? : number,
174 | paddingRight ? : number,
175 | paddingTop ? : number,
176 | paddingBottom ? : number,
177 | paddingCenter ? : number,
178 | },
179 | sortable ? : boolean | { // 排序支持,假如是true,会整个画布都支持排序
180 | source ? : boolean, // 假如是true,单纯左侧来源表支持排序
181 | target ? : boolean // 假如是true,单纯左侧目标表支持排序
182 | },
183 | linkNumLimit ? : number | { // 连线数量支持,假如是number,会整个画布都支持n条连线
184 | source ? : number, // 假如是number,单纯左侧来源表支持n条连线
185 | target ? : number // 假如是number,单纯左侧来源表支持n条连线
186 | },
187 | checkable ?: { // 源表目标表是否有勾选框
188 | source ? : boolean, // 假如是true,单纯左侧来源表支持勾选框
189 | target ? : boolean
190 | }
191 | }
192 |
193 | interface ComProps { // 组件props属性
194 | width ? : number | string; // 组件的宽度,自适应的话可以设置"auto"
195 | height ? : number | string; // 组件的高度,自适应的话可以设置"auto"
196 | type ? : string; // "single"or"mutiply",单表映射(上图一) or 多表映射(上图二)
197 | className ? : string; // 组件类名
198 | sourceClassName ? : string; // 来源表类名
199 | targetClassName ? : string; // 目标表类名
200 | columns: Array < columns > ; // 请参考上述interface columns
201 | sourceData: Array < any > | Object; // 单表映射对应Object,多表映射Array,可参考demo
202 | targetData: Array < any > | Object; // 单表映射对应Object,多表映射Array,可参考demo
203 | mappingData: Array < any > ; // 初始化对应关系数据,可参考demo
204 | emptyContent ? : string | JSX.Element; // 当表字段为空时显示内容
205 | emptyWidth ? : number | string; // 当表字段为空时表容器宽度
206 | isConnect?(edge: any): boolean; // 每次连线前触发isConnect,返回true则进行连线,false则不会
207 | onChange(data: any): void; // 每次连线都是触发onChange事件
208 | onRowMouseOver?(row:any):void, // 鼠标移入某一行数据时触发
209 | onRowMouseOut?(row:any):void, // 鼠标移出某一行数据时触发
210 | };
211 | ```
212 |
213 | ``` jsx
214 | import ButterflyDataMapping from 'react-data-mapping';
215 | import 'react-data-mapping/dist/index.css';
216 |
217 |
229 | ```
230 |
231 | 如需要更多定制的需求,您可以提issue或者参考[Butterfly](https://github.com/alibaba/butterfly)来定制您需要的需求
232 |
--------------------------------------------------------------------------------
/README.en-US.md:
--------------------------------------------------------------------------------
1 |
2 | React-based data/field mapping Component
3 |
4 |
5 | English | [简体中文](./README.md)
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | ## ✨ Features
17 |
18 | * support for custom field attributes
19 | * support custom table name
20 | * support field connection number limit
21 | * support field sorting
22 | * support delay rendering, automatic adaptation of height and width, blank padding around
23 | * support custom empty field content
24 |
25 | ## 🔨QUCIK DEMO LOCAL
26 |
27 | ```
28 |
29 | git clone git@github.com:aliyun/react-data-mapping.git
30 | npm install
31 | cd example
32 | npm install
33 | npm start
34 | ```
35 |
36 | ## 📦 Install
37 |
38 | ```
39 |
40 | npm install react-data-mapping
41 | ```
42 |
43 | ## API:
44 |
45 | ### DataMapping属性
46 |
47 | | Property | Description | Type | Default |
48 | |-----------------|-------------------------------|--------------|------------------------------------------------------------------------|
49 | | width | Component width | number | Default 500, you can set "auto" for adaptive |
50 | | height | Component height | number | Default 500, you can set "auto" for adaptive |
51 | | type | mapping type | string | `single` | `mutiply`, default `single`|
52 | | className | Component className | string | - |
53 | | sourceClassName | Source table className | string | - |
54 | | targetClassName | Target table className | string | - |
55 | | columns | Column props |[Columns](#columns-type) | Array<Columns> | undefined |
56 | | sourceData | Source table data |[SourceData](#source-data) | Object | Array<SourceData> | undefined |
57 | | targetData | Target table data |[TargetData](#target-data) | Object | Array<TargetData> | undefined |
58 | | mappingData | Init mapping data, [mappingData Prop](#mapping-data) | array | [ ] |
59 | | emptyContent | Show content when table field is empty | string | ReactNode | - |
60 | | emptyWidth | Table container width when table field is empty, [config Prop](#config) | string | number | 150 |
61 | | config | The extra configuration of components,please reviewe the detailed API below | object | {} |
62 | | isConnect | Event triggered before each edge connection to determine whether it can be connected | function(edge): boolean |
63 | | onChange | Event triggered by connection | function |
64 | | onRowMouseOver | Event triggered when the mouse moves onto a row of data | function(row) |
65 | | onRowMouseOut | Event triggered when the mouse moves out of a row of data | function(row)
66 | | onEdgeClick | Event triggered when the connection is clicked | function(row) |
67 | | readonly | Read only | boolean | Default false |
68 |
69 |
70 |
71 | ### Column
72 |
73 | A column describes a data object and is an item in a Columns.
74 |
75 | | Property | Description | Type | Default |
76 | |-----------------|--------------------------------|---------|--------------------------|
77 | | key | The path of column data in a data item| string| - |
78 | | title | The column header displays text |string| - |
79 | | width | The column width | number| - |
80 | | primaryKey | Whether this property is uniquely identified for the set of data | boolean| `必须且仅有一个属性为true` |
81 | | render |Custom rendering function, parameters are the value of the current row, the current row data, row index | function(text, record, index) {}| - |
82 |
83 |
84 |
85 |
86 |
87 | ### sourceData
88 |
89 | Source table data,when [type](#data-mapping-type) is `single` , the sourceData type is { }; when [type](#data-mapping-type)为 `mutiply` , the sourceData type is [ ].
90 |
91 |
92 | | Property | Description | Type | Default |
93 | |-------------------------------|----------------------------------------|---------|--------------------------|
94 | | id | Column identifies, when [type](#data-mapping-type) is `single`,the id is not required, when the [type](#data-mapping-type) is `mutiply`, the id is required| string | - |
95 | | title | Column title | string | - |
96 | | fileds | Data record array to be displayed | array | - |
97 | | checked | Is it checked | boolean | false |
98 | | disable | No connection | boolean | false |
99 |
100 |
101 |
102 | ### targetData
103 |
104 | Target table data, when [type](#data-mapping-type) is `single` , the targetData type is { }, when [type](#data-mapping-type) is `mutiply` , the targetData type is [ ], Please check [sourceData](#source-data)
105 |
106 |
107 |
108 | ### mappingData
109 |
110 | | Property | Description | Type |
111 | |------------|-------------------------|---------|
112 | | source | Unique identification of the current row data in the source table | - |
113 | | target | Unique identification of the current row data in the target table | - |
114 | | sourceNode | The ID of the source table, Please check [sourceData ID](#source-data-id)| string |
115 | | targetNode | The ID of the target table, Please check [targetData ID](#target-data)| string |
116 |
117 |
118 |
119 | ### config
120 |
121 | The extra configuration of components
122 |
123 | | Property | Description | Type | Default |
124 | |------------|-------------------------|---------|------------------------|
125 | | delayDraw | Delayed rendering. This component must ensure that the canvas container rendering (including animation execution) is completed before rendering, otherwise the coordinates will be offset, for example:Animation of Ant Design Modal | number | 0 |
126 | | extraPos | Padding is reserved when rendering the canvas | [extraPos Prop](#extraPos-prop) { } | - |
127 | | sortable | Sorter | boolean | object | - |
128 | | linkNumLimit | Number of lines limited | number | object | - |
129 | | checkable | Support check box | [checkable Prop](#checkable-prop) { } | - |
130 |
131 |
132 |
133 | ### extraPos
134 |
135 | Padding is reserved when rendering the canvas
136 |
137 | | Property | Description | Type | Default |
138 | |----------- |-----------------------------|------------|-------|
139 | |paddingLeft | Padding spacing on the left | number | 0 |
140 | |paddingRight | Padding spacing on the right | number | 0 |
141 | |paddingTop | Padding spacing on the top | number | 0 |
142 | |paddingBottom | Padding spacing on the bottom | number | 0 |
143 | |paddingCenter | Center spacing | number | 150 |
144 | |nodeVerticalPadding | Node vertical spacing | number | 10 |
145 | |rowHeight | field row height | number | 26 |
146 |
147 |
148 |
149 | ### checkable
150 |
151 | Table supports checkbox
152 |
153 | | Property | Description | Type | Default |
154 | |----------- |----------------|-------------|------|
155 | |source | left table supports checkbox | boolean | false |
156 | |target | right table supports checkbox | boolean | false |
157 |
158 | ## 🔗API
159 |
160 | ``` javascript
161 | interface columns { // setting the attributes of each column
162 | title ? : string; // the title of each column, similar to the concept of thead
163 | key: string; // the unique mark of each column, corresponding to the key value on the data
164 | width ? : number; // width of each column
165 | primaryKey: boolean // whether the value corresponding to the key in this column is used as a unique sign
166 | render?(text: any, record: any, index: number): void; // Custom rendering function
167 | }
168 |
169 | interface config {
170 | delayDraw: number; // Delayed rendering, this component must ensure that the canvas container rendering (including animation execution) is completed before rendering, otherwise the coordinates will be offset, such as: antd's modal animation
171 | extraPos ? : { // Padding is reserved when the canvas is rendered
172 | paddingLeft ? : number,
173 | paddingRight ? : number,
174 | paddingTop ? : number,
175 | paddingBottom ? : number,
176 | paddingCenter ? : number,
177 | },
178 | sortable ? : boolean | { // Sorting support, if it is true, the canvas will support sorting
179 | source ? : boolean, // If it is true, only the left source table supports sorting
180 | target ? : boolean // If it is true, only pure right target table supports sorting
181 | },
182 | linkNumLimit ? : number | { // Connection Number support, if it is number, the canvas supports n connections
183 | source ? : number, // If it is number, only the left source table supports n connections
184 | target ? : number // If it is number, only the left target table supports n connections
185 | },
186 | checkable ?: { // table supports checkbox
187 | source ? : boolean, // // If it is true, only pure right target table supports checkbox
188 | target ? : boolean
189 | }
190 | }
191 |
192 | interface ComProps { // component props
193 | width ? : number; // component width
194 | height ? : number; // component height
195 | type ? : string; // "single"or"mutiply", single-table mapping (above pic one) or multi-table mapping (above pic two)
196 | className ? : string; // component className
197 | sourceClassName ? : string; // source table className
198 | targetClassName ? : string; // target table className
199 | columns: Array < columns > ; // please refer to the above interface columns
200 | sourceData: Array < any > | Object; // single-table mapping corresponds to Object, multi-table mapping Array, please refer to demo
201 | targetData: Array < any > | Object; // single-table mapping corresponds to Object, multi-table mapping Array, please refer to demo
202 | mappingData: Array < any > ; // initialize correspondence data, please refer to demo
203 | emptyContent ? : string | JSX.Element; // show content when table field is empty
204 | emptyWidth ? : number | string; // table container width when table field is empty
205 | isConnect?(edge: any): boolean; // isConnect event is triggered before you connect an edge, return true, it will connect, and false will not
206 | onChange(data: any): void // onChange event is triggered every time you connect edge
207 | onRowMouseOver?(row:any):void, // onRowMouseOver event is triggered when you move the cursor onto a row of data
208 | onRowMouseOut?(row:any):void, // onRowMouseOver event is triggered when you move the cursor out of a row of data
209 | };
210 | ```
211 |
212 | ``` tsx
213 | import ButterflyDataMapping from 'react-data-mapping';
214 | import 'react-data-mapping/dist/index.css';
215 |
216 |
228 | ```
229 |
230 | If you need more customized requirements, you can refer to issue or [butterfly](https://github.com/alibaba/butterfly/blob/master/README.en-US.md) to customize your needs
231 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import $ from 'jquery';
4 | import * as React from 'react';
5 | import * as ReactDOM from 'react-dom';
6 | import './index.less';
7 | import Canvas from './canvas/canvas';
8 | import 'butterfly-dag/dist/index.css';
9 | import {transformInitData, transformChangeData, diffPropsData} from './adaptor';
10 | import * as _ from 'lodash';
11 |
12 | // 跟antd的table的column的概念类似
13 | interface columns {
14 | title?: string,
15 | key: string,
16 | width?: number,
17 | primaryKey: boolean,
18 | render?(text: any, record: any, index: number): void
19 | }
20 |
21 | interface config {
22 | delayDraw: number,
23 | extraPos?: {
24 | paddingLeft?: number,
25 | paddingRight?: number,
26 | paddingTop?: number,
27 | paddingBottom?: number,
28 | paddingCenter?: number,
29 | },
30 | sortable?: boolean | {
31 | source?: boolean,
32 | target?: boolean
33 | },
34 | checkable?: boolean | {
35 | source?: boolean,
36 | target?: boolean
37 | },
38 | linkNumLimit?: number | {
39 | source?: number,
40 | target?: number
41 | }
42 | }
43 |
44 | interface ComProps {
45 | width?: number | string,
46 | height?: number | string,
47 | type?: string,
48 | className?: string,
49 | sourceClassName?: string,
50 | targetClassName?: string,
51 | columns: Array,
52 | sourceColumns: Array,
53 | targetColumns: Array,
54 | sourceData: Array | Object,
55 | targetData: Array | Object,
56 | mappingData: Array,
57 | readonly?: boolean,
58 | config?: config,
59 | emptyContent?: string | JSX.Element,
60 | emptyWidth?: number | string,
61 | isConnect?(edge: any): boolean,
62 | onLoaded(canvas: any): void,
63 | onChange(data: any): void,
64 | onRowMouseOver?(row:any):void,
65 | onRowMouseOut?(row:any):void,
66 | onEdgeClick?(edge: any): void,
67 | onCheckChange(data: any): void,
68 | };
69 |
70 | export default class DataMapping extends React.Component {
71 | protected canvas: any;
72 | private _isRendering: any;
73 | private _isOnchange: boolean;
74 | private _readOnly: boolean;
75 | props: any;
76 | constructor(props: ComProps) {
77 | super(props);
78 | this.canvas = null;
79 | this._isRendering = false;
80 | this._isOnchange = false;
81 | this._readOnly = false;
82 | }
83 | componentDidMount() {
84 | let root = ReactDOM.findDOMNode(this) as HTMLElement;
85 |
86 | if (this.props.width !== undefined || this.props.width !== 'auto') {
87 | root.style.width = (this.props.width || 500) + 'px';
88 | }
89 | if (this.props.height !== undefined || this.props.height !== 'auto') {
90 | root.style.height = (this.props.height || 500) + 'px';
91 | }
92 |
93 | this._readOnly = this.props.readonly || false;
94 |
95 | let result = transformInitData({
96 | columns: this.props.columns,
97 | sourceColumns: this.props.sourceColumns,
98 | targetColumns: this.props.targetColumns,
99 | type: this.props.type || 'single',
100 | sortable: _.get(this.props, 'config.sortable') || false,
101 | checkable: _.get(this.props, 'config.checkable') || false,
102 | sourceData: _.cloneDeep(this.props.sourceData),
103 | targetData: _.cloneDeep(this.props.targetData),
104 | mappingData: _.cloneDeep(this.props.mappingData),
105 | extraPos: _.get(this.props, 'config.extraPos'),
106 | linkNumLimit: _.get(this.props, 'config.linkNumLimit'),
107 | emptyContent: this.props.emptyContent,
108 | emptyWidth: this.props.emptyWidth,
109 | sourceClassName: this.props.sourceClassName || '',
110 | targetClassName: this.props.targetClassName || '',
111 | readonly: this._readOnly
112 | });
113 |
114 | let canvasObj = {
115 | root: root,
116 | disLinkable: true,
117 | linkable: true,
118 | draggable: false,
119 | zoomable: false,
120 | moveable: false,
121 | theme: {
122 | edge: {
123 | type: 'endpoint',
124 | shapeType: 'AdvancedBezier',
125 | arrow: true,
126 | isExpandWidth: true,
127 | arrowPosition: 1,
128 | arrowOffset: 5
129 | },
130 | endpoint: {
131 | limitNum: undefined,
132 | expandArea: {
133 | left: 0,
134 | right: 0,
135 | top: 0,
136 | botton: 0
137 | }
138 | }
139 | },
140 | extraPos: _.get(this.props, 'config.extraPos')
141 | };
142 | if (!!this.props.readonly) {
143 | canvasObj.disLinkable = false;
144 | canvasObj.linkable = false;
145 | }
146 | let _linkNumLimit = _.get(this.props, 'config.linkNumLimit')
147 | if (typeof _linkNumLimit === 'number' && !isNaN(_linkNumLimit)) {
148 | canvasObj.theme.endpoint.limitNum = _linkNumLimit;
149 | }
150 | if (Object.prototype.toString.call(_linkNumLimit) === '[object Object]'){
151 | canvasObj.theme.endpoint.limitNum = _linkNumLimit;
152 | }
153 | this.canvas = new Canvas(canvasObj);
154 | this._isRendering = new Promise((resolve, reject) => {
155 | setTimeout(() => {
156 | this.canvas.draw(result, () => {
157 | this.canvas._calcPos();
158 | if (this.props.width === 'auto') {
159 | this.canvas._autoResize('width');
160 | }
161 | if (this.props.height === 'auto') {
162 | this.canvas._autoResize('height');
163 | }
164 | resolve();
165 | this.props.onLoaded && this.props.onLoaded(this.canvas);
166 | // 做滚动中修正
167 | this.canvas._coordinateService._calcScrollPos(true);
168 | this.canvas.nodes.forEach((item) => {
169 | item.endpoints.forEach((point) => {
170 | point.updatePos();
171 | })
172 | });
173 | this.canvas.edges.forEach((item) => {
174 | item.redraw();
175 | });
176 | });
177 | this._addEventListener();
178 | }, _.get(this.props, 'config.delayDraw', 0));
179 | });
180 | }
181 | shouldComponentUpdate(newProps: ComProps, newState: any) {
182 |
183 | const _update = () => {
184 | let result = transformInitData({
185 | columns: newProps.columns,
186 | sourceColumns: newProps.sourceColumns,
187 | targetColumns: newProps.targetColumns,
188 | type: newProps.type || 'single',
189 | sortable: _.get(newProps, 'config.sortable') || false,
190 | checkable: _.get(this.props, 'config.checkable') || false,
191 | sourceData: _.cloneDeep(newProps.sourceData),
192 | targetData: _.cloneDeep(newProps.targetData),
193 | mappingData: _.cloneDeep(newProps.mappingData),
194 | extraPos: _.get(newProps, 'config.extraPos'),
195 | linkNumLimit: _.get(newProps, 'config.linkNumLimit'),
196 | emptyContent: newProps.emptyContent,
197 | emptyWidth: newProps.emptyWidth,
198 | sourceClassName: newProps.sourceClassName || '',
199 | targetClassName: newProps.targetClassName || '',
200 | readonly: this.props.readonly || false
201 | });
202 | let diffInfo = diffPropsData(result, {
203 | nodes: this.canvas.nodes,
204 | edges: this.canvas.edges.map((item) => {
205 | return _.assign(item.options, {
206 | source: (item.options.source || '').replace('-right', ''),
207 | target: (item.options.target || '').replace('-left', ''),
208 | });
209 | })
210 | });
211 |
212 | if (diffInfo.rmEdges && diffInfo.rmEdges.length > 0) {
213 | this.canvas.removeEdges(diffInfo.rmEdges.map((item) => item.id));
214 | }
215 |
216 | if (diffInfo.addEdges && diffInfo.addEdges.length > 0) {
217 | this.canvas.addEdges(diffInfo.addEdges);
218 | }
219 |
220 | if (diffInfo.rmNodes && diffInfo.rmNodes.length > 0) {
221 | this.canvas.removeNodes(diffInfo.rmNodes);
222 | }
223 |
224 | if (diffInfo.addNodes && diffInfo.addNodes.length > 0) {
225 | this.canvas.addNodes(diffInfo.addNodes);
226 | this.canvas._calcPos();
227 | }
228 |
229 | if (diffInfo.rmFields && diffInfo.rmFields.length > 0) {
230 | this.canvas.removeFields(diffInfo.rmFields);
231 | }
232 |
233 | if (diffInfo.addFields && diffInfo.addFields.length > 0) {
234 | this.canvas.addFields(diffInfo.addFields);
235 | }
236 |
237 | if (diffInfo.checkedFields && diffInfo.checkedFields.length > 0) {
238 | this.canvas.updateCheckedStatus(diffInfo.checkedFields);
239 | }
240 |
241 | if (this._readOnly != newProps.readonly) {
242 | this._readOnly = newProps.readonly;
243 | this.canvas.setLinkable(!!!newProps.readonly);
244 | this.canvas.setDisLinkable(!!!newProps.readonly);
245 | (this.canvas.nodes || []).forEach((item) => item.setReadOnly(newProps.readonly));
246 | }
247 |
248 | this.canvas.updateDisableStatus(result);
249 | }
250 |
251 | if (this._isRendering) {
252 | this._isRendering.then(() => {
253 | _update();
254 | })
255 | this._isRendering = false;
256 | } else {
257 | _update();
258 | }
259 |
260 | return false;
261 | }
262 | onChange() {
263 | if (!this._isOnchange) {
264 | this._isOnchange = true;
265 | setTimeout(() => {
266 | let result = transformChangeData(this.canvas.getDataMap(), this.props.type || 'single');
267 | this.props.onChange && this.props.onChange(result);
268 | this._isOnchange = false;
269 | }, 0);
270 | }
271 | }
272 | _genClassName() {
273 | let classname = '';
274 | if (this.props.className) {
275 | classname = this.props.className + ' butterfly-data-mapping';
276 | } else {
277 | classname = 'butterfly-data-mapping';
278 | }
279 | return classname;
280 | }
281 | _addEventListener() {
282 | let _addLinks = (links: any) => {
283 | let newLinkOpts = links.map((item: any) => {
284 | let _oldSource = _.get(item, 'sourceEndpoint.id', '');
285 | let _oldTarget = _.get(item, 'targetEndpoint.id', '');
286 | let _newSource = _oldSource.indexOf('-right') !== -1 ? _oldSource : _oldSource + '-right';
287 | let _newTarget = _oldTarget.indexOf('-left') !== -1 ? _oldTarget : _oldTarget + '-left';
288 | return {
289 | id: item.options.id || `${item.options.sourceNode}-${item.options.targetNode}`,
290 | sourceNode: item.options.sourceNode,
291 | targetNode: item.options.targetNode,
292 | source: _newSource,
293 | target: _newTarget,
294 | type: 'endpoint'
295 | };
296 | });
297 | this.canvas.removeEdges(links, true);
298 | newLinkOpts = newLinkOpts.filter((item) => {
299 | let targetNode = this.canvas.getNode(item.targetNode);
300 | let targetEndpoint = targetNode.getEndpoint(item.target);
301 | let sourceEndpoint = targetNode.getEndpoint(item.source);
302 | let result = this.canvas._checkLinkNum(targetEndpoint, undefined, 'target');
303 | // 取消link状态
304 | if(!result) {
305 | sourceEndpoint && $(sourceEndpoint.dom).removeClass('link');
306 | }
307 | return result;
308 | });
309 | return this.canvas.addEdges(newLinkOpts, true);
310 | }
311 | let _isInit = true;
312 | this.canvas.on('system.link.connect', (data: { links: any; }) => {
313 | let addEdges = _addLinks(data.links || []);
314 | let result = [];
315 | addEdges.forEach((item) => {
316 | let isConnect = true;
317 | this.props.isConnect && (isConnect = this.props.isConnect(item));
318 | if (isConnect) {
319 | result.push(item);
320 | } else {
321 | this.canvas.removeEdge(item, true);
322 | }
323 | });
324 | if (!_isInit) {
325 | this.onChange();
326 | }
327 | _isInit = false;
328 | this.canvas._linkedChain(result);
329 | });
330 |
331 | this.canvas.on('system.link.reconnect', (data: { addLinks: any, delLinks: any }) => {
332 | let addEdges = _addLinks(data.addLinks || []);
333 | let result = [];
334 | addEdges.forEach((item) => {
335 | let isConnect = true;
336 | this.props.isConnect && (isConnect = this.props.isConnect(item));
337 | if (isConnect) {
338 | result.push(item);
339 | } else {
340 | this.canvas.removeEdge(item, true);
341 | }
342 | });
343 | this.onChange();
344 | this.canvas._unLinkedChain(data.delLinks);
345 | this.canvas._linkedChain(result);
346 | });
347 |
348 | this.canvas.on('system.links.delete', (data: { links: any; }) => {
349 | this.onChange();
350 | this.canvas._unLinkedChain(data.links);
351 | });
352 |
353 | // 线段删除特殊处理
354 | this.canvas.on('custom.endpoint.dragNode', (data: { data: any; }) => {
355 | let point = data.data;
356 | let node = this.canvas.getNode(point.nodeId);
357 | let linkedPoint = node.getEndpoint(point.id + '-left', 'target');
358 | this.canvas.emit('InnerEvents', {
359 | type: 'endpoint:drag',
360 | data: linkedPoint
361 | });
362 | });
363 | // 连线特殊处理
364 | this.canvas.on('system.drag.move', (data: any) => {
365 | let dragEdge = _.get(data, 'dragEdges[0]');
366 | let sourcePointId = _.get(dragEdge, 'sourceEndpoint.id', '');
367 | if (sourcePointId.indexOf('right') === -1) {
368 | let souceNode = _.get(dragEdge, 'sourceNode');
369 | let newSourcePoint = souceNode.getEndpoint(sourcePointId + '-right');
370 | dragEdge.sourceEndpoint = newSourcePoint;
371 | dragEdge.options.sourceEndpoint = newSourcePoint;
372 | this.canvas._checkLinkNum(newSourcePoint, dragEdge, 'source');
373 | }
374 | });
375 | // 聚焦链路
376 | this.canvas.on('custom.endpoint.focus', (data: { point: any; }) => {
377 | this.canvas._focusChain(data.point);
378 | this.props.onRowMouseOver && this.props.onRowMouseOver(data.point);
379 | });
380 | // 失焦链路
381 | this.canvas.on('custom.endpoint.unFocus', (data: { point: any; }) => {
382 | this.canvas._unFocusChain(data.point);
383 | this.props.onRowMouseOut && this.props.onRowMouseOut(data.point);
384 | });
385 |
386 | // 字段重新排列
387 | this.canvas.on('custom.field.sort', (data?: any) => {
388 | const {nodeId, pointIds} = data;
389 | let node = this.canvas.getNode(nodeId);
390 | if (!node) {
391 | return;
392 | }
393 | pointIds.forEach((pointId: string) => {
394 | let fieldPoints = [
395 | node.getEndpoint(pointId),
396 | node.getEndpoint(pointId + '-left'),
397 | node.getEndpoint(pointId + '-right')
398 | ];
399 | fieldPoints.forEach((point) => {
400 | if (!point) {
401 | return;
402 | }
403 | point.updatePos();
404 | });
405 | let updateEdges = this.canvas.edges.filter((item: any) => {
406 | if (nodeId === item.sourceNode.id && (pointId + '-right' === item.sourceEndpoint.id)) {
407 | return true;
408 | }
409 | if (nodeId === item.targetNode.id && (pointId + '-left' === item.targetEndpoint.id)) {
410 | return true;
411 | }
412 | return false;
413 | });
414 | updateEdges.forEach((item: any) => {
415 | item.redraw();
416 | });
417 | this.onChange();
418 | });
419 | });
420 |
421 | // 字段选择状态变更
422 | this.canvas.on('custom.field.checked', (checkData) => {
423 | let result = transformChangeData(this.canvas.getDataMap(), this.props.type || 'single');
424 | let dataSource = result[`${checkData.nodeType}Data`];
425 | let targetNode = dataSource;
426 | if (dataSource.constructor === Array) {
427 | targetNode = _.find(dataSource, (_item) => _item.id === checkData.nodeId);
428 | }
429 |
430 | let fields = targetNode.fields;
431 | let targetField = _.find(fields, (_item) => _item.id === checkData.fieldId);
432 | targetField.checked = checkData.checked;
433 | this.props.onChange && this.props.onChange(result);
434 | });
435 |
436 | this.canvas.on('system.link.click', (data?: any) => {
437 | let _edge = data.edge;
438 | this.props.onEdgeClick && this.props.onEdgeClick({
439 | id: _edge.id,
440 | sourceNodeId: _edge.sourceNode.id,
441 | targetNodeId: _edge.targetNode.id,
442 | sourceEndpointId: _edge.sourceEndpoint.originId,
443 | targetEndpointId: _edge.targetEndpoint.originId
444 | });
445 | })
446 | }
447 | render() {
448 | return (
449 |
452 |
453 |
454 | )
455 | }
456 | }
457 |
--------------------------------------------------------------------------------
/src/canvas/node.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import {Node} from 'butterfly-dag';
4 | import * as ReactDOM from 'react-dom';
5 | import $ from 'jquery';
6 | import * as _ from 'lodash';
7 | import {Tips} from 'butterfly-dag';
8 | import emptyDom from './empty';
9 | import Endpoint from './endpoint';
10 | import './checkbox.less';
11 |
12 | export default class TableNode extends Node {
13 | constructor(opts) {
14 | super(opts);
15 | // 标题高度
16 | this.TITLE_HEIGHT = 34;
17 | // 列标题高度
18 | this.COLUMNS_TITLE_HEIGHT = 28;
19 | // 每列宽度
20 | this.COLUMN_WIDTH = 60;
21 | // 每行高度
22 | this.ROW_HEIGHT = _.get(opts, '_extraPos.rowHeight')|| 26;
23 | // 垂直间距
24 | this.PADDING_VERTICAL = _.get(opts, '_extraPos.nodeVerticalPadding') || 10;
25 | // 水平间距
26 | this.PADDING_HORIZONTAL = _.get(opts, '_extraPos.paddingCenter') || 150;
27 | // 排序宽度
28 | this.SORTABLE_WIDTH = 40;
29 | // checkout宽度
30 | this.CHECKBOX_WIDTH = this.ROW_HEIGHT;
31 |
32 | this.height = 0;
33 | this.width = 0;
34 |
35 | // 选择状态
36 | this.checked = opts.checked;
37 |
38 | // 只读状态
39 | this.readonly = opts.readonly;
40 |
41 | this.fieldsList = [];
42 | }
43 | _addEventListener() {
44 | $(this.dom).on('mouseDown', (e) => {
45 | const LEFT_KEY = 0;
46 | if (e.button !== LEFT_KEY) {
47 | return;
48 | }
49 |
50 | if (this.draggable) {
51 | this._isMoving = true;
52 | this.emit('InnerEvents', {
53 | type: 'node:dragBegin',
54 | data: this
55 | });
56 | } else {
57 | // 单纯为了抛错事件给canvas,为了让canvas的dragtype不为空,不会触发canvas:click事件
58 | this.emit('InnerEvents', {
59 | type: 'node:mouseDown',
60 | data: this
61 | });
62 |
63 | return true;
64 | }
65 | });
66 | }
67 | mounted() {
68 | // 生成endpoint
69 | this._createNodeEndpoint();
70 |
71 | // 保持title宽度
72 | if (!this.fieldsList.length) {
73 | $(this.dom).find('.title').css('width', this.options._emptyWidth || 150);
74 | }
75 |
76 | // 加tips
77 | this._addFieldItemTips();
78 | }
79 | draw(obj) {
80 | let _dom = obj.dom;
81 | if (!_dom) {
82 | _dom = $('')
83 | .attr('class', 'node table-node')
84 | .attr('id', obj.name);
85 | }
86 | if (!_.isEmpty(obj.options._sourceClassName) && _.get(obj, 'options.type') === 'source') {
87 | _dom.addClass(obj.options._sourceClassName)
88 | }
89 | if (!_.isEmpty(obj.options._targetClassName) && _.get(obj, 'options.type') === 'target') {
90 | _dom.addClass(obj.options._targetClassName)
91 | }
92 | const node = $(_dom);
93 | // 计算节点坐标
94 | if (obj.top !== undefined) {
95 | node.css('top', `${obj.top + _.get(obj, 'options._extraPos.paddingTop', 0)}px`);
96 | }
97 | if (obj.left !== undefined) {
98 | node.css('left', `${obj.left + _.get(obj, 'options._extraPos.paddingLeft', 0)}px`);
99 | }
100 |
101 | this._calcSize(node, obj);
102 |
103 | this._createTableName(node); // 表名
104 | this._createFieldTitle(node); // 字段标题
105 | this._createFields(node); // 字段
106 | return node[0];
107 | }
108 | _createTableName(container = this.dom) {
109 | let title = _.get(this, 'options.title');
110 | if (title) {
111 | let titleDom = $(`${title}
`);
112 | titleDom.css({
113 | 'height': this.TITLE_HEIGHT + 'px',
114 | 'line-height': this.TITLE_HEIGHT + 'px'
115 | });
116 | $(container).append(titleDom);
117 | }
118 | }
119 | _addFieldItemTips(fieldItems) {
120 | const _fieldItems = fieldItems || $(this.dom).find('.field-item');
121 | const fieldItemDoms = Array.prototype.slice.apply(_fieldItems);
122 | fieldItemDoms.forEach((_fieldItem, index) => {
123 | if(_fieldItem.scrollWidth > _fieldItem.clientWidth) {
124 | const fieldItem = $(_fieldItems[index])
125 | Tips.createTip({
126 | className: 'field-item-tooltip',
127 | targetDom: fieldItem[0],
128 | genTipDom: () => fieldItem.text(),
129 | });
130 | }
131 | })
132 | }
133 | _createFieldTitle(container = this.dom) {
134 | let type = _.get(this, 'options.type', '');
135 | let columns = _.get(this, ['options', type === 'source' ? '_sourceColumns' : '_targetColumns'], []);
136 | let checkable = _.get(this, 'options.checkable');
137 | let hasFieldTitle = _.some(columns, (item) => {
138 | return item.title;
139 | });
140 | let isObject = (object) => Object.prototype.toString.call(object) === "[object Object]";
141 | if (hasFieldTitle) {
142 | const columnsTitleDom = $('');
143 | if (checkable) {
144 | let hasCheckBox = false;
145 | if (type === 'source') {
146 | if (isObject(checkable) && checkable.source) {
147 | hasCheckBox = true;
148 | }
149 | } else if (type === 'target') {
150 | if (isObject(checkable) && checkable.target) {
151 | hasCheckBox = true;
152 | }
153 | }
154 | if (typeof(checkable) === 'boolean') {
155 | hasCheckBox = true;
156 | }
157 | if (hasCheckBox) {
158 | let emptyDom = $(``);
159 | emptyDom.css('width', this.CHECKBOX_WIDTH + 'px');
160 | columnsTitleDom.append(emptyDom);
161 | }
162 | }
163 | columns.forEach(_col => {
164 | const columnsTitleItem = $(`${_col.title}`);
165 | columnsTitleItem.css('width', (_col.width || this.COLUMN_WIDTH) + 'px');
166 | columnsTitleDom.append(columnsTitleItem);
167 | });
168 | columnsTitleDom.css('height', this.COLUMNS_TITLE_HEIGHT + 'px')
169 | .css('line-height', this.COLUMNS_TITLE_HEIGHT + 'px')
170 | container.append(columnsTitleDom);
171 | }
172 | }
173 | _createSortableBtn(field) {
174 | let sortFieldDom = $(`
175 |
176 |
177 |
178 |
179 | `);
180 | sortFieldDom.css({
181 | width: this.SORTABLE_WIDTH + 'px',
182 | });
183 | sortFieldDom.find('.move-up').click(this._moveUp.bind(this, field));
184 | sortFieldDom.find('.move-down').click(this._moveDown.bind(this, field));
185 | return sortFieldDom;
186 | }
187 | _createCheckBox(field) {
188 | let checkboxDom = $(`
189 |
190 |
191 |
192 |
193 |
194 | `);
195 | checkboxDom.css('height', this.CHECKBOX_WIDTH + 'px');
196 | if (field.checked) {
197 | checkboxDom.find('.dm-checkbox').addClass('dm-checkbox-checked');
198 | }
199 | if (this.readonly) {
200 | checkboxDom.addClass('field-checkbox-disable');
201 | }
202 | checkboxDom.click((e) => {
203 | e.preventDefault();
204 | e.stopPropagation();
205 | if (this.readonly) {
206 | return;
207 | }
208 | // 发送事件,更新选择状态
209 | this.emit('custom.field.checked', {
210 | nodeId: this.id,
211 | nodeType: this.options.type,
212 | fieldId: field.id,
213 | checked: !field.checked
214 | });
215 | });
216 | return checkboxDom;
217 | }
218 | _createFields(container = $(this.dom), addFields = []) {
219 | let fields = addFields.length === 0 ? _.get(this, 'options.fields') : addFields;
220 | let type = _.get(this, 'options.type', '');
221 | let columns = _.get(this, ['options', type === 'source' ? '_sourceColumns' : '_targetColumns'], []);
222 | let sortable = _.get(this, 'options.sortable');
223 | let checkable = _.get(this, 'options.checkable');
224 | let isObject = (object) => Object.prototype.toString.call(object) === "[object Object]";
225 | let result = [];
226 |
227 | if (fields && fields.length) {
228 | fields.forEach((_field, index) => {
229 | let fieldDom = $('');
230 | let _primaryKey = columns[0].key;
231 | let sortFieldDom = undefined;
232 | let checkFieldDom = undefined;
233 |
234 | if (sortable) {
235 | sortFieldDom = this._createSortableBtn(_field);
236 | }
237 | if (checkable) {
238 | if (type === 'source') {
239 | if (isObject(checkable) && checkable.source) {
240 | checkFieldDom = this._createCheckBox(_field);
241 | fieldDom.append(checkFieldDom);
242 | }
243 | } else if (type === 'target') {
244 | if (isObject(checkable) && checkable.target) {
245 | checkFieldDom = this._createCheckBox(_field);
246 | fieldDom.append(checkFieldDom);
247 | }
248 | }
249 | if (typeof(checkable) === 'boolean') {
250 | checkFieldDom = this._createCheckBox(_field);
251 | fieldDom.append(checkFieldDom);
252 | }
253 | }
254 | fieldDom.css({
255 | height: this.ROW_HEIGHT + 'px',
256 | 'line-height': this.ROW_HEIGHT + 'px'
257 | });
258 | columns.forEach((_col) => {
259 | if (_col.render) {
260 | let fieldItemDom = $(``);
261 | fieldItemDom.css('width', (_col.width || this.COLUMN_WIDTH) + 'px');
262 | ReactDOM.render(_col.render(_field[_col.key], _field, index), fieldItemDom[0]);
263 | fieldDom.append(fieldItemDom);
264 | } else {
265 | let fieldItemDom = $(`${_field[_col.key]}`);
266 | fieldItemDom.css('width', (_col.width || this.COLUMN_WIDTH) + 'px');
267 | fieldDom.append(fieldItemDom);
268 | }
269 | if (_col.primaryKey) {
270 | _primaryKey = _col.key;
271 | }
272 |
273 | });
274 | if (sortFieldDom) {
275 | fieldDom.append(sortFieldDom);
276 | }
277 | if (type === 'source') {
278 | let rightPoint = $('');
279 | fieldDom.append(rightPoint);
280 | if (isObject(sortable) && sortable.source) {
281 | sortFieldDom = this._createSortableBtn(_field);
282 | fieldDom.append(sortFieldDom);
283 | }
284 | }
285 | if (type === 'target') {
286 | let leftPoint = $('');
287 | fieldDom.append(leftPoint);
288 | if (isObject(sortable) && sortable.target) {
289 | sortFieldDom = this._createSortableBtn(_field);
290 | fieldDom.append(sortFieldDom);
291 | }
292 | }
293 | container.append(fieldDom);
294 | result.push({
295 | id: _field[_primaryKey],
296 | dom: fieldDom
297 | });
298 | });
299 | this.fieldsList = this.fieldsList.concat(result);
300 | } else {
301 | const _emptyContent = _.get(this.options, '_emptyContent');
302 | const noDataTree = emptyDom({
303 | content: _emptyContent,
304 | width: this.options._emptyWidth
305 | });
306 | container.append(noDataTree);
307 | this.height = $(container).outerHeight();
308 | }
309 |
310 | return result;
311 | }
312 | _createNodeEndpoint(fieldList) {
313 | let type = this.options.type;
314 | let _fieldList = fieldList || this.fieldsList || [];
315 |
316 | _fieldList.forEach((item) => {
317 | this.addEndpoint({
318 | id: item.id,
319 | orientation: type === 'source' ? [1,0] : [-1,0],
320 | type: type,
321 | _isNodeSelf: true,
322 | dom: item.dom[0],
323 | Class: Endpoint
324 | });
325 | if (type === 'source') {
326 | this.addEndpoint({
327 | id: item.id + '-right',
328 | orientation: [1,0],
329 | type: type,
330 | _isNodeSelf: false,
331 | dom: $(item.dom).find('.right-point')[0],
332 | Class: Endpoint,
333 | linkable: false
334 | });
335 | } else if (type === 'target') {
336 | this.addEndpoint({
337 | id: item.id + '-left',
338 | orientation: [-1,0],
339 | type: type,
340 | _isNodeSelf: false,
341 | dom: $(item.dom).find('.left-point')[0],
342 | Class: Endpoint,
343 | disLinkable: false
344 | });
345 | }
346 | });
347 | }
348 | _calcSize(node, obj) {
349 | let hasTitle = _.get(obj, 'options.title');
350 | let fields = _.get(obj, 'options.fields', []);
351 | let sortable = _.get(obj, 'options.sortable');
352 | let type = _.get(obj, 'options.type');
353 |
354 | if (hasTitle) {
355 | this.height += this.TITLE_HEIGHT;
356 | }
357 | this.height += fields.length * this.ROW_HEIGHT;
358 |
359 | let columns = _.get(this, ['options', type === 'source' ? '_sourceColumns' : '_targetColumns'], []);
360 | columns.forEach((item) => {
361 | this.width += item.width || this.COLUMN_WIDTH;
362 | });
363 |
364 | if (typeof(sortable) === 'boolean') this.width += this.SORTABLE_WIDTH;
365 |
366 | if (Object.prototype.toString.call(sortable) === '[object Object]') {
367 | if (type === 'source') this.width += this.SORTABLE_WIDTH;
368 | if (type === 'target') this.width += this.SORTABLE_WIDTH;
369 | }
370 | // todo: 记得算上SORTABLE_WIDTH
371 | }
372 |
373 | _moveUp(curField, event) {
374 | event.preventDefault();
375 | event.stopPropagation();
376 | let curIndex = this.fieldsList.findIndex(i => i.id === curField.id);
377 | let oldFields = _.get(this, 'options.fields', []);
378 | let oldFieldsItem = oldFields.splice(curIndex, 1);
379 | let point = this.getEndpoint(curField.id);
380 | let curFieldDom = point.dom;
381 | let curFieldData = this.fieldsList[curIndex];
382 | // 处理边界
383 | if (curIndex === 0) {
384 | console.warn('this field has reach the top!');
385 | return;
386 | }
387 | let preFieldData = this.fieldsList[curIndex - 1];
388 | let preFieldDom = preFieldData.dom;
389 |
390 | // 交换dom
391 | $(preFieldDom).before(curFieldDom);
392 |
393 | // 交换数据
394 | this.fieldsList[curIndex] = preFieldData;
395 | this.fieldsList[curIndex - 1] = curFieldData;
396 | oldFields.splice(curIndex - 1, 0, oldFieldsItem[0]);
397 |
398 | // 发送事件,更新线段和锚点坐标
399 | this.emit('custom.field.sort', {
400 | nodeId: this.id,
401 | pointIds: [curFieldData.id, preFieldData.id]
402 | });
403 | }
404 |
405 | _moveDown(curField, event) {
406 | event.preventDefault();
407 | event.stopPropagation();
408 | let curIndex = this.fieldsList.findIndex(i => i.id === curField.id);
409 | let oldFields = _.get(this, 'options.fields', []);
410 | let oldFieldsItem = oldFields.splice(curIndex, 1);
411 | let point = this.getEndpoint(curField.id);
412 | let curFieldDom = point.dom;
413 | let curFieldData = this.fieldsList[curIndex];
414 | // 处理边界
415 | if (curIndex === this.fieldsList.length - 1) {
416 | console.warn('this field has reach the bottom!');
417 | return;
418 | }
419 | let nextFieldData = this.fieldsList[curIndex + 1];
420 | let nextFieldDom = nextFieldData.dom;
421 |
422 | // 交换dom
423 | $(nextFieldDom).after(curFieldDom);
424 |
425 | // 交换数据
426 | this.fieldsList[curIndex] = nextFieldData;
427 | this.fieldsList[curIndex + 1] = curFieldData;
428 | oldFields.splice(curIndex + 1, 0, oldFieldsItem[0]);
429 |
430 | // 发送事件,更新线段和锚点坐标
431 | this.emit('custom.field.sort', {
432 | nodeId: this.id,
433 | pointIds: [curFieldData.id, nextFieldData.id]
434 | });
435 |
436 | }
437 | setReadOnly (newStatus) {
438 | if (this.readonly != newStatus) {
439 | this.readonly = newStatus;
440 | if (newStatus) {
441 | $(this.dom).find('.field-checkbox').addClass('field-checkbox-disable');
442 | } else {
443 | $(this.dom).find('.field-checkbox').removeClass('field-checkbox-disable');
444 | }
445 | }
446 | }
447 | addFields(fields) {
448 | let _addFieldsList = this._createFields(undefined, fields);
449 | this._createNodeEndpoint(_addFieldsList);
450 | let _addFieldsDomList = _addFieldsList.map((item) => {
451 | return $(item.dom).find('.field-item');
452 | });
453 | this._addFieldItemTips(_addFieldsDomList);
454 | }
455 | removeFields(fields) {
456 | fields.forEach((item) => {
457 | let index = _.findIndex(this.fieldsList, _field => _field.id === item.id);
458 | if (index === -1) {
459 | return;
460 | }
461 | let field = this.fieldsList.splice(index, 1)[0];
462 | if (field) {
463 | $(field.dom).find('.field-item').off();
464 | $(field.dom).off();
465 | $(field.dom).remove();
466 | }
467 | });
468 | }
469 | updateCheckedStatus(fields) {
470 | fields.forEach((field) => {
471 | let realField = _.find(this.fieldsList, (item) => {
472 | return item.id === field.id;
473 | });
474 | let realFieldData = _.find(this.options.fields || [], (item) => {
475 | return item.id === field.id;
476 | });
477 | if (!realFieldData) {
478 | return;
479 | }
480 | if (field.checked) {
481 | realFieldData.checked = field.checked;
482 | realField.dom.find('.dm-checkbox').addClass('dm-checkbox-checked');
483 | } else {
484 | realFieldData.checked = field.checked;
485 | realField.dom.find('.dm-checkbox').removeClass('dm-checkbox-checked');
486 | }
487 | });
488 | }
489 | };
490 |
--------------------------------------------------------------------------------