├── .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 | ![](react-virtualized-tree-checkable.gif) 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 | 3 | 4 | arrownormal 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/assets/arrow-rt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | arrowdown 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /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 | 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 |