├── 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 |
DTDesign-React数据映射组件
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 | [![npm version](https://img.shields.io/npm/v/react-data-mapping.svg?style=flat)](https://www.npmjs.com/package/react-data-mapping) 6 | [![download](https://img.shields.io/npm/dm/react-data-mapping.svg?style=flat)](https://www.npmjs.com/package/react-data-mapping) 7 | [![gzip size](https://img.shields.io/bundlephobia/minzip/react-data-mapping)](https://www.npmjs.com/package/react-data-mapping) 8 | [![license](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](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 | --------------------------------------------------------------------------------