├── .DS_Store
├── .babelrc
├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── package.json
├── react-virtualized-tree-checkable.gif
├── src
├── .DS_Store
├── assets
│ ├── arrow-dn.svg
│ ├── arrow-rt.svg
│ └── loader.gif
├── nodeShape.js
├── tree.css
├── tree.js
└── treeNode.js
└── webpack.config.js
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Anuj16/react-tree-virtualized/0bde9e1c0d78b5e5eb6cbf79cc4fbeb5157143b5/.DS_Store
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "react",
4 | "env",
5 | "stage-0"
6 | ]
7 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /lib
2 | /node_modules
3 | package-lock.json
4 | yarn.lock
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .babelrc
2 | webpack.config.js
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Shivratna Kumar
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-tree-virtualized
2 | A react tree component which can handle huge number of nodes using builtin virtualisation. It also supports asynchronous data fetching.
3 |
4 | - Demo :-
5 |
6 | 
7 |
8 | # Usage
9 | ### Installation
10 | Using npm
11 |
12 | ```
13 | npm install react-tree-virtualized --save
14 | ```
15 |
16 | Using yarn
17 | ```
18 | yarn add react-tree-virtualized
19 | ```
20 |
21 | ### Include css
22 |
23 | ```
24 | import 'react-tree-virtualized/src/tree.css'
25 | ```
26 |
27 | ### Sample Usage
28 | Note - react-tree-virtualized is stateless, so you must update its `checked`, `expanded` and `loading` properties whenever any changes occur.
29 |
30 | ```
31 | import React, { Component } from 'react';
32 | import Tree from 'react-tree-virtualized';
33 | import { nodes } from './data';
34 | import 'react-tree-virtualized/src/tree.css';
35 |
36 | class App extends Component {
37 | constructor() {
38 | super();
39 | this.state = {
40 | checked: [],
41 | expanded: [],
42 | loading: []
43 | }
44 | }
45 |
46 | onCheck = (checked, node) => {
47 | this.setState({checked});
48 | }
49 |
50 | onExpand = (expanded, loading, node) => {
51 | this.setState({expanded});
52 | }
53 |
54 | render() {
55 | const { checked, expanded, loading } = this.state;
56 |
57 | return (
58 |
59 |
67 |
68 | );
69 | }
70 | }
71 |
72 | export default App;
73 |
74 | ```
75 |
76 | # Properties
77 |
78 | ### Tree Props
79 |
80 | | prop | type | description | default value |
81 | | ------ | ------ | ------ | ------ |
82 | | nodes | `array` | **Required**. Array of tree nodes and its children. |
83 | | checked | `array` | Array of node values which are checked | `[]`
84 | | expanded | `array` | Array of node values which are expanded | `[]`
85 | | loading | `array` | Array of node values of which are in loading state. Can be used when the node's children needs to be **fetched from an API** | `[]`
86 | | noCascade | `boolean` | If `true`, changing a children node's state will not affect the parent nodes. | `false`
87 | | optimisticToggle | `boolean` | If `true`, changing a partially checked node's state will select all children. If `false`, it will deselect. | `true`
88 | | showNodeIcon | `boolean` | If `false`, node's icon will not be displayed. | `true`
89 | | onCheck | `function` | A function which will be called on selecting/deselecting any node. The function will get two parameters, `checked` and `node`. `checked` is the array of all the node values which are checked and `node` is the clicked node object. | `() => {}`
90 | | onExpand | `function` | A function which will be called on expanding any node. The function will get three parameters, `expanded`, `loading` and `node`. `expanded` is the array of all the node values which are expanded. `loading` is the array of all the node values which are in loading state. `node` is the expanded node object. | `() => {}`
91 |
92 | ### Node props
93 | Every node object of above `nodes` prop can have following props.
94 |
95 | | prop | type | description | default value |
96 | | ------ | ------ | ------ | ------ |
97 | | label | `string` | **Required**. The display name of a node.
98 | | value | `string` | **Required** A unique value for the node.
99 | | level | `number` | **Required**. The level of nesting that the node has to be at. Ex - for root node the level will be 0 and for all the immediate children of the root, the level will be 1. |
100 | | children | `array` | An array of children nodes. | `null`
101 | | isLeaf | `boolean` | If `true` expand/collapse icon will not be displayed | `false`
102 | | disabled | `boolean` | If `true`, the node will not be selectable but still expandable. | `false`
103 | | icon | `node` | The icon that should be displayed for the node. | `null`
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-tree-virtualized",
3 | "version": "1.1.0-beta.0",
4 | "description": "A react tree component which can handle huge number of nodes using builtin virtualisation",
5 | "main": "./lib/tree.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "build": "webpack"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/Anuj16/react-tree-virtualized.git"
13 | },
14 | "keywords": [
15 | "react",
16 | "tree",
17 | "virtualized",
18 | "component",
19 | "scalable"
20 | ],
21 | "author": "kumarshivratna@gmail.com",
22 | "license": "MIT",
23 | "bugs": {
24 | "url": "https://github.com/Anuj16/react-tree-virtualized/issues"
25 | },
26 | "homepage": "https://github.com/Anuj16/react-tree-virtualized#readme",
27 | "peerDependencies": {
28 | "prop-types": "15.6.2",
29 | "react": "16.5.2",
30 | "react-dom": "16.5.2"
31 | },
32 | "dependencies": {
33 | "classnames": "2.2.6",
34 | "lodash": "4.17.11",
35 | "shortid": "2.2.13"
36 | },
37 | "devDependencies": {
38 | "babel-core": "6.21.0",
39 | "babel-loader": "7.1.4",
40 | "babel-preset-env": "1.6.1",
41 | "babel-preset-react": "6.16.0",
42 | "babel-preset-stage-0": "6.24.1",
43 | "extract-text-webpack-plugin": "3.0.2",
44 | "file-loader": "2.0.0",
45 | "path": "0.12.7",
46 | "prop-types": "15.6.0",
47 | "react": "16.5.2",
48 | "react-dom": "16.5.2",
49 | "url-loader": "1.1.2",
50 | "webpack": "4.20.2",
51 | "webpack-cli": "3.1.1"
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/react-virtualized-tree-checkable.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Anuj16/react-tree-virtualized/0bde9e1c0d78b5e5eb6cbf79cc4fbeb5157143b5/react-virtualized-tree-checkable.gif
--------------------------------------------------------------------------------
/src/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Anuj16/react-tree-virtualized/0bde9e1c0d78b5e5eb6cbf79cc4fbeb5157143b5/src/.DS_Store
--------------------------------------------------------------------------------
/src/assets/arrow-dn.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/arrow-rt.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/loader.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Anuj16/react-tree-virtualized/0bde9e1c0d78b5e5eb6cbf79cc4fbeb5157143b5/src/assets/loader.gif
--------------------------------------------------------------------------------
/src/nodeShape.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 |
3 | const nodeShape = {
4 | label: PropTypes.string.isRequired,
5 | value: PropTypes.oneOfType([
6 | PropTypes.string,
7 | PropTypes.number,
8 | ]).isRequired,
9 |
10 | icon: PropTypes.node,
11 | };
12 |
13 | const nodeShapeWithChildren = PropTypes.oneOfType([
14 | PropTypes.shape(nodeShape),
15 | PropTypes.shape({
16 | ...nodeShape,
17 | children: PropTypes.arrayOf(nodeShape).isRequired,
18 | }),
19 | ]);
20 |
21 | export default nodeShapeWithChildren;
22 |
--------------------------------------------------------------------------------
/src/tree.css:
--------------------------------------------------------------------------------
1 | .react-virtualized-tree {
2 | margin: 0;
3 | height: 100%;
4 | overflow-y: auto;
5 | overflow-x: auto;
6 | font-size: 13px;
7 | }
8 | .react-virtualized-tree button {
9 | line-height: normal;
10 | color: inherit;
11 | }
12 | .react-virtualized-tree button:disabled {
13 | cursor: not-allowed;
14 | }
15 | .react-virtualized-tree label {
16 | margin-bottom: 0;
17 | width: 100%;
18 | position: relative;
19 | min-height: 16px;
20 | text-align: left;
21 | }
22 | .react-virtualized-tree input {
23 | display: none;
24 | }
25 | .react-virtualized-tree .rvt-disabled {
26 | opacity: .75;
27 | }
28 | .react-virtualized-tree .rvt-disabled label {
29 | cursor: not-allowed;
30 | }
31 | .react-virtualized-tree .rvt-disabled label:hover {
32 | background: transparent;
33 | }
34 | .react-virtualized-tree .rvt-collapse,
35 | .react-virtualized-tree .rvt-checkbox {
36 | margin: 0 5px;
37 | padding: 0;
38 | cursor: default;
39 | }
40 | .react-virtualized-tree .rvt-node:focus,
41 | .react-virtualized-tree .rvt-collapse:focus {
42 | outline: none;
43 | }
44 | .react-virtualized-tree .rvt-collapse *,
45 | .react-virtualized-tree .rvt-node-icon *,
46 | .react-virtualized-tree .rvt-checkbox * {
47 | display: inline-block;
48 | width: 12px;
49 | }
50 | .react-virtualized-tree .rvt-node-icon {
51 | display: inline-block;
52 | vertical-align: middle;
53 | }
54 | .react-virtualized-tree .rvt-node-icon img {
55 | max-width: 20px;
56 | max-height: 20px;
57 | margin: 0 2px 0 0;
58 | width: 20px;
59 | vertical-align: middle;
60 | }
61 | .react-virtualized-tree .rvt-text {
62 | display: -webkit-flex;
63 | display: -ms-flexbox;
64 | display: -ms-flex;
65 | display: flex;
66 | -webkit-align-items: center;
67 | -ms-align-items: center;
68 | align-items: center;
69 | padding: 7px 0 5px 0;
70 | min-height: 30px;
71 | position: relative;
72 | z-index: 1;
73 | font-family: 'SF Pro Text Medium', 'HelveticaNeue-Medium', sans-serif;
74 | white-space: nowrap;
75 | }
76 | .react-virtualized-tree .rvt-text .rvt-title {
77 | padding: 2px 5px 2px 3px;
78 | color: #525252;
79 | }
80 | .react-virtualized-tree .rvt-text .disabled .rvt-title {
81 | color: rgba(0, 0, 0, 0.4);
82 | }
83 | .react-virtualized-tree .rvt-text.even-node {
84 | background: #f5f5f5;
85 | }
86 | .react-virtualized-tree .rvt-collapse {
87 | border: 0;
88 | background: none;
89 | line-height: normal;
90 | color: inherit;
91 | font-size: 12px;
92 | }
93 | .react-virtualized-tree.static-tree .rvt-text.selected-node {
94 | background: #4a90e2;
95 | }
96 | .react-virtualized-tree.static-tree .rvt-text.selected-node .rvt-icon-expand-open {
97 | filter: alpha(opacity=100);
98 | -webkit-opacity: 1;
99 | -moz-opacity: 1;
100 | opacity: 1;
101 | }
102 | .react-virtualized-tree.static-tree .rvt-text.selected-node .rvt-title {
103 | color: #fff;
104 | }
105 | .plainBackupTreeContainer .react-virtualized-tree.static-tree .rvt-text.even-node {
106 | background: #fff;
107 | }
108 | .plainBackupTreeContainer .react-virtualized-tree.static-tree .rvt-text.even-node.selected-node {
109 | background: #4a90e2;
110 | }
111 | .rvt-icon.rvt-icon-uncheck,
112 | .rvt-icon.rvt-icon-check,
113 | .rvt-icon-half-check {
114 | background-color: #fff;
115 | border: 1px solid #a2a2a2;
116 | display: inline-block;
117 | height: 12px;
118 | position: relative;
119 | width: 12px;
120 | border-radius: 3px;
121 | margin: 2px 3px -2px 0;
122 | }
123 | .rvt-icon.rvt-icon-uncheck:before,
124 | .rvt-icon.rvt-icon-check:before,
125 | .rvt-icon-half-check:before {
126 | content: "";
127 | }
128 | .rvt-icon.rvt-icon-check {
129 | border: 1px solid #1973fd;
130 | position: relative;
131 | background: #1973fd;
132 | }
133 | .rvt-icon.rvt-icon-check:before {
134 | position: absolute;
135 | content: "";
136 | width: 4px;
137 | border-bottom: 2px solid #fff;
138 | height: 7px;
139 | border-right: 2px solid #fff;
140 | margin: 0 0 0 3px;
141 | transform: rotate(45deg);
142 | -webkit-transform: rotate(45deg);
143 | -moz-transform: rotate(45deg);
144 | -ms-transform: rotate(45deg);
145 | -o-transform: rotate(45deg);
146 | }
147 | .rvt-icon.rvt-icon-half-check {
148 | border: 1px solid #1973fd;
149 | position: relative;
150 | }
151 | .rvt-icon.rvt-icon-half-check:before {
152 | position: absolute;
153 | content: "";
154 | width: 8px;
155 | height: 8px;
156 | background: #1973fd;
157 | margin: 2px 0 0 2px;
158 | border-radius: 2px;
159 | }
160 | .disabled .rvt-icon.rvt-icon-uncheck,
161 | .disabled .rvt-icon.rvt-icon-check {
162 | filter: alpha(opacity=50);
163 | -webkit-opacity: 0.5;
164 | -moz-opacity: 0.5;
165 | opacity: 0.5;
166 | background: #fff;
167 | border: 1px solid #a2a2a2;
168 | }
169 | .disabled .rvt-icon.rvt-icon-check:before {
170 | border-bottom: 2px solid #a2a2a2;
171 | border-right: 2px solid #a2a2a2;
172 | }
173 | .rvt-collapse > .rvt-icon-expand-close {
174 | float: left;
175 | margin-top: -1px;
176 | height: 14px;
177 | background-image: url("./assets/arrow-rt.svg");
178 | background-size: 24px;
179 | background-position: -5px -5px;
180 | filter: alpha(opacity=50);
181 | -webkit-opacity: 0.5;
182 | -moz-opacity: 0.5;
183 | opacity: 0.5;
184 | }
185 | .rvt-icon-expand-open {
186 | float: left;
187 | margin-top: 0;
188 | height: 14px;
189 | background-image: url("./assets/arrow-dn.svg");
190 | background-size: 24px;
191 | background-position: -5px -5px;
192 | filter: alpha(opacity=50);
193 | -webkit-opacity: 0.5;
194 | -moz-opacity: 0.5;
195 | opacity: 0.5;
196 | }
197 | .static-tree .rvt-collapse > .selected-node .rvt-icon-expand-close {
198 | filter: alpha(opacity=100);
199 | -webkit-opacity: 1;
200 | -moz-opacity: 1;
201 | opacity: 1;
202 | }
203 | .static-tree .selected-node .rvt-icon-expand-close {
204 | filter: alpha(opacity=100);
205 | -webkit-opacity: 1;
206 | -moz-opacity: 1;
207 | opacity: 1;
208 | }
209 | .rvt-collapse > .rvt-icon-expand-close:hover {
210 | opacity: 1;
211 | }
212 | .rvt-node-icon {
213 | color: #33c;
214 | }
215 | .react-virtualized-tree .hiddenNode .rvt-title {
216 | color: rgba(0, 0, 0, 0.4);
217 | }
218 |
--------------------------------------------------------------------------------
/src/tree.js:
--------------------------------------------------------------------------------
1 | import classNames from 'classnames';
2 | import { isEqual } from 'lodash';
3 | import PropTypes from 'prop-types';
4 | import React from 'react';
5 | import shortid from 'shortid';
6 | import ReactDOM from 'react-dom';
7 |
8 | import TreeNode from './treeNode';
9 | import nodeShape from './nodeShape';
10 |
11 | // import './tree.css';
12 |
13 |
14 | class Tree extends React.Component {
15 | static propTypes = {
16 | checkable: PropTypes.bool,
17 | childHeight: PropTypes.number,
18 | nodes: PropTypes.arrayOf(nodeShape).isRequired,
19 | checked: PropTypes.arrayOf(PropTypes.string),
20 | loading: PropTypes.arrayOf(PropTypes.string),
21 | expandDisabled: PropTypes.bool,
22 | expanded: PropTypes.arrayOf(PropTypes.string),
23 | name: PropTypes.string,
24 | nameAsArray: PropTypes.bool,
25 | noCascade: PropTypes.bool,
26 | optimisticToggle: PropTypes.bool,
27 | showNodeIcon: PropTypes.bool,
28 | onCheck: PropTypes.func,
29 | onExpand: PropTypes.func,
30 | };
31 |
32 | static defaultProps = {
33 | checkable: true,
34 | childHeight: 30,
35 | checked: [],
36 | loading: [],
37 | expandDisabled: false,
38 | expanded: [],
39 | name: undefined,
40 | nameAsArray: false,
41 | noCascade: false,
42 | optimisticToggle: true,
43 | showNodeIcon: true,
44 | onCheck: () => {},
45 | onExpand: () => {},
46 | };
47 |
48 | constructor(props) {
49 | super(props);
50 |
51 | this.id = `rvt-${shortid.generate()}`;
52 | this.nodes = {};
53 |
54 | this.flattenNodes(props.nodes, props.expanded);
55 | this.unserializeLists({
56 | checked: props.checked,
57 | expanded: props.expanded,
58 | loading: props.loading,
59 | });
60 |
61 | this.onCheck = this.onCheck.bind(this);
62 | this.onExpand = this.onExpand.bind(this);
63 |
64 | // Virtualization related default values. These changes on componentDidMount and onScroll.
65 | this.state = {
66 | numberOfNodesToRender: 10,
67 | scrollTop: 0,
68 | startNodeIndex: 0,
69 | endNodeIndex: 9,
70 | }
71 | }
72 |
73 | componentDidMount = () => {
74 | const treeContainer = this.treeContainer,
75 | numberOfNodesToRender = Math.floor(treeContainer.clientHeight / this.props.childHeight) + 2,
76 | startNodeIndex = 0,
77 | endNodeIndex = startNodeIndex + numberOfNodesToRender - 1;
78 |
79 | this.setState({
80 | numberOfNodesToRender,
81 | startNodeIndex,
82 | endNodeIndex
83 | });
84 | }
85 |
86 | componentWillReceiveProps({ nodes, checked, expanded, loading }) {
87 | if (!isEqual(this.props.nodes, nodes) || !isEqual(this.props.expanded, expanded)) {
88 | this.nodes = {};
89 | this.flattenNodes(nodes, expanded);
90 | }
91 |
92 | this.unserializeLists({ checked, expanded, loading });
93 | }
94 |
95 | onCheck(node) {
96 | const { checkable, noCascade, onCheck } = this.props;
97 | this.toggleChecked(node, node.checked, noCascade);
98 |
99 | if(checkable) {
100 | onCheck(this.serializeList('checked'), node);
101 | } else {
102 | onCheck([node.value], node);
103 | }
104 | }
105 |
106 | onExpand(node) {
107 | const { onExpand } = this.props;
108 |
109 | this.toggleNode('expanded', node, node.expanded);
110 | this.toggleNode('loading', node, node.expanded);
111 | onExpand(this.serializeList('expanded'), this.serializeList('loading'), node);
112 | }
113 |
114 | getCheckState(node, noCascade) {
115 |
116 | // If halfChecked key is true and there are no children, return 2 irrespective the number of children
117 | if( this.isChildrenEmpty(node) && node.halfChecked ) {
118 | return 2;
119 | }
120 |
121 | if (this.isChildrenEmpty(node) || noCascade) {
122 | return node.checked ? 1 : 0;
123 | }
124 |
125 | if (this.isEveryChildChecked(node)) {
126 | return 1;
127 | }
128 |
129 | if (this.isSomeChildChecked(node)) {
130 | return 2;
131 | }
132 |
133 | return 0;
134 | }
135 |
136 | getLoadingState = (node) => {
137 | if(node.loading) {
138 | return true;
139 | }
140 |
141 | return false;
142 | }
143 |
144 | toggleChecked(node, isChecked, noCascade) {
145 | if (this.isChildrenEmpty(node) ) {
146 | // Set the check status of a leaf node or an uncoupled parent if the node is not disabled
147 | if(!node.disabled) {
148 | this.toggleNode('checked', node, isChecked);
149 | }
150 | } else {
151 | this.toggleNode('checked', node, isChecked);
152 | // Percolate check status down to all children
153 | node.children.forEach((child) => {
154 | this.toggleChecked(child, isChecked);
155 | });
156 | }
157 | }
158 |
159 | toggleNode(key, node, toggleValue) {
160 | this.nodes[node.value][key] = toggleValue;
161 | }
162 |
163 | flattenNodes(nodes, expanded, parentNodeValue='root') {
164 | if (!Array.isArray(nodes) || nodes.length === 0) {
165 | return;
166 | }
167 |
168 | nodes.forEach((node, index) => {
169 | this.nodes[node.value] = {};
170 |
171 | this.nodes[node.value]['parent'] = parentNodeValue
172 |
173 | // Copying each key of the node
174 | for(let key in node) {
175 | this.nodes[node.value][key] = node[key];
176 | }
177 |
178 | this.flattenNodes(node.children, expanded, node.value);
179 | });
180 | }
181 |
182 | unserializeLists(lists) {
183 | // Reset values to false
184 | Object.keys(this.nodes).forEach((value) => {
185 | Object.keys(lists).forEach((listKey) => {
186 | this.nodes[value][listKey] = false;
187 | });
188 | });
189 |
190 | // Unserialize values and set their nodes to true
191 | Object.keys(lists).forEach((listKey) => {
192 | lists[listKey].forEach((value) => {
193 | this.nodes[value][listKey] = true;
194 | });
195 | });
196 | }
197 |
198 | serializeList(key) {
199 | const list = [];
200 |
201 | Object.keys(this.nodes).forEach((value) => {
202 | if (this.nodes[value][key]) {
203 | list.push(value);
204 | }
205 | });
206 |
207 | return list;
208 | }
209 |
210 | isEveryChildChecked(node) {
211 | return node.children.every((child) => {
212 | if (!this.isChildrenEmpty(child)) {
213 | return this.isEveryChildChecked(child);
214 | }
215 |
216 | return this.nodes[child.value].checked;
217 | });
218 | }
219 |
220 | isSomeChildChecked(node) {
221 | return node.children.some((child) => {
222 | if (!this.isChildrenEmpty(child)) {
223 | return this.isSomeChildChecked(child);
224 | }
225 |
226 | return this.nodes[child.value].checked || this.nodes[child.value].halfChecked;
227 | });
228 | }
229 |
230 | renderTreeNodes(nodes) {
231 | const { checkable, expandDisabled, noCascade, optimisticToggle, showNodeIcon } = this.props;
232 | const treeNodes = nodes.map((node) => {
233 | const key = `${node.value}`;
234 | const checked = this.getCheckState(node, noCascade);
235 | const loading = this.getLoadingState(node);
236 | let firstNodeIndex = (this.state.startNodeIndex > 0 ? this.state.startNodeIndex-1 : this.state.startNodeIndex);
237 |
238 | return (
239 |
265 |
266 | );
267 | });
268 |
269 | return treeNodes;
270 | }
271 |
272 | renderHiddenInput() {
273 | if (this.props.name === undefined) {
274 | return null;
275 | }
276 |
277 | if (this.props.nameAsArray) {
278 | return this.renderArrayHiddenInput();
279 | }
280 |
281 | return this.renderJoinedHiddenInput();
282 | }
283 |
284 | renderArrayHiddenInput() {
285 | return this.props.checked.map((value) => {
286 | const name = `${this.props.name}[]`;
287 |
288 | return ;
289 | });
290 | }
291 |
292 | renderJoinedHiddenInput() {
293 | const checked = this.props.checked.join(',');
294 |
295 | return ;
296 | }
297 |
298 | getNodesToRender = (startNodeIndex, endNodeIndex, nodesArray) => {
299 | let nodesToRender = [];
300 | for(let i=0; i= nodesArray[i].index) {
302 | nodesToRender.push(nodesArray[i]);
303 | }
304 | }
305 |
306 | nodesToRender.sort(function(a, b) {
307 | return parseFloat(a.index) - parseFloat(b.index);
308 | });
309 |
310 | return nodesToRender;
311 | }
312 |
313 | onScroll = () => {
314 | if(Object.keys(this.nodes).length <= this.numberOfNodesToRender) {
315 | return;
316 | }
317 | const container = this.treeContainer,
318 | containerDom = ReactDOM.findDOMNode(container),
319 | scrollTop = containerDom.scrollTop,
320 | startNodePosition = Math.ceil(scrollTop / this.props.childHeight),
321 | startNodeIndex = startNodePosition === 0 ? startNodePosition : startNodePosition - 1,
322 | endNodeIndex = startNodeIndex + this.state.numberOfNodesToRender - 1;
323 |
324 | this.setState({
325 | scrollTop,
326 | startNodeIndex,
327 | endNodeIndex
328 | });
329 | }
330 |
331 | isAnyParentCollapsed = (nodes, node) => {
332 | // If the parent's value is root it means there is no parent to this node.
333 | // We need to show the root node irrespective of the fact that it's expanded or collapsed.
334 | if(node.parent === 'root') {
335 | return false;
336 | }
337 |
338 | if(nodes[node.parent] && !nodes[node.parent].expanded) {
339 | return true;
340 | } else {
341 | return this.isAnyParentCollapsed(nodes, nodes[node.parent])
342 | }
343 | }
344 |
345 | getDisplaybleNodesArray = (nodes) => {
346 | let nodesArray = [];
347 | Object.keys(nodes).forEach((key) => {
348 | if(!this.isAnyParentCollapsed(nodes, nodes[key]) ) {
349 | nodesArray.push(nodes[key]);
350 | }
351 | });
352 |
353 | return nodesArray;
354 | }
355 |
356 | updateNodeMetaData = (nodesArray) => {
357 | let updatedNodesArray = nodesArray.map(function(node, index) {
358 | node.index = index;
359 | node.evenNode = ( index % 2 ) === 0;
360 | return node;
361 | });
362 |
363 | return updatedNodesArray;
364 | }
365 |
366 | isChildrenEmpty = (node) => {
367 | if(node.children === null) return true;
368 |
369 | if(Array.isArray(node.children) && node.children.length <=0) return true;
370 |
371 | return false;
372 | }
373 |
374 | render() {
375 | let nodesArray = this.getDisplaybleNodesArray(this.nodes);
376 | nodesArray = this.updateNodeMetaData(nodesArray);
377 | const totalNodes = nodesArray.length,
378 | startNodeIndex = this.state.startNodeIndex,
379 | endNodeIndex = this.state.endNodeIndex,
380 | childHeight = this.props.childHeight;
381 |
382 | let topDivHeight = 0,
383 | bottomDivHeight = 0;
384 |
385 | if(totalNodes <= this.state.numberOfNodesToRender) {
386 | topDivHeight = 0;
387 | bottomDivHeight = 0;
388 | } else {
389 | topDivHeight = startNodeIndex * childHeight;
390 | bottomDivHeight = ( totalNodes - startNodeIndex - this.state.numberOfNodesToRender ) * childHeight;
391 | if(bottomDivHeight < 0) {
392 | bottomDivHeight = 0;
393 | }
394 | }
395 |
396 |
397 | const nodesToRender = this.getNodesToRender(startNodeIndex, endNodeIndex, nodesArray);
398 |
399 | const treeNodes = this.renderTreeNodes(nodesToRender);
400 | const className = classNames({
401 | 'react-virtualized-tree': true,
402 | 'static-tree': !this.props.checkable,
403 | 'rvt-disabled': this.props.disabled,
404 | });
405 |
406 | return (
407 | this.onScroll()} ref={(ref) => this.treeContainer = ref}>
408 | {this.renderHiddenInput()}
409 |
410 | {treeNodes}
411 |
412 |
413 | );
414 | }
415 | }
416 |
417 | export default Tree;
418 |
--------------------------------------------------------------------------------
/src/treeNode.js:
--------------------------------------------------------------------------------
1 | import classNames from 'classnames';
2 | import PropTypes from 'prop-types';
3 | import React from 'react';
4 |
5 | import nodeShape from './nodeShape';
6 | import loaderImage from './assets/loader.gif';
7 |
8 | class TreeNode extends React.Component {
9 | static propTypes = {
10 | isLeaf: PropTypes.bool.isRequired,
11 | checked: PropTypes.number.isRequired,
12 | disabled: PropTypes.bool,
13 | loading: PropTypes.bool,
14 | expandDisabled: PropTypes.bool.isRequired,
15 | expanded: PropTypes.bool.isRequired,
16 | label: PropTypes.string.isRequired,
17 | optimisticToggle: PropTypes.bool.isRequired,
18 | showNodeIcon: PropTypes.bool.isRequired,
19 | treeId: PropTypes.string.isRequired,
20 | value: PropTypes.string.isRequired,
21 | onCheck: PropTypes.func.isRequired,
22 | onExpand: PropTypes.func.isRequired,
23 |
24 | children: PropTypes.node,
25 | className: PropTypes.string,
26 | icon: PropTypes.node,
27 | rawChildren: PropTypes.arrayOf(nodeShape),
28 |
29 | index: PropTypes.number,
30 | level: PropTypes.number.isRequired
31 | };
32 |
33 | static defaultProps = {
34 | children: null,
35 | className: null,
36 | icon: null,
37 | rawChildren: null,
38 | disabled: false,
39 | loading: false,
40 | index: null
41 | };
42 |
43 | constructor(props) {
44 | super(props);
45 |
46 | this.onCheck = this.onCheck.bind(this);
47 | this.onExpand = this.onExpand.bind(this);
48 | }
49 |
50 | onCheck() {
51 | let isChecked = false;
52 |
53 | // Toggle off state to checked
54 | if (this.props.checked === 0) {
55 | isChecked = true;
56 | }
57 |
58 | // Toggle partial state based on cascade model
59 | if (this.props.checked === 2) {
60 | isChecked = this.props.optimisticToggle;
61 | }
62 |
63 | this.props.onCheck({
64 | value: this.props.value,
65 | checked: isChecked,
66 | children: this.props.rawChildren,
67 | });
68 | }
69 |
70 | onExpand() {
71 | let isChecked = false;
72 |
73 | if(this.props.checked === 0) {
74 | isChecked = false;
75 | } else if (this.props.checked === 1) {
76 | isChecked = true;
77 | } else if(this.props.isChecked === 2) {
78 | isChecked = this.props.optimisticToggle;
79 | }
80 |
81 | const expanded = !this.props.expanded;
82 |
83 | let loading = false;
84 | if(expanded) {
85 | loading = true;
86 | }
87 |
88 | this.props.onExpand({
89 | value: this.props.value,
90 | checked: isChecked,
91 | expanded: expanded,
92 | loading: loading,
93 | halfChecked: (this.props.checked === 2)
94 | });
95 | }
96 |
97 | hasChildren() {
98 | return this.props.rawChildren !== null;
99 | }
100 |
101 | renderCollapseButton() {
102 | const { expandDisabled, isLeaf } = this.props;
103 |
104 | if (isLeaf) {
105 | return (
106 |
107 |
108 |
109 | );
110 | }
111 |
112 | if(this.props.loading) {
113 | return (
114 |
120 |
121 |
122 | );
123 | }
124 |
125 | return (
126 |
135 | );
136 | }
137 |
138 | renderCollapseIcon() {
139 |
140 |
141 | if (!this.props.expanded) {
142 | return ;
143 | }
144 |
145 | return ;
146 | }
147 |
148 | renderCheckboxIcon() {
149 | if (this.props.checked === 0) {
150 | return ;
151 | }
152 |
153 | if (this.props.checked === 1) {
154 | return ;
155 | }
156 |
157 | return ;
158 | }
159 |
160 | renderNodeIcon() {
161 | if (this.props.icon !== null) {
162 | return
;
163 | }
164 |
165 | if (!this.hasChildren()) {
166 | return ;
167 | }
168 |
169 | if (!this.props.expanded) {
170 | return ;
171 | }
172 |
173 | return ;
174 | }
175 |
176 | renderChildren() {
177 | if (!this.props.expanded) {
178 | return null;
179 | }
180 |
181 | return this.props.children;
182 | }
183 |
184 | render() {
185 | const { checkable, evenNode, checked, className, disabled, treeId, label, showNodeIcon, value, tooltipText, expanded, index, level, firstNodeIndex, isLeaf } = this.props;
186 | const inputId = `${treeId}-${value}`;
187 | const nodeClass = classNames({
188 | 'rvt-node': true,
189 | 'rvt-node-parent': this.hasChildren(),
190 | 'rvt-node-leaf': !this.hasChildren(),
191 | 'expanded-node': expanded
192 | }, className);
193 | const outerSpanClass = classNames({
194 | 'rvt-text': true,
195 | 'selected-node': (checked === 1),
196 | 'even-node': evenNode,
197 | });
198 | let labelText = isLeaf ? '': 'Folder'
199 | let blankSpacesArray = [];
200 | for (let i=0; i );
202 | }
203 | return (
204 |
205 |
206 |
207 | {blankSpacesArray}
208 | {this.renderCollapseButton()}
209 |
241 |
242 | {this.renderChildren()}
243 |
244 | );
245 | }
246 | }
247 |
248 | export default TreeNode;
249 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | const ExtractTextPlugin = require('extract-text-webpack-plugin');
3 |
4 | module.exports = {
5 | mode: 'production',
6 | entry: './src/tree.js',
7 | output: {
8 | path: path.resolve('lib'),
9 | filename: 'tree.js',
10 | libraryTarget: 'commonjs2'
11 | },
12 | module: {
13 | rules : [
14 | {
15 | test: /\.(png|svg|jpg|gif)$/,
16 | use: [
17 | {
18 | loader: 'url-loader',
19 | options:{
20 | fallback: "file-loader",
21 | name: "[name][md5:hash].[ext]",
22 | outputPath: 'assets/',
23 | publicPath: '/assets/'
24 | }
25 | }
26 | ]
27 | },
28 | {
29 | test: /\.(js|jsx)$/,
30 | use: ["babel-loader"],
31 | exclude: /node_modules/,
32 | }
33 | ]
34 | }
35 | }
--------------------------------------------------------------------------------