├── test ├── styleMock.js ├── setup.js ├── index.test.js └── __snapshots__ │ └── index.test.js.snap ├── .DS_Store ├── .gitignore ├── .npmignore ├── .luciorc.lib.js ├── .luciorc.js ├── src ├── index.js ├── utils.js ├── style.less └── treeTransfer.js ├── example ├── index.ejs ├── async.json ├── style.less ├── data.json └── index.js ├── .eslintrc ├── package.json └── README.md /test/styleMock.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jindada/tree-transfer/HEAD/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | node_modules 3 | gh-pages 4 | dist 5 | coverage 6 | npm-debug.log -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | example 2 | src 3 | node_modules 4 | test 5 | gh-pages 6 | coverage 7 | npm-debug.log 8 | .luciorc.js 9 | .luciorc.lib.js -------------------------------------------------------------------------------- /.luciorc.lib.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = { 4 | entry: './src/index.js', 5 | library: 'lucio-tree-transfer', 6 | libraryTarget: 'umd' 7 | }; -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | var jsdom = require('jsdom'); 2 | var Enzyme = require('enzyme'); 3 | var Adapter = require('enzyme-adapter-react-16'); 4 | 5 | Enzyme.configure({ adapter: new Adapter() }); -------------------------------------------------------------------------------- /.luciorc.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = { 4 | entry: './example/index.js', 5 | output: './gh-pages', 6 | babelLoaderDir: [path.join(__dirname, './src')] 7 | }; -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import 'antd/lib/button/style'; 2 | import 'antd/lib/checkbox/style'; 3 | import 'antd/lib/tree/style'; 4 | import 'antd/lib/input/style'; 5 | import 'antd/lib/alert/style'; 6 | import 'antd/lib/spin/style'; 7 | 8 | import TreeTransfer from './treeTransfer'; 9 | 10 | export default TreeTransfer; -------------------------------------------------------------------------------- /example/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | lucio-tree-transfer 8 | 9 | 10 |
11 | 12 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | export const hasUnLoadNode = (node) => { 2 | let status = false; 3 | const loop = data => data.forEach(item => { 4 | if (item.props.children && !status) { 5 | if (item.props.children.length === 0) { 6 | status = true; 7 | return; 8 | } else { 9 | loop(item.props.children); 10 | } 11 | } 12 | }); 13 | loop(node); 14 | return status; 15 | }; 16 | 17 | export const unique = (array, key) => { 18 | const res = new Map(); 19 | return array.filter(item => !res.has(item[key]) && res.set(item[key], 1)); 20 | }; -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser" : "babel-eslint", 3 | "extends" : [ 4 | "standard", 5 | "standard-react" 6 | ], 7 | "env": { 8 | "browser": true, 9 | "node": true, 10 | "jasmine": true, 11 | "jest": true, 12 | "es6": true 13 | }, 14 | "plugins": [ 15 | "babel" 16 | ], 17 | "rules": { 18 | "camelcase": 1, 19 | "react/jsx-no-bind": 0, 20 | "handle-callback-err":1, 21 | "space-before-function-paren":0, 22 | "comma-dangle": 0, 23 | "quotes": 0, 24 | "no-console": 0, 25 | "no-debugger": 1, 26 | "no-var": 1, 27 | "semi": [1, "always"], 28 | "no-trailing-spaces": 0, 29 | "eol-last": 0, 30 | "no-underscore-dangle": 0, 31 | "no-alert": 0, 32 | "no-lone-blocks": 0, 33 | "jsx-quotes": 1 34 | } 35 | } -------------------------------------------------------------------------------- /example/async.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "value": "0", 4 | "label": "哈尔滨", 5 | "children": [ 6 | { 7 | "value": "0-1", 8 | "label": "南岗区", 9 | "children": [] 10 | }, 11 | { 12 | "value": "0-2", 13 | "label": "香坊区", 14 | "children": [] 15 | }, 16 | { 17 | "value": "0-3", 18 | "label": "松北区", 19 | "children": [] 20 | } 21 | ] 22 | }, 23 | { 24 | "value": "1", 25 | "label": "齐齐哈尔", 26 | "children": [ 27 | { 28 | "value": "1-1", 29 | "label": "A区", 30 | "children": [] 31 | } 32 | ] 33 | }, 34 | { 35 | "value": "2", 36 | "label": "佳木斯", 37 | "children": [ 38 | { 39 | "value": "2-1", 40 | "label": "B区", 41 | "children": [] 42 | } 43 | ] 44 | } 45 | ] -------------------------------------------------------------------------------- /example/style.less: -------------------------------------------------------------------------------- 1 | .lucio-tree-transfer-example { 2 | position: relative; 3 | text-align: center; 4 | overflow-x: hidden; 5 | padding-bottom: 32px; 6 | p.pkname { 7 | font-family: monospace; 8 | font-size: 36px; 9 | font-weight: 800; 10 | margin: 32px 0 16px 0; 11 | } 12 | h4 { 13 | font-family: monospace; 14 | width: 600px; 15 | text-align: left; 16 | margin: 0 auto; 17 | padding: 32px 0 8px 0; 18 | } 19 | .lucio-tree-transfer { 20 | width: 600px; 21 | margin: 0 auto; 22 | } 23 | .gh-ribbon { 24 | display: block; 25 | position: absolute; 26 | right: -60px; 27 | top: 44px; 28 | -webkit-transform: rotate(45deg); 29 | transform: rotate(45deg); 30 | width: 230px; 31 | z-index: 10000; 32 | white-space: nowrap; 33 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 34 | background-color: #686868; 35 | box-shadow: 0 0 2px rgba(102,102,102,0.4); 36 | padding: 1px 0; 37 | a { 38 | text-decoration: none !important; 39 | border: 1px solid #ccc; 40 | color: #fff; 41 | display: block; 42 | font-size: 13px; 43 | font-weight: 700; 44 | outline: medium none; 45 | padding: 4px 50px 2px; 46 | text-align: center; 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /example/data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "value": "0", 4 | "label": "哈尔滨", 5 | "children": [ 6 | { 7 | "value": "0-1", 8 | "label": "南岗区", 9 | "children": [ 10 | { 11 | "value": "0-1-1", 12 | "label": "黑龙江大学" 13 | }, 14 | { 15 | "value": "0-1-2", 16 | "label": "哈尔滨理工大学" 17 | }, 18 | { 19 | "value": "0-1-3", 20 | "label": "哈尔滨工业大学" 21 | } 22 | ] 23 | }, 24 | { 25 | "value": "0-2", 26 | "label": "香坊区", 27 | "children": [ 28 | { 29 | "value": "0-2-1", 30 | "label": "东北农业大学" 31 | }, 32 | { 33 | "value": "0-2-2", 34 | "label": "东北林业大学" 35 | } 36 | ] 37 | }, 38 | { 39 | "value": "0-3", 40 | "label": "松北区", 41 | "children": [ 42 | { 43 | "value": "0-3-1", 44 | "label": "哈尔滨师范大学" 45 | }, 46 | { 47 | "value": "0-3-2", 48 | "label": "黑龙江科技大学" 49 | } 50 | ] 51 | } 52 | ] 53 | }, 54 | { 55 | "value": "1", 56 | "label": "齐齐哈尔", 57 | "children": [ 58 | { 59 | "value": "1-1", 60 | "label": "A区", 61 | "children": [ 62 | { 63 | "value": "1-1-1", 64 | "label": "齐齐哈尔大学" 65 | } 66 | ] 67 | } 68 | ] 69 | }, 70 | { 71 | "value": "2", 72 | "label": "佳木斯", 73 | "children": [ 74 | { 75 | "value": "2-1", 76 | "label": "B区", 77 | "children": [ 78 | { 79 | "value": "2-1-1", 80 | "label": "佳木斯大学" 81 | } 82 | ] 83 | } 84 | ] 85 | } 86 | ] -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "antd-tree-transfer", 3 | "version": "0.0.3", 4 | "description": "react tree transfer components by antd", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "start": "lucio start", 8 | "eslint": "lucio eslint", 9 | "prebuild:gh-pages": "npm run eslint && npm run test", 10 | "build:gh-pages": "lucio build", 11 | "prebuild": "npm run eslint && npm run test", 12 | "build": "lucio library -c .luciorc.lib.js", 13 | "test": "jest", 14 | "coverage": "jest --coverage", 15 | "codecov": "codecov --token=28bebdf1-7240-4033-87bb-1c7143cf660e" 16 | }, 17 | "jest": { 18 | "setupFiles": [ 19 | "./test/setup.js" 20 | ], 21 | "testMatch": [ 22 | "**/?(*.)(spec|test|e2e).js?(x)" 23 | ], 24 | "moduleFileExtensions": [ 25 | "js", 26 | "jsx" 27 | ], 28 | "transform": { 29 | "^.+\\.js$": "babel-jest" 30 | }, 31 | "snapshotSerializers": [ 32 | "enzyme-to-json/serializer" 33 | ], 34 | "moduleNameMapper": { 35 | "\\.(css|less)$": "/test/styleMock.js" 36 | }, 37 | "testURL": "http://localhost/" 38 | }, 39 | "babel": { 40 | "presets": [ 41 | "es2015", 42 | "stage-0", 43 | "react" 44 | ], 45 | "plugins": [ 46 | "transform-decorators-legacy", 47 | "transform-class-properties" 48 | ] 49 | }, 50 | "repository": { 51 | "type": "git", 52 | "url": "git+https://github.com/jindada/tree-transfer.git" 53 | }, 54 | "author": "", 55 | "license": "MIT", 56 | "bugs": { 57 | "url": "https://github.com/jindada/tree-transfer/issues" 58 | }, 59 | "homepage": "https://github.com/jindada/tree-transfer#readme", 60 | "dependencies": { 61 | "classnames": "^2.2.5", 62 | "lodash.difference": "^4.5.0", 63 | "lodash.uniq": "^4.5.0" 64 | }, 65 | "devDependencies": { 66 | "babel-jest": "^22.0.3", 67 | "babel-plugin-transform-class-properties": "^6.24.1", 68 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 69 | "babel-preset-stage-0": "^6.24.1", 70 | "enzyme": "^3.2.0", 71 | "enzyme-adapter-react-16": "^1.1.1", 72 | "enzyme-to-json": "^3.3.0", 73 | "jest": "^22.0.3", 74 | "jsdom": "^11.5.1", 75 | "lucio-cli": "^1.1.0-beta.4", 76 | "rimraf": "^2.6.2" 77 | }, 78 | "peerDependencies": { 79 | "react": ">=16.0.0", 80 | "react-dom": ">=16.0.0", 81 | "prop-types": ">=15.6.0", 82 | "antd": ">=3.0.0" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## antd-tree-transfer (old: lucio-tree-transfer) 2 | --- 3 | 4 | React tree transfer Component with antd 5 | 6 |

