├── 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 | [](https://npmjs.org/package/antd-tree-transfer)
11 | [](https://npmjs.org/package/antd-tree-transfer)
12 | [](https://codecov.io/gh/luciojs/tree-transfer/branch/master)
13 |
14 | ## Install
15 |
16 | [](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 |
244 |
245 |
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 |
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 |
263 |
264 | `;
265 |
266 | exports[`TreeTransfer should support loading 1`] = `
267 |
270 |
273 |
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 |
548 |
549 | `;
550 |
551 | exports[`TreeTransfer should support sourceTitle and targetTitle 1`] = `
552 |
555 |
558 |
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 |
811 |
812 | `;
813 |
--------------------------------------------------------------------------------