7 | 8 |

9 | 10 | [![NPM version](https://img.shields.io/npm/v/antd-tree-transfer.svg?style=flat)](https://npmjs.org/package/antd-tree-transfer) 11 | [![NPM downloads](http://img.shields.io/npm/dm/antd-tree-transfer.svg?style=flat)](https://npmjs.org/package/antd-tree-transfer) 12 | [![Test coverage](https://img.shields.io/codecov/c/github/jindada/tree-transfer/master.svg?style=flat-square)](https://codecov.io/gh/luciojs/tree-transfer/branch/master) 13 | 14 | ## Install 15 | 16 | [![rc-rate](https://nodei.co/npm/antd-tree-transfer.png)](https://npmjs.org/package/antd-tree-transfer) 17 | 18 | 19 | ## Development 20 | 21 | ``` 22 | npm install 23 | npm start 24 | ``` 25 | 26 | ## Example 27 | 28 | http://localhost:9000/ 29 | 30 | online example: https://jindada.github.io/tree-transfer/ 31 | 32 | 33 | ## Usage 34 | 35 | ```js 36 | import React, { Component } from 'react'; 37 | import TreeTransfer from 'antd-tree-transfer'; 38 | 39 | const source = [ 40 | { 41 | key: '0', 42 | title: '0', 43 | children: [ 44 | { 45 | key: '0-0', 46 | title: '0-0', 47 | }, 48 | { 49 | key: '0-1', 50 | title: '0-1', 51 | } 52 | ] 53 | } 54 | ], 55 | 56 | class App extends Component { 57 | state = { 58 | target: ['0-1'] 59 | } 60 | 61 | handleChange = (target) => { 62 | this.setState({ 63 | target 64 | }); 65 | } 66 | 67 | render() { 68 | return 69 | } 70 | } 71 | 72 | render(, document.querySelector('#app')); 73 | ``` 74 | 75 | 76 | ## API 77 | 78 | | 参数 | 说明 | 类型 | 默认值 | 79 | | --- | --- | --- | --- | 80 | | className | 选择器 className | String | - | 81 | | rowKey | 指定数据列的key | String | 'key' | 82 | | rowTitle | 指定数据列的title | String | 'title' | 83 | | rowChildren | 指定数据列的children | String | 'children' | 84 | | source | 数据源,其中的数据将会被渲染到左侧框(Tree)中 | Array | [] | 85 | | target | 显示在右侧框数据的key集合 | Array | [] | 86 | | sourceTitle | 左侧框标题 | String | '源数据' | 87 | | targetTitle | 右侧框标题 | String | '目的数据' | 88 | | treeLoading | 加载状态 | Boolean | false | 89 | | showSearch | 是否显示搜索框 | Boolean | false | 90 | | onLoadData | 异步加载数据 | function(node) | - | 91 | | onTreeSearch | 异步搜索数据 | function(value) | - | 92 | 93 | ## License 94 | 95 | antd-tree-transfer is released under the MIT license. 96 | -------------------------------------------------------------------------------- /src/style.less: -------------------------------------------------------------------------------- 1 | @prefix: ~"tree-transfer"; 2 | @size: 12px; 3 | @color: rgba(0,0,0,.65); 4 | @borderColor: #d9d9d9; 5 | 6 | .lucio-@{prefix} { 7 | position: relative; 8 | line-height: 1.5; 9 | font-family: Consolas,Menlo,Courier,monospace; 10 | text-align: left; 11 | color: @color; 12 | .@{prefix}-panel { 13 | width: 250px; 14 | height: 300px; 15 | font-size: @size; 16 | border: 1px solid @borderColor; 17 | display: inline-block; 18 | border-radius: 4px; 19 | vertical-align: middle; 20 | position: relative; 21 | padding-top: 34px; 22 | &-header { 23 | padding: 8px 12px; 24 | height: 34px; 25 | border-radius: 4px 4px 0 0; 26 | border-bottom: 1px solid @borderColor; 27 | overflow: hidden; 28 | position: absolute; 29 | top: 0; 30 | left: 0; 31 | width: 100%; 32 | .ant-checkbox-wrapper { 33 | font-size: @size; 34 | } 35 | &-title { 36 | position: absolute; 37 | right: 12px; 38 | } 39 | } 40 | &-body { 41 | height: 100%; 42 | font-size: @size; 43 | position: relative; 44 | overflow: auto; 45 | .ant-alert { 46 | font-size: @size; 47 | .ant-alert-icon { 48 | top: @size - 1.5px 49 | } 50 | } 51 | &-search { 52 | position: absolute; 53 | top: 0; 54 | width: 100%; 55 | padding: 8px; 56 | .ant-input { 57 | font-size: @size; 58 | } 59 | } 60 | .ant-spin-nested-loading { 61 | height: 100%; 62 | .ant-spin-container { 63 | height: 100%; 64 | } 65 | } 66 | &-content { 67 | padding: 0px; 68 | overflow: auto; 69 | height: 100%; 70 | .ant-tree { 71 | font-size: @size; 72 | } 73 | li { 74 | padding: 8px 8px 8px 12px; 75 | min-height: 32px; 76 | transition: all .3s; 77 | overflow: hidden; 78 | white-space: nowrap; 79 | text-overflow: ellipsis; 80 | &:hover { 81 | background-color: #d2eafb; 82 | } 83 | } 84 | } 85 | &-has-search { 86 | padding-top: 46px; 87 | } 88 | } 89 | } 90 | .@{prefix}-operation { 91 | display: inline-block; 92 | overflow: hidden; 93 | margin: 0 8px; 94 | vertical-align: middle; 95 | button.ant-btn { 96 | display: block; 97 | margin: 4px 0; 98 | } 99 | } 100 | } -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { render } from 'react-dom'; 3 | import TreeTransfer from '../src'; 4 | import './style.less'; 5 | import async from './async.json'; 6 | import data from './data.json'; 7 | 8 | class App extends Component { 9 | constructor(props) { 10 | super(props); 11 | this.state = { 12 | source: data, 13 | target: ['1-1-1'], 14 | asyncSource: async, 15 | asyncTarget: [], 16 | asyncLoading: false 17 | }; 18 | } 19 | 20 | onChange = (target) => { 21 | this.setState({ 22 | target 23 | }); 24 | } 25 | 26 | onAsyncChange = (target) => { 27 | this.setState({ 28 | asyncTarget: target 29 | }); 30 | } 31 | 32 | onLoadData = (node) => new Promise(resolve => { 33 | if (node.props.children.length > 0) { 34 | resolve(); 35 | return; 36 | } else { 37 | setTimeout(() => { 38 | this.setState({ 39 | asyncSource: mergeTree(this.state.asyncSource, node.props.eventKey, makeChildren(node.props.eventKey)) 40 | }, () => { 41 | resolve(); 42 | return; 43 | }); 44 | }, 2000); 45 | } 46 | }) 47 | 48 | onTreeSearch = (value) => { 49 | this.setState({ 50 | asyncLoading: true 51 | }, () => { 52 | setTimeout(() => { 53 | this.setState({ 54 | asyncSource: data, 55 | asyncLoading: false 56 | }); 57 | }, 2000); 58 | }); 59 | } 60 | 61 | render() { 62 | const { source, target, asyncSource, asyncTarget, asyncLoading } = this.state; 63 | 64 | const treeTransferProps = { 65 | source, 66 | target, 67 | rowKey: "value", 68 | rowTitle: "label", 69 | onChange: this.onChange 70 | }; 71 | 72 | return ( 73 |
74 |

lucio-tree-transfer

75 |

1.基本用法

76 | 77 |

2.显示搜索框

78 | 79 |

3.异步用法

80 | 81 |

3.异步用法,显示搜索框

82 | 83 | 84 |
85 | ); 86 | } 87 | } 88 | 89 | export const mergeTree = (treeData, key, children) => { 90 | const loop = data => data.forEach((item) => { 91 | if (item.children) { 92 | if (item.value === key) { 93 | item.children = children; 94 | } else { 95 | loop(item.children); 96 | } 97 | } 98 | }); 99 | loop(treeData); 100 | return treeData; 101 | }; 102 | 103 | const makeChildren = (key) => [ 104 | { 105 | "value": `${key}-0`, 106 | "label": "异步叶子" 107 | }, 108 | { 109 | "value": `${key}-1`, 110 | "label": "异步叶子" 111 | }, 112 | ]; 113 | 114 | render(, document.querySelector('#app')); -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, mount } from 'enzyme'; 3 | import TreeTransfer from '../src'; 4 | 5 | const treeTransferProps = { 6 | source: [ 7 | { 8 | key: '0', 9 | title: '0', 10 | children: [ 11 | { 12 | key: '0-0', 13 | title: '0-0', 14 | }, 15 | { 16 | key: '0-1', 17 | title: '0-1', 18 | } 19 | ] 20 | } 21 | ], 22 | target: ['0-1'] 23 | }; 24 | 25 | describe('TreeTransfer', () => { 26 | it('renders correctly', () => { 27 | const wrapper = render(); 28 | expect(wrapper).toMatchSnapshot(); 29 | }); 30 | 31 | it('should support loading', () => { 32 | const wrapper = render(); 33 | expect(wrapper).toMatchSnapshot(); 34 | }); 35 | 36 | it('should support sourceTitle and targetTitle', () => { 37 | const wrapper = render(); 38 | expect(wrapper).toMatchSnapshot(); 39 | }); 40 | 41 | it('should move all selected tree leafs to right list', () => { 42 | const handleChange = jest.fn(); 43 | const wrapper = mount(); 44 | wrapper.find('.ant-tree').find('.ant-tree-checkbox').at(0).simulate('click'); 45 | wrapper.find('.tree-transfer-operation').find('button').at(0).simulate('click'); 46 | expect(handleChange).toHaveBeenLastCalledWith(['0-1', '0-0']); 47 | }); 48 | 49 | it('should move unselected tree leafs to right list', () => { 50 | const handleChange = jest.fn(); 51 | const wrapper = mount(); 52 | wrapper.find('.ant-tree').find('.ant-tree-checkbox').at(2).simulate('click'); 53 | wrapper.find('.tree-transfer-operation').find('button').at(0).simulate('click'); 54 | expect(handleChange).toHaveBeenLastCalledWith([]); 55 | }); 56 | 57 | it('should check all item when click on check all', () => { 58 | const handleChange = jest.fn(); 59 | const wrapper = mount(); 60 | wrapper.find('.tree-transfer-panel-header').find('.ant-checkbox-input').at(0).simulate('change', { target: { checked: true } }); 61 | wrapper.find('.tree-transfer-operation').find('button').at(1).simulate('click'); 62 | expect(handleChange).toHaveBeenLastCalledWith([]); 63 | }); 64 | 65 | it('should move selected list item to left tree', () => { 66 | const handleChange = jest.fn(); 67 | const wrapper = mount(); 68 | wrapper.find('.tree-transfer-panel-right').find('li').find('.ant-checkbox-input').at(0).simulate('change', { target: { checked: true } }); 69 | wrapper.find('.tree-transfer-operation').find('button').at(1).simulate('click'); 70 | expect(handleChange).toHaveBeenLastCalledWith([]); 71 | }); 72 | 73 | it('should move selected list item to left tree', () => { 74 | const mockFunction = jest.fn(); 75 | const onLoadData = node => new Promise(resolve => { 76 | mockFunction(node); 77 | resolve(); 78 | }); 79 | 80 | const props = { 81 | source: [ 82 | { 83 | key: '0', 84 | title: '0', 85 | children: [] 86 | } 87 | ], 88 | target: [] 89 | }; 90 | const wrapper = mount(); 91 | wrapper.find('.ant-tree').find('.ant-tree-switcher').at(0).simulate('click'); 92 | expect(mockFunction).toHaveBeenCalled(); 93 | }); 94 | 95 | // it('should search result when use Search in tree panel', () => { 96 | // const wrapper = mount(); 97 | // wrapper.find('.tree-transfer-panel-left').find('.ant-input-search').find('input').at(0).simulate('change', { target: { value: '0-1' } }); 98 | // wrapper.find('.tree-transfer-panel-left').find('.ant-input-search').find('input').at(0).simulate('keyDown', { keyCode: 13 }); 99 | // expect(wrapper.find('.tree-transfer-panel-left').find('.ant-tree-title').at(2).html()).toBe(''); 100 | // }); 101 | }); 102 | -------------------------------------------------------------------------------- /src/treeTransfer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import classNames from 'classnames'; 4 | import Button from 'antd/lib/button'; 5 | import Checkbox from 'antd/lib/checkbox'; 6 | import Input from 'antd/lib/input'; 7 | import Tree from 'antd/lib/tree'; 8 | import Alert from 'antd/lib/alert'; 9 | import Spin from 'antd/lib/spin'; 10 | import uniq from 'lodash.uniq'; 11 | import difference from 'lodash.difference'; 12 | import { hasUnLoadNode, unique } from './utils'; 13 | import './style.less'; 14 | const TreeNode = Tree.TreeNode; 15 | const Search = Input.Search; 16 | 17 | class TreeTransfer extends Component { 18 | constructor(props) { 19 | super(props); 20 | const { treeNode, listData, leafKeys } = this.generate(props); 21 | const treeCheckedKeys = listData.map(({key}) => key); 22 | this.state = { 23 | treeNode, 24 | listData, 25 | leafKeys, 26 | treeCheckedKeys, 27 | treeExpandedKeys: treeCheckedKeys, 28 | treeAutoExpandParent: true, // 自动展开父节点 初始为true 有展开操作的时候为false 29 | listCheckedKeys: [], 30 | treeSearchKey: '', 31 | listSearchKey: '', 32 | unLoadAlert: false 33 | }; 34 | } 35 | 36 | componentWillReceiveProps(nextProps) { 37 | const { treeNode, listData, leafKeys, expandedKeys } = this.generate(nextProps, this.state); 38 | const treeCheckedKeys = listData.map(({key}) => key); 39 | const { treeSearchKey, treeExpandedKeys } = this.state; 40 | const searching = !!(nextProps.showSearch && treeSearchKey && treeSearchKey.length > 0); 41 | this.setState({ 42 | treeNode, 43 | listData, 44 | leafKeys, 45 | treeCheckedKeys, 46 | treeExpandedKeys: searching ? uniq([...treeCheckedKeys, ...expandedKeys]) : treeExpandedKeys, 47 | treeAutoExpandParent: searching, // 搜索的时候 自动展开父节点设为true 48 | }); 49 | } 50 | 51 | generate = (props, state = {}) => { 52 | const { source, target, rowKey, rowTitle, rowChildren, showSearch } = props; 53 | const { treeSearchKey } = state; 54 | 55 | const leafKeys = []; // 叶子节点集合 56 | const listData = []; // 列表数据 57 | const expandedKeys = []; // 搜索时 展开的节点 58 | 59 | const loop = data => data.map(item => { 60 | const { [rowChildren]: children, [rowKey]: key, [rowTitle]: title, ...otherProps } = item; 61 | if (children === undefined) { 62 | leafKeys.push(key); 63 | let nodeTitle = title; 64 | if (showSearch && treeSearchKey && treeSearchKey.length > 0) { // if tree searching 65 | if (title.indexOf(treeSearchKey) > -1) { 66 | expandedKeys.push(key); 67 | const idx = title.indexOf(treeSearchKey); 68 | nodeTitle = ( 69 | 70 | {title.substr(0, idx)} 71 | {treeSearchKey} 72 | {title.substr(idx + treeSearchKey.length)} 73 | 74 | ); 75 | } 76 | } 77 | if (target.indexOf(key) > -1) { 78 | listData.push({ key, title }); 79 | } 80 | return ; 81 | } else { 82 | return ( 83 | 84 | {loop(children)} 85 | 86 | ); 87 | } 88 | }); 89 | 90 | return { 91 | treeNode: loop(source), 92 | leafKeys, 93 | listData: unique(listData, 'key'), 94 | expandedKeys 95 | }; 96 | } 97 | 98 | // tree checkbox checked 99 | treeOnCheck = (checkedKeys, e) => { 100 | if (e.checked) { 101 | if (this.props.onLoadData && hasUnLoadNode([e.node])) { 102 | this.setState({ 103 | unLoadAlert: true 104 | }); 105 | } else { 106 | this.setState({ 107 | treeCheckedKeys: checkedKeys.filter(key => this.state.leafKeys.indexOf(key) > -1), 108 | unLoadAlert: false 109 | }); 110 | } 111 | } else { 112 | this.setState({ 113 | treeCheckedKeys: checkedKeys.filter(key => this.state.leafKeys.indexOf(key) > -1), 114 | unLoadAlert: false 115 | }); 116 | } 117 | } 118 | 119 | // list checkbox checked 120 | listOnCheck = (e, checkedKeys) => { 121 | if (e.target.checked) { 122 | this.setState({ 123 | listCheckedKeys: uniq([...this.state.listCheckedKeys, ...checkedKeys]) 124 | }); 125 | } else { 126 | this.setState({ 127 | listCheckedKeys: this.state.listCheckedKeys.filter(key => checkedKeys.indexOf(key) < 0) 128 | }); 129 | } 130 | } 131 | 132 | // left tree search 133 | onTreeSearch = (value) => { 134 | this.setState({ 135 | treeSearchKey: value 136 | }, () => { 137 | if (this.props.onLoadData && this.props.onTreeSearch) { // async search 138 | this.props.onTreeSearch(value); 139 | } else { 140 | const { treeNode, listData, leafKeys, expandedKeys } = this.generate(this.props, this.state); 141 | const treeCheckedKeys = listData.map(({key}) => key); 142 | this.setState({ 143 | treeNode, 144 | listData, 145 | leafKeys, 146 | treeCheckedKeys, 147 | treeExpandedKeys: uniq([...treeCheckedKeys, ...expandedKeys]), 148 | treeAutoExpandParent: true, // 搜索的时候 自动展开父节点设为true 149 | }); 150 | } 151 | }); 152 | } 153 | 154 | // right list search 155 | onListSearch = (value) => { 156 | this.setState({ 157 | listSearchKey: value 158 | }); 159 | } 160 | 161 | render() { 162 | const { className, treeLoading, sourceTitle, targetTitle, showSearch, onLoadData } = this.props; 163 | const { treeNode, listData, leafKeys, treeCheckedKeys, listCheckedKeys, treeExpandedKeys, treeAutoExpandParent, listSearchKey, unLoadAlert } = this.state; 164 | 165 | const treeTransferClass = classNames({ 166 | 'lucio-tree-transfer': true, 167 | [className]: !!className 168 | }); 169 | 170 | const treeTransferPanelBodyClass = classNames({ 171 | 'tree-transfer-panel-body': true, 172 | 'tree-transfer-panel-body-has-search': showSearch, 173 | }); 174 | 175 | const treeProps = { 176 | checkable: true, 177 | checkedKeys: treeCheckedKeys, 178 | onCheck: this.treeOnCheck, 179 | expandedKeys: treeExpandedKeys, 180 | autoExpandParent: treeAutoExpandParent, 181 | onExpand: (expandedKeys) => { 182 | this.setState({ 183 | treeAutoExpandParent: false, 184 | treeExpandedKeys: expandedKeys, 185 | }); 186 | }, 187 | loadData: onLoadData 188 | }; 189 | 190 | const listHeaderCheckProps = { 191 | checked: listCheckedKeys.length > 0 && listCheckedKeys.length === listData.length, 192 | indeterminate: listCheckedKeys.length > 0 && listCheckedKeys.length < listData.length, 193 | onChange: (e) => this.listOnCheck(e, listData.map(({key}) => key)) 194 | }; 195 | 196 | const operaRightButtonProps = { 197 | type: 'primary', 198 | icon: 'right', 199 | size: 'small', 200 | disabled: difference(treeCheckedKeys, listData.map(({key}) => key)).length === 0 && difference(listData.map(({key}) => key), treeCheckedKeys).length === 0, 201 | onClick: () => { 202 | this.setState({ 203 | unLoadAlert: false 204 | }); 205 | this.props.onChange && this.props.onChange(this.state.treeCheckedKeys); 206 | } 207 | }; 208 | 209 | const operaLeftButtonProps = { 210 | type: 'primary', 211 | icon: 'left', 212 | size: 'small', 213 | disabled: listCheckedKeys.length === 0, 214 | onClick: () => { 215 | this.setState({ 216 | listCheckedKeys: [], 217 | unLoadAlert: false 218 | }); 219 | this.props.onChange && this.props.onChange(this.state.listData.map(({key}) => key).filter(key => this.state.listCheckedKeys.indexOf(key) < 0)); 220 | } 221 | }; 222 | 223 | return ( 224 |
225 |
226 |
227 | {`${treeCheckedKeys.length > 0 ? `${treeCheckedKeys.length}/` : ''}${leafKeys.length}`} 条数据 228 | {sourceTitle} 229 |
230 |
231 | {showSearch ?
: null} 232 | 233 | {unLoadAlert ?
: null} 234 |
235 | 236 | {treeNode} 237 | 238 |
239 |
240 |
241 |
242 |
243 |
246 |
247 |
248 | 249 | {`${listCheckedKeys.length > 0 ? `${listCheckedKeys.length}/` : ''}${listData.length}`} 条数据 250 | {targetTitle} 251 |
252 |
253 | {showSearch ?
: null} 254 |
    255 | { 256 | listData.map(item => ( 257 |
  • 258 | -1} onChange={(e) => this.listOnCheck(e, [item.key])} /> 259 | { 260 | showSearch && listSearchKey && listSearchKey.length > 0 && item.title.indexOf(listSearchKey) > -1 ? ( 261 | 262 | {item.title.substr(0, item.title.indexOf(listSearchKey))} 263 | {listSearchKey} 264 | {item.title.substr(item.title.indexOf(listSearchKey) + listSearchKey.length)} 265 | 266 | ) : {item.title} 267 | } 268 |
  • 269 | )) 270 | } 271 |
272 |
273 |
274 |
275 | ); 276 | } 277 | } 278 | 279 | TreeTransfer.propTypes = { 280 | className: PropTypes.string, 281 | rowKey: PropTypes.string, 282 | rowTitle: PropTypes.string, 283 | rowChildren: PropTypes.string, 284 | source: PropTypes.array, 285 | target: PropTypes.array, 286 | treeLoading: PropTypes.bool, 287 | sourceTitle: PropTypes.string, 288 | targetTitle: PropTypes.string, 289 | onChange: PropTypes.func, 290 | showSearch: PropTypes.bool, 291 | onLoadData: PropTypes.func, 292 | onTreeSearch: PropTypes.func, 293 | }; 294 | 295 | TreeTransfer.defaultProps = { 296 | rowKey: 'key', 297 | rowTitle: 'title', 298 | rowChildren: 'children', 299 | source: [], 300 | target: [], 301 | treeLoading: false, 302 | sourceTitle: '源数据', 303 | targetTitle: '目的数据', 304 | showSearch: false 305 | }; 306 | 307 | export default TreeTransfer; -------------------------------------------------------------------------------- /test/__snapshots__/index.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`TreeTransfer renders correctly 1`] = ` 4 |
7 |
10 |
13 | 16 | 1/2 条数据 17 | 18 | 21 | 源数据 22 | 23 |
24 |
27 |
30 |
33 |
36 |
    41 |
  • 45 | 48 | 52 | 66 | 67 | 68 | 71 | 74 | 75 | 79 | 82 | 0 83 | 84 | 85 |
      90 |
    • 94 | 97 | 100 | 103 | 104 | 108 | 111 | 0-0 112 | 113 | 114 |
    • 115 |
    • 119 | 122 | 125 | 128 | 129 | 133 | 136 | 0-1 137 | 138 | 139 |
    • 140 |
    141 |
  • 142 |
143 |
144 |
145 |
146 |
147 |
148 |
151 | 176 | 201 |
202 |
205 |
208 | 223 | 226 | 1 条数据 227 | 228 | 231 | 目的数据 232 | 233 |
234 |
237 |
    240 |
  • 241 | 256 | 257 | 0-1 258 | 259 |
  • 260 |
261 |
262 |
263 |
264 | `; 265 | 266 | exports[`TreeTransfer should support loading 1`] = ` 267 |
270 |
273 |
276 | 279 | 1/2 条数据 280 | 281 | 284 | 源数据 285 | 286 |
287 |
290 |
293 |
294 |
297 | 300 | 303 | 306 | 309 | 312 | 313 |
314 |
315 |
318 |
321 |
    326 |
  • 330 | 333 | 337 | 351 | 352 | 353 | 356 | 359 | 360 | 364 | 367 | 0 368 | 369 | 370 |
      375 |
    • 379 | 382 | 385 | 388 | 389 | 393 | 396 | 0-0 397 | 398 | 399 |
    • 400 |
    • 404 | 407 | 410 | 413 | 414 | 418 | 421 | 0-1 422 | 423 | 424 |
    • 425 |
    426 |
  • 427 |
428 |
429 |
430 |
431 |
432 |
433 |
436 | 461 | 486 |
487 |
490 |
493 | 508 | 511 | 1 条数据 512 | 513 | 516 | 目的数据 517 | 518 |
519 |
522 |
    525 |
  • 526 | 541 | 542 | 0-1 543 | 544 |
  • 545 |
546 |
547 |
548 |
549 | `; 550 | 551 | exports[`TreeTransfer should support sourceTitle and targetTitle 1`] = ` 552 |
555 |
558 |
561 | 564 | 1/2 条数据 565 | 566 | 569 | 1 570 | 571 |
572 |
575 |
578 |
581 |
584 |
    589 |
  • 593 | 596 | 600 | 614 | 615 | 616 | 619 | 622 | 623 | 627 | 630 | 0 631 | 632 | 633 |
      638 |
    • 642 | 645 | 648 | 651 | 652 | 656 | 659 | 0-0 660 | 661 | 662 |
    • 663 |
    • 667 | 670 | 673 | 676 | 677 | 681 | 684 | 0-1 685 | 686 | 687 |
    • 688 |
    689 |
  • 690 |
691 |
692 |
693 |
694 |
695 |
696 |
699 | 724 | 749 |
750 |
753 |
756 | 771 | 774 | 1 条数据 775 | 776 | 779 | 2 780 | 781 |
782 |
785 |
    788 |
  • 789 | 804 | 805 | 0-1 806 | 807 |
  • 808 |
809 |
810 |
811 |
812 | `; 813 | --------------------------------------------------------------------------------