├── .gitignore ├── .prettierrc ├── getSortedParentChildrenMap.js ├── example ├── entry.js ├── webpack.config.js ├── getExampleTree.js ├── HtmlTree.js ├── App.js └── TextTree.js ├── getRoots.spec.js ├── getRoots.js ├── getParentChildrenMap.js ├── .eslintrc.yaml ├── Makefile ├── getChildParentMap.js ├── getDescendents.js ├── getRenderList.spec.js ├── getParentChildrenMap.spec.js ├── getChildParentMap.spec.js ├── getLeaves.spec.js ├── getSortedParentChildrenMap.spec.js ├── getLeaves.js ├── traverseDepthFirst.js ├── getDescendents.spec.js ├── getNode.spec.js ├── test.js ├── README.md ├── traverseDepthFirst.spec.js ├── getTree.spec.js ├── karma.conf.js ├── package.json ├── getNode.js ├── getRenderList.js ├── __snapshots__ └── getRenderList().md └── getTree.js /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | printWidth: 100 2 | semi: false 3 | singleQuote: true 4 | tabWidth: 4 5 | trailingComma: es5 6 | -------------------------------------------------------------------------------- /getSortedParentChildrenMap.js: -------------------------------------------------------------------------------- 1 | module.exports = function getSortedParentChildrenMap(parentChildrenMap, comparator) { 2 | return parentChildrenMap.map(children => children.toList().sort(comparator)) 3 | } 4 | -------------------------------------------------------------------------------- /example/entry.js: -------------------------------------------------------------------------------- 1 | const React = require('react') 2 | const { render } = require('react-dom') 3 | const App = require('./App') 4 | 5 | document.addEventListener('DOMContentLoaded', function() { 6 | render(, document.body.appendChild(document.createElement('div'))) 7 | }) 8 | -------------------------------------------------------------------------------- /getRoots.spec.js: -------------------------------------------------------------------------------- 1 | var { expect } = require('chai') 2 | const { Set } = require('immutable') 3 | const getRoots = require('./getRoots') 4 | 5 | describe('getRoots()', function() { 6 | it('should get the roots', function() { 7 | expect(getRoots(this.parentChildrenMap)).to.equal(new Set(['a0'])) 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /getRoots.js: -------------------------------------------------------------------------------- 1 | const isEmpty = require('lodash/isEmpty') 2 | const getChildParentMap = require('./getChildParentMap') 3 | 4 | module.exports = function getRoots(parentChildrenMap) { 5 | return getChildParentMap(parentChildrenMap) 6 | .filter(parentId => isEmpty(parentId)) 7 | .keySeq() 8 | .toSet() 9 | } 10 | -------------------------------------------------------------------------------- /getParentChildrenMap.js: -------------------------------------------------------------------------------- 1 | const { Map, Set } = require('immutable') 2 | 3 | module.exports = function getParentChildrenMap(childParentMap) { 4 | return childParentMap.reduce((acc, parentId, nodeId) => { 5 | if (!parentId) return acc 6 | return acc.update(parentId, (children = new Set()) => children.add(nodeId)) 7 | }, new Map()) 8 | } 9 | -------------------------------------------------------------------------------- /.eslintrc.yaml: -------------------------------------------------------------------------------- 1 | env: 2 | browser: true 3 | commonjs: true 4 | node: true 5 | extends: 6 | - prettier 7 | - eslint:recommended 8 | - plugin:react/recommended 9 | globals: 10 | beforeEach: true 11 | describe: true 12 | it: true 13 | parser: babel-eslint 14 | parserOptions: 15 | ecmaFeatures: 16 | jsx: true 17 | plugins: 18 | - react 19 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | style: 2 | node_modules/.bin/eslint '**/*.js' 3 | 4 | styled: 5 | node_modules/.bin/prettier '**/*.js' --write 6 | node_modules/.bin/eslint '**/*.js' --fix 7 | 8 | testd: 9 | node_modules/.bin/karma start karma.conf.js 10 | 11 | web: 12 | node_modules/.bin/webpack \ 13 | --config=./example/webpack.config.js 14 | 15 | webd: 16 | node_modules/.bin/webpack-dev-server \ 17 | --config=./example/webpack.config.js 18 | -------------------------------------------------------------------------------- /getChildParentMap.js: -------------------------------------------------------------------------------- 1 | const { Map } = require('immutable') 2 | 3 | module.exports = function getChildParentMap(parentChildrenMap) { 4 | return parentChildrenMap.reduce( 5 | (acc, children, parentId) => 6 | children.reduce( 7 | (_acc, childId) => _acc.set(childId, parentId), 8 | !acc.has(parentId) ? acc.set(parentId) : acc 9 | ), 10 | new Map() 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /getDescendents.js: -------------------------------------------------------------------------------- 1 | const { Set } = require('immutable') 2 | const traverseDepthFirst = require('./traverseDepthFirst') 3 | 4 | module.exports = function getDescendents({ depth, nodeId, parentChildrenMap }) { 5 | return new Set() 6 | .withMutations(mutable => 7 | traverseDepthFirst( 8 | { 9 | depth, 10 | nodeId, 11 | parentChildrenMap, 12 | }, 13 | nodeId => mutable.add(nodeId) 14 | ) 15 | ) 16 | .delete(nodeId) 17 | } 18 | -------------------------------------------------------------------------------- /getRenderList.spec.js: -------------------------------------------------------------------------------- 1 | var { expect } = require('chai') 2 | const getRenderList = require('./getRenderList') 3 | const getRoots = require('./getRoots') 4 | const getSortedParentChildrenMap = require('./getSortedParentChildrenMap') 5 | 6 | describe('getRenderList()', function() { 7 | it('should get the render list', function() { 8 | expect( 9 | getRenderList({ 10 | roots: getRoots(this.parentChildrenMap), 11 | sortedParentChildrenMap: getSortedParentChildrenMap(this.parentChildrenMap), 12 | }) 13 | ).to.matchSnapshot() 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /getParentChildrenMap.spec.js: -------------------------------------------------------------------------------- 1 | var { expect } = require('chai') 2 | const { Map, Set } = require('immutable') 3 | const getParentChildrenMap = require('./getParentChildrenMap') 4 | 5 | describe('getParentChildrenMap()', function() { 6 | it('should get the parent-children map', function() { 7 | expect(getParentChildrenMap(this.childParentMap)).to.equal( 8 | new Map({ 9 | a0: new Set(['b1', 'a1']), 10 | a1: new Set(['b2', 'a2']), 11 | b1: new Set(['d2', 'c2']), 12 | b2: new Set(['b3', 'a3']), 13 | }) 14 | ) 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /getChildParentMap.spec.js: -------------------------------------------------------------------------------- 1 | var { expect } = require('chai') 2 | const { Map } = require('immutable') 3 | const getChildParentMap = require('./getChildParentMap') 4 | 5 | describe('getChildParentMap()', function() { 6 | it('should get the child-parent map', function() { 7 | expect(getChildParentMap(this.parentChildrenMap)).to.equal( 8 | new Map({ 9 | a0: undefined, 10 | a1: 'a0', 11 | b1: 'a0', 12 | a2: 'a1', 13 | b2: 'a1', 14 | c2: 'b1', 15 | d2: 'b1', 16 | a3: 'b2', 17 | b3: 'b2', 18 | }) 19 | ) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /getLeaves.spec.js: -------------------------------------------------------------------------------- 1 | var { expect } = require('chai') 2 | const { Set } = require('immutable') 3 | const getLeaves = require('./getLeaves') 4 | 5 | describe('getLeaves()', function() { 6 | it('should get all leaves', function() { 7 | expect( 8 | getLeaves({ 9 | parentChildrenMap: this.parentChildrenMap, 10 | }) 11 | ).to.equal(new Set(['d2', 'c2', 'b3', 'a3', 'a2'])) 12 | }) 13 | 14 | it('should get leaves by nodeId', function() { 15 | expect( 16 | getLeaves({ 17 | nodeId: 'b1', 18 | parentChildrenMap: this.parentChildrenMap, 19 | }) 20 | ).to.equal(new Set(['d2', 'c2'])) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /getSortedParentChildrenMap.spec.js: -------------------------------------------------------------------------------- 1 | var { expect } = require('chai') 2 | const { Map, List } = require('immutable') 3 | const getSortedParentChildrenMap = require('./getSortedParentChildrenMap') 4 | 5 | describe('getSortedParentChildrenMap()', function() { 6 | it('should get the sorted parent-children map', function() { 7 | expect( 8 | getSortedParentChildrenMap(this.parentChildrenMap, (a, b) => a.localeCompare(b)) 9 | ).to.equal( 10 | new Map({ 11 | a0: new List(['a1', 'b1']), 12 | a1: new List(['a2', 'b2']), 13 | b1: new List(['c2', 'd2']), 14 | b2: new List(['a3', 'b3']), 15 | }) 16 | ) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /getLeaves.js: -------------------------------------------------------------------------------- 1 | const { Set } = require('immutable') 2 | const isEmpty = require('lodash/isEmpty') 3 | const getRoots = require('./getRoots') 4 | 5 | module.exports = function getLeaves({ parentChildrenMap, nodeId }) { 6 | const rootNodeIds = !isEmpty(nodeId) ? new Set([nodeId]) : getRoots(parentChildrenMap) 7 | return rootNodeIds.reduce( 8 | (acc, rootNodeId) => acc.concat(getLeavesR({ parentChildrenMap }, acc, rootNodeId)), 9 | new Set() 10 | ) 11 | } 12 | 13 | function getLeavesR({ parentChildrenMap }, acc, nodeId) { 14 | if (!parentChildrenMap.has(nodeId)) return acc.add(nodeId) 15 | return parentChildrenMap 16 | .get(nodeId) 17 | .reduce(getLeavesR.bind(undefined, { parentChildrenMap }), acc) 18 | } 19 | -------------------------------------------------------------------------------- /traverseDepthFirst.js: -------------------------------------------------------------------------------- 1 | const noop = require('lodash/noop') 2 | 3 | module.exports = function traverseDepthFirst( 4 | { depth = Number.MAX_SAFE_INTEGER, nodeId, parentChildrenMap }, 5 | iteratee = noop 6 | ) { 7 | return traverseDepthFirstR( 8 | { 9 | depth, 10 | iteratee, 11 | parentChildrenMap, 12 | }, 13 | nodeId 14 | ) 15 | } 16 | 17 | function traverseDepthFirstR({ depth, iteratee, parentChildrenMap }, nodeId) { 18 | iteratee(nodeId) 19 | if (depth === 0 || !parentChildrenMap.has(nodeId)) return 20 | return parentChildrenMap.get(nodeId).forEach( 21 | traverseDepthFirstR.bind(undefined, { 22 | depth: depth - 1, 23 | iteratee, 24 | parentChildrenMap, 25 | }) 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /getDescendents.spec.js: -------------------------------------------------------------------------------- 1 | var { expect } = require('chai') 2 | const { Set } = require('immutable') 3 | const getDescendents = require('./getDescendents') 4 | 5 | describe('getDescendents()', function() { 6 | it('should get the children', function() { 7 | expect( 8 | getDescendents({ 9 | nodeId: 'a0', 10 | parentChildrenMap: this.parentChildrenMap, 11 | }) 12 | ).to.equal(new Set(['a1', 'a2', 'b2', 'a3', 'b3', 'b1', 'c2', 'd2'])) 13 | }) 14 | 15 | it('should get the children to depth', function() { 16 | expect( 17 | getDescendents({ 18 | depth: 1, 19 | nodeId: 'a0', 20 | parentChildrenMap: this.parentChildrenMap, 21 | }) 22 | ).to.equal(new Set(['a1', 'b1'])) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /getNode.spec.js: -------------------------------------------------------------------------------- 1 | const { mount } = require('enzyme') 2 | const { List, Set } = require('immutable') 3 | const noop = require('lodash/noop') 4 | const React = require('react') 5 | const getNode = require('./getNode') 6 | 7 | describe('getNode()', function() { 8 | let options 9 | 10 | beforeEach(function() { 11 | options = { 12 | ancestors: new List([]), 13 | descendents: new Set([]), 14 | descenders: new List([]), 15 | disabledDescendents: new Set([]), 16 | hiddenDescendents: new Set([]), 17 | isDisabled: false, 18 | isSelected: false, 19 | isUnselectable: false, 20 | onEvent: noop, 21 | selectedDescendents: new Set([]), 22 | style: {}, 23 | } 24 | }) 25 | 26 | it('should get the node', function() { 27 | const Node = getNode(() =>
) 28 | mount() 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai') 2 | const chaiImmutable = require('chai-immutable') 3 | const { matchSnapshot } = require('chai-karma-snapshot') 4 | const enzyme = require('enzyme') 5 | const Adapter = require('enzyme-adapter-react-16') 6 | const { Map } = require('immutable') 7 | const getParentChildrenMap = require('./getParentChildrenMap') 8 | const testsContext = require.context('.', false, /\.spec\.js$/) 9 | testsContext.keys().forEach(testsContext) 10 | 11 | enzyme.configure({ adapter: new Adapter() }) 12 | chai.use(chaiImmutable) 13 | chai.use(matchSnapshot) 14 | chai.config.truncateThreshold = 0 15 | 16 | beforeEach(function() { 17 | // a0 18 | // ├── a1 19 | // │ ├── a2 20 | // │ └── b2 21 | // │ ├── a3 22 | // │ └── b3 23 | // └── b1 24 | // ├── c2 25 | // └── d2 26 | this.childParentMap = new Map({ 27 | a0: undefined, 28 | a1: 'a0', 29 | b1: 'a0', 30 | a2: 'a1', 31 | b2: 'a1', 32 | c2: 'b1', 33 | d2: 'b1', 34 | a3: 'b2', 35 | b3: 'b2', 36 | }) 37 | this.parentChildrenMap = getParentChildrenMap(this.childParentMap) 38 | }) 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-tree-list 2 | 3 | A higher-order React Component for developing tree list UIs. The API is composable and extensible. 4 | Renders are performant up to 1000s of nodes. 5 | 6 | ## Demo 7 | 8 | :point_right: [christophercliff.com/react-tree-list/](https://christophercliff.com/react-tree-list/) 9 | 10 | ## Usage 11 | 12 | ```jsx 13 | const { Map } = require('immutable') 14 | const getTree = require('react-tree-list/getTree') 15 | const getNode = require('react-tree-list/getNode') 16 | 17 | const Tree = getTree(getNode(({ nodeId, onEvent }) =>
{nodeId}
)) 18 | class MyTree extends React.Component { 19 | render() { 20 | return ( 21 | console.log(`clicked: ${nodeId}`)} 30 | /> 31 | ) 32 | } 33 | } 34 | ``` 35 | 36 | ## Example 37 | 38 | See https://github.com/christophercliff/react-tree-list/tree/master/example. Run locally with `make 39 | webd`. 40 | -------------------------------------------------------------------------------- /traverseDepthFirst.spec.js: -------------------------------------------------------------------------------- 1 | var { expect } = require('chai') 2 | const { List } = require('immutable') 3 | const traverseDepthFirst = require('./traverseDepthFirst') 4 | 5 | describe('traverseDepthFirst()', function() { 6 | it('should traverse depth first', function() { 7 | expect( 8 | new List().withMutations(mutable => 9 | traverseDepthFirst( 10 | { 11 | nodeId: 'a0', 12 | parentChildrenMap: this.parentChildrenMap, 13 | }, 14 | nodeId => mutable.push(nodeId) 15 | ) 16 | ) 17 | ).to.equal(new List(['a0', 'b1', 'd2', 'c2', 'a1', 'b2', 'b3', 'a3', 'a2'])) 18 | }) 19 | 20 | it('should traverse depth first to depth', function() { 21 | expect( 22 | new List().withMutations(mutable => 23 | traverseDepthFirst( 24 | { 25 | depth: 2, 26 | nodeId: 'a0', 27 | parentChildrenMap: this.parentChildrenMap, 28 | }, 29 | nodeId => mutable.push(nodeId) 30 | ) 31 | ) 32 | ).to.equal(new List(['a0', 'b1', 'd2', 'c2', 'a1', 'b2', 'a2'])) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /getTree.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai') 2 | const { mount } = require('enzyme') 3 | const { Map, Set } = require('immutable') 4 | const React = require('react') 5 | const getNode = require('./getNode') 6 | const getTree = require('./getTree') 7 | 8 | describe('getTree()', function() { 9 | const Node = getNode(() =>
) 10 | let options 11 | 12 | beforeEach(function() { 13 | options = { 14 | childParentMap: this.childParentMap, 15 | disabledNodes: new Set(), 16 | hiddenNodes: new Set(), 17 | nodePropsMap: new Map({ 18 | a0: new Map({ name: 'A0' }), 19 | a1: new Map({ name: 'A1' }), 20 | b1: new Map({ name: 'B1' }), 21 | a2: new Map({ name: 'A2' }), 22 | b2: new Map({ name: 'B2' }), 23 | c2: new Map({ name: 'C2' }), 24 | d2: new Map({ name: 'D2' }), 25 | a3: new Map({ name: 'A3' }), 26 | b3: new Map({ name: 'B3' }), 27 | }), 28 | selectedNodes: new Set(), 29 | unselectableNodes: new Set(), 30 | } 31 | }) 32 | 33 | it('should pass on the node props', function() { 34 | const Tree = getTree(Node) 35 | const wrapper = mount() 36 | const firstNodeWrapper = wrapper.find(Node).first() 37 | expect(firstNodeWrapper.prop('name')).to.equal('A0') 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | const entry = 'test.js' 2 | const snapshots = '**/__snapshots__/**/*.md' 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | browsers: ['Chrome'], 7 | client: { 8 | mocha: { reporter: 'html' }, 9 | }, 10 | colors: true, 11 | files: [entry, snapshots], 12 | frameworks: ['mocha', 'snapshot', 'mocha-snapshot'], 13 | logLevel: config.LOG_INFO, 14 | port: 9876, 15 | preprocessors: { 16 | [snapshots]: ['snapshot'], 17 | [entry]: ['webpack', 'sourcemap'], 18 | }, 19 | reporters: ['progress'], 20 | singleRun: false, 21 | snapshot: { update: false }, 22 | webpack: { 23 | devtool: 'inline-source-map', 24 | module: { 25 | rules: [ 26 | { 27 | exclude: /node_modules/, 28 | test: /\.js$/, 29 | use: { 30 | loader: 'babel-loader', 31 | options: { 32 | presets: ['es2015'], 33 | plugins: [ 34 | 'transform-class-properties', 35 | 'transform-object-rest-spread', 36 | 'transform-react-jsx', 37 | ], 38 | }, 39 | }, 40 | }, 41 | ], 42 | }, 43 | }, 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-tree-list", 3 | "version": "1.0.0", 4 | "dependencies": { 5 | "immutable": "^3.8.2", 6 | "lodash": "^4.17.4", 7 | "prop-types": "^15.6.0", 8 | "react": "^16.0.0", 9 | "react-immutable-proptypes": "^2.1.0", 10 | "react-virtualized": "^9.12.0" 11 | }, 12 | "devDependencies": { 13 | "babel-core": "^6.26.0", 14 | "babel-eslint": "^8.0.2", 15 | "babel-loader": "^7.1.2", 16 | "babel-plugin-transform-class-properties": "^6.24.1", 17 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 18 | "babel-plugin-transform-react-jsx": "^6.24.1", 19 | "babel-preset-es2015": "^6.24.1", 20 | "chai": "^4.1.2", 21 | "chai-immutable": "^1.6.0", 22 | "chai-karma-snapshot": "^0.6.0", 23 | "clean-webpack-plugin": "^0.1.17", 24 | "enzyme": "^3.2.0", 25 | "enzyme-adapter-react-16": "^1.1.0", 26 | "eslint": "^4.11.0", 27 | "eslint-config-prettier": "^2.8.0", 28 | "eslint-plugin-prettier": "^2.3.1", 29 | "eslint-plugin-react": "^7.5.1", 30 | "google-fonts-webpack-plugin": "^0.4.3", 31 | "html-webpack-plugin": "^2.30.1", 32 | "karma": "^1.7.1", 33 | "karma-chrome-launcher": "^2.2.0", 34 | "karma-mocha": "^1.3.0", 35 | "karma-mocha-snapshot": "^0.2.1", 36 | "karma-snapshot": "^0.5.1", 37 | "karma-sourcemap-loader": "^0.3.7", 38 | "karma-webpack": "^2.0.5", 39 | "mocha": "^4.0.1", 40 | "prettier": "^1.8.2", 41 | "react-dom": "^16.0.0", 42 | "webpack": "^3.8.1", 43 | "webpack-dev-server": "2.7.1" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | const CleanWebpackPlugin = require('clean-webpack-plugin') 2 | const GoogleFontsWebpackPlugin = require('google-fonts-webpack-plugin') 3 | const HtmlWebpackPlugin = require('html-webpack-plugin') 4 | const { resolve } = require('path') 5 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin') 6 | const { DefinePlugin } = require('webpack') 7 | 8 | const outputDir = 'dist' 9 | 10 | module.exports = { 11 | context: __dirname, 12 | devServer: { contentBase: outputDir }, 13 | devtool: 'inline-source-map', 14 | entry: './entry.js', 15 | module: { 16 | rules: [ 17 | { 18 | exclude: /node_modules/, 19 | test: /\.js$/, 20 | use: { 21 | loader: 'babel-loader', 22 | options: { 23 | presets: ['es2015'], 24 | plugins: [ 25 | 'transform-class-properties', 26 | 'transform-object-rest-spread', 27 | 'transform-react-jsx', 28 | ], 29 | }, 30 | }, 31 | }, 32 | ], 33 | }, 34 | output: { 35 | filename: 'app-[hash].js', 36 | path: resolve(__dirname, outputDir), 37 | }, 38 | plugins: [ 39 | new DefinePlugin({ 40 | 'process.env': { 41 | NODE_ENV: JSON.stringify('production'), 42 | }, 43 | }), 44 | // new UglifyJsPlugin(), 45 | new CleanWebpackPlugin([outputDir]), 46 | new HtmlWebpackPlugin({ 47 | title: 'react-tree-list', 48 | }), 49 | new GoogleFontsWebpackPlugin({ 50 | fonts: [ 51 | { 52 | family: 'Roboto Mono', 53 | variants: ['500'], 54 | }, 55 | { 56 | family: 'Source Sans Pro', 57 | variants: ['600'], 58 | }, 59 | ], 60 | formats: ['woff'], 61 | }), 62 | ], 63 | } 64 | -------------------------------------------------------------------------------- /getNode.js: -------------------------------------------------------------------------------- 1 | const isEqual = require('lodash/isEqual') 2 | const PropTypes = require('prop-types') 3 | const React = require('react') 4 | const ImmutablePropTypes = require('react-immutable-proptypes') 5 | 6 | module.exports = function getNode(WrappedNode) { 7 | return class Node extends React.Component { 8 | static displayName = 'Node' 9 | static propTypes = { 10 | ancestors: ImmutablePropTypes.list.isRequired, 11 | descendents: ImmutablePropTypes.set.isRequired, 12 | descenders: ImmutablePropTypes.list.isRequired, 13 | disabledDescendents: ImmutablePropTypes.set.isRequired, 14 | hiddenDescendents: ImmutablePropTypes.set.isRequired, 15 | isDisabled: PropTypes.bool.isRequired, 16 | isSelected: PropTypes.bool.isRequired, 17 | isUnselectable: PropTypes.bool.isRequired, 18 | onEvent: PropTypes.func.isRequired, 19 | selectedDescendents: ImmutablePropTypes.set.isRequired, 20 | style: PropTypes.object, 21 | } 22 | 23 | onEvent = options => { 24 | this.props.onEvent({ 25 | ...this.props, 26 | ...options, 27 | }) 28 | } 29 | 30 | shouldComponentUpdate(nextProps) { 31 | return ( 32 | nextProps.isDisabled !== this.props.isDisabled || 33 | nextProps.isSelected !== this.props.isSelected || 34 | nextProps.isUnselectable !== this.props.isUnselectable || 35 | !nextProps.ancestors.equals(this.props.ancestors) || 36 | !nextProps.descendents.equals(this.props.descendents) || 37 | !nextProps.descenders.equals(this.props.descenders) || 38 | !nextProps.disabledDescendents.equals(this.props.disabledDescendents) || 39 | !nextProps.hiddenDescendents.equals(this.props.hiddenDescendents) || 40 | !nextProps.selectedDescendents.equals(this.props.selectedDescendents) || 41 | !isEqual(nextProps.style, this.props.style) 42 | ) 43 | } 44 | 45 | render() { 46 | return 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /example/getExampleTree.js: -------------------------------------------------------------------------------- 1 | const { Set } = require('immutable') 2 | const get = require('lodash/get') 3 | const noop = require('lodash/noop') 4 | const PropTypes = require('prop-types') 5 | const React = require('react') 6 | const ImmutablePropTypes = require('react-immutable-proptypes') 7 | const getDescendents = require('../getDescendents') 8 | const getTree = require('../getTree') 9 | 10 | module.exports = function getExampleTree(Node) { 11 | const Tree = getTree(Node) 12 | return class ExampleTree extends React.PureComponent { 13 | static defaultProps = { 14 | hiddenNodes: new Set(), 15 | selectedNodes: new Set(), 16 | onChange: noop, 17 | } 18 | static displayName = 'ExampleTree' 19 | static propTypes = { 20 | childParentMap: ImmutablePropTypes.map.isRequired, 21 | hiddenNodes: ImmutablePropTypes.set, 22 | selectedNodes: ImmutablePropTypes.set, 23 | onChange: PropTypes.func, 24 | } 25 | 26 | onEvent = ({ type, ...options }) => { 27 | get(this, type)(options) 28 | } 29 | 30 | selectNode = ({ ancestors, descendents, nodeId, parentChildrenMap }) => { 31 | this.props.onChange({ 32 | selectedNodes: this.props.selectedNodes.withMutations(mutable => { 33 | mutable.add(nodeId).union(descendents) 34 | ancestors.forEach( 35 | nodeId => 36 | parentChildrenMap.get(nodeId).every(_nodeId => mutable.has(_nodeId)) && 37 | mutable.add(nodeId) 38 | ) 39 | }), 40 | }) 41 | } 42 | 43 | deselectNode = ({ ancestors, descendents, nodeId, parentChildrenMap }) => { 44 | this.props.onChange({ 45 | selectedNodes: this.props.selectedNodes.withMutations(mutable => { 46 | mutable.subtract(descendents).delete(nodeId) 47 | ancestors.forEach( 48 | nodeId => 49 | !parentChildrenMap.get(nodeId).every(_nodeId => mutable.has(_nodeId)) && 50 | mutable.delete(nodeId) 51 | ) 52 | }), 53 | }) 54 | } 55 | 56 | hideDescendents = ({ descendents }) => { 57 | this.props.onChange({ hiddenNodes: this.props.hiddenNodes.union(descendents) }) 58 | } 59 | 60 | showDescendents = ({ nodeId, parentChildrenMap }) => { 61 | this.props.onChange({ 62 | hiddenNodes: this.props.hiddenNodes.subtract( 63 | getDescendents({ depth: 1, nodeId, parentChildrenMap }) 64 | ), 65 | }) 66 | } 67 | 68 | render() { 69 | return 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /getRenderList.js: -------------------------------------------------------------------------------- 1 | const { fromJS, List, OrderedMap, Set } = require('immutable') 2 | 3 | module.exports = function getRenderList({ 4 | disabledNodes = new Set(), 5 | hiddenNodes = new Set(), 6 | roots, 7 | selectedNodes = new Set(), 8 | sortedParentChildrenMap, 9 | }) { 10 | return roots.reduce((acc, rootNodeId) => { 11 | return acc.concat( 12 | getRenderListR( 13 | { 14 | disabledNodes, 15 | hiddenNodes, 16 | selectedNodes, 17 | sortedParentChildrenMap, 18 | }, 19 | new OrderedMap(), 20 | rootNodeId, 21 | 0, 22 | new List([rootNodeId]) 23 | ).filter((node, nodeId) => !hiddenNodes.has(nodeId)) 24 | ) 25 | }, new List()) 26 | } 27 | 28 | function getRenderListR( 29 | { 30 | disabledNodes, 31 | hiddenNodes, 32 | parentId, 33 | parentIsLasts = new List(), 34 | selectedNodes, 35 | sortedParentChildrenMap, 36 | }, 37 | acc, 38 | nodeId, 39 | i, 40 | list 41 | ) { 42 | const isLastChild = i === list.size - 1 43 | parentIsLasts = parentIsLasts.push(isLastChild) 44 | acc = acc.withMutations(mutable => { 45 | mutable.set( 46 | nodeId, 47 | fromJS({ 48 | ancestors: parentId 49 | ? mutable.getIn([parentId, 'ancestors']).unshift(parentId) 50 | : new List(), 51 | descendents: new Set(), 52 | descenders: parentIsLasts.shift().map((parentIsLast, _i, _list) => { 53 | return _i === _list.size - 1 ? true : !parentIsLast 54 | }), 55 | disabledDescendents: new Set(), 56 | hiddenDescendents: new Set(), 57 | isLastChild, 58 | nodeId, 59 | selectedDescendents: new Set(), 60 | }) 61 | ) 62 | if (parentId) { 63 | mutable.updateIn([parentId, 'descendents'], set => set.add(nodeId)) 64 | if (disabledNodes.has(nodeId)) 65 | mutable.updateIn([parentId, 'disabledDescendents'], set => set.add(nodeId)) 66 | if (hiddenNodes.has(nodeId)) 67 | mutable.updateIn([parentId, 'hiddenDescendents'], set => set.add(nodeId)) 68 | if (selectedNodes.has(nodeId)) 69 | mutable.updateIn([parentId, 'selectedDescendents'], set => set.add(nodeId)) 70 | } 71 | }) 72 | if (!sortedParentChildrenMap.get(nodeId)) return acc 73 | acc = sortedParentChildrenMap.get(nodeId).reduce( 74 | getRenderListR.bind(undefined, { 75 | disabledNodes, 76 | hiddenNodes, 77 | parentId: nodeId, 78 | parentIsLasts, 79 | selectedNodes, 80 | sortedParentChildrenMap, 81 | }), 82 | acc 83 | ) 84 | return acc.withMutations(mutable => { 85 | if (parentId) { 86 | mutable.updateIn([parentId, 'descendents'], set => 87 | set.union(acc.getIn([nodeId, 'descendents'])) 88 | ) 89 | mutable.updateIn([parentId, 'disabledDescendents'], set => 90 | set.union(acc.getIn([nodeId, 'disabledDescendents'])) 91 | ) 92 | mutable.updateIn([parentId, 'hiddenDescendents'], set => 93 | set.union(acc.getIn([nodeId, 'hiddenDescendents'])) 94 | ) 95 | mutable.updateIn([parentId, 'selectedDescendents'], set => 96 | set.union(acc.getIn([nodeId, 'selectedDescendents'])) 97 | ) 98 | } 99 | }) 100 | } 101 | -------------------------------------------------------------------------------- /example/HtmlTree.js: -------------------------------------------------------------------------------- 1 | const extend = require('lodash/extend') 2 | const omit = require('lodash/omit') 3 | const PropTypes = require('prop-types') 4 | const React = require('react') 5 | const getExampleTree = require('./getExampleTree') 6 | const getNode = require('../getNode') 7 | 8 | class Checkbox extends React.Component { 9 | static propTypes = { 10 | indeterminate: PropTypes.bool, 11 | } 12 | 13 | componentDidMount() { 14 | this.el.indeterminate = this.props.indeterminate 15 | } 16 | 17 | componentDidUpdate(prevProps) { 18 | if (prevProps.indeterminate !== this.props.indeterminate) { 19 | this.el.indeterminate = this.props.indeterminate 20 | } 21 | } 22 | 23 | render() { 24 | return ( 25 | (this.el = el)} 28 | type="checkbox" 29 | /> 30 | ) 31 | } 32 | } 33 | 34 | const Tree = getExampleTree( 35 | getNode( 36 | ({ 37 | ancestors, 38 | descendents, 39 | hiddenDescendents, 40 | isSelected, 41 | nodeId, 42 | onEvent, 43 | selectedDescendents, 44 | style, 45 | }) => { 46 | const hasVisibleDescendents = !hiddenDescendents.equals(descendents) 47 | return ( 48 |
57 | 72 | 76 | onEvent({ 77 | type: isSelected ? 'deselectNode' : 'selectNode', 78 | }) 79 | } 80 | style={{ 81 | display: 'inline-block', 82 | fontSize: 'inherit', 83 | marginLeft: '0.50em', 84 | }} 85 | /> 86 | 92 | {nodeId} 93 | 94 |
95 | ) 96 | } 97 | ) 98 | ) 99 | 100 | module.exports = class MyTree extends React.PureComponent { 101 | static displayName = 'HtmlTree' 102 | 103 | render() { 104 | return ( 105 |
122 | 123 |
124 | ) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /__snapshots__/getRenderList().md: -------------------------------------------------------------------------------- 1 | # `getRenderList()` 2 | 3 | #### `should get the render list` 4 | 5 | ``` 6 | Immutable.List [ 7 | Immutable.Map { 8 | ancestors: Immutable.List [ 9 | ], 10 | descendents: Immutable.Set [ 11 | "a1", 12 | "a2", 13 | "b2", 14 | "a3", 15 | "b3", 16 | "b1", 17 | "c2", 18 | "d2", 19 | ], 20 | descenders: Immutable.List [ 21 | ], 22 | disabledDescendents: Immutable.Set [ 23 | ], 24 | hiddenDescendents: Immutable.Set [ 25 | ], 26 | isLastChild: true, 27 | nodeId: "a0", 28 | selectedDescendents: Immutable.Set [ 29 | ], 30 | }, 31 | Immutable.Map { 32 | ancestors: Immutable.List [ 33 | "a0", 34 | ], 35 | descendents: Immutable.Set [ 36 | "a2", 37 | "b2", 38 | "a3", 39 | "b3", 40 | ], 41 | descenders: Immutable.List [ 42 | true, 43 | ], 44 | disabledDescendents: Immutable.Set [ 45 | ], 46 | hiddenDescendents: Immutable.Set [ 47 | ], 48 | isLastChild: false, 49 | nodeId: "a1", 50 | selectedDescendents: Immutable.Set [ 51 | ], 52 | }, 53 | Immutable.Map { 54 | ancestors: Immutable.List [ 55 | "a1", 56 | "a0", 57 | ], 58 | descendents: Immutable.Set [ 59 | ], 60 | descenders: Immutable.List [ 61 | true, 62 | true, 63 | ], 64 | disabledDescendents: Immutable.Set [ 65 | ], 66 | hiddenDescendents: Immutable.Set [ 67 | ], 68 | isLastChild: false, 69 | nodeId: "a2", 70 | selectedDescendents: Immutable.Set [ 71 | ], 72 | }, 73 | Immutable.Map { 74 | ancestors: Immutable.List [ 75 | "a1", 76 | "a0", 77 | ], 78 | descendents: Immutable.Set [ 79 | "a3", 80 | "b3", 81 | ], 82 | descenders: Immutable.List [ 83 | true, 84 | true, 85 | ], 86 | disabledDescendents: Immutable.Set [ 87 | ], 88 | hiddenDescendents: Immutable.Set [ 89 | ], 90 | isLastChild: true, 91 | nodeId: "b2", 92 | selectedDescendents: Immutable.Set [ 93 | ], 94 | }, 95 | Immutable.Map { 96 | ancestors: Immutable.List [ 97 | "b2", 98 | "a1", 99 | "a0", 100 | ], 101 | descendents: Immutable.Set [ 102 | ], 103 | descenders: Immutable.List [ 104 | true, 105 | false, 106 | true, 107 | ], 108 | disabledDescendents: Immutable.Set [ 109 | ], 110 | hiddenDescendents: Immutable.Set [ 111 | ], 112 | isLastChild: false, 113 | nodeId: "a3", 114 | selectedDescendents: Immutable.Set [ 115 | ], 116 | }, 117 | Immutable.Map { 118 | ancestors: Immutable.List [ 119 | "b2", 120 | "a1", 121 | "a0", 122 | ], 123 | descendents: Immutable.Set [ 124 | ], 125 | descenders: Immutable.List [ 126 | true, 127 | false, 128 | true, 129 | ], 130 | disabledDescendents: Immutable.Set [ 131 | ], 132 | hiddenDescendents: Immutable.Set [ 133 | ], 134 | isLastChild: true, 135 | nodeId: "b3", 136 | selectedDescendents: Immutable.Set [ 137 | ], 138 | }, 139 | Immutable.Map { 140 | ancestors: Immutable.List [ 141 | "a0", 142 | ], 143 | descendents: Immutable.Set [ 144 | "c2", 145 | "d2", 146 | ], 147 | descenders: Immutable.List [ 148 | true, 149 | ], 150 | disabledDescendents: Immutable.Set [ 151 | ], 152 | hiddenDescendents: Immutable.Set [ 153 | ], 154 | isLastChild: true, 155 | nodeId: "b1", 156 | selectedDescendents: Immutable.Set [ 157 | ], 158 | }, 159 | Immutable.Map { 160 | ancestors: Immutable.List [ 161 | "b1", 162 | "a0", 163 | ], 164 | descendents: Immutable.Set [ 165 | ], 166 | descenders: Immutable.List [ 167 | false, 168 | true, 169 | ], 170 | disabledDescendents: Immutable.Set [ 171 | ], 172 | hiddenDescendents: Immutable.Set [ 173 | ], 174 | isLastChild: false, 175 | nodeId: "c2", 176 | selectedDescendents: Immutable.Set [ 177 | ], 178 | }, 179 | Immutable.Map { 180 | ancestors: Immutable.List [ 181 | "b1", 182 | "a0", 183 | ], 184 | descendents: Immutable.Set [ 185 | ], 186 | descenders: Immutable.List [ 187 | false, 188 | true, 189 | ], 190 | disabledDescendents: Immutable.Set [ 191 | ], 192 | hiddenDescendents: Immutable.Set [ 193 | ], 194 | isLastChild: true, 195 | nodeId: "d2", 196 | selectedDescendents: Immutable.Set [ 197 | ], 198 | }, 199 | ] 200 | ``` 201 | 202 | -------------------------------------------------------------------------------- /example/App.js: -------------------------------------------------------------------------------- 1 | const { Map, Set } = require('immutable') 2 | const get = require('lodash/get') 3 | const isEmpty = require('lodash/isEmpty') 4 | const keyBy = require('lodash/keyBy') 5 | const map = require('lodash/map') 6 | const random = require('lodash/random') 7 | const range = require('lodash/range') 8 | const shuffle = require('lodash/shuffle') 9 | const React = require('react') 10 | const HtmlTree = require('./HtmlTree') 11 | const TextTree = require('./TextTree') 12 | 13 | const nodeCounts = [50, 100, 500, 1000, 5000] 14 | const modes = keyBy(['text', 'html']) 15 | 16 | module.exports = class App extends React.PureComponent { 17 | static displayName = 'App' 18 | constructor(props) { 19 | super(props) 20 | const nodeCount = get(nodeCounts, 3) 21 | this.state = { 22 | childParentMap: getRandomChildParentMap(nodeCount), 23 | hiddenNodes: new Set(), 24 | mode: modes.text, 25 | nodeCount, 26 | selectedNodes: new Set(), 27 | } 28 | } 29 | 30 | onChangeMode = mode => { 31 | this.setState({ mode }) 32 | } 33 | 34 | onChangeNodeCount = nodeCount => { 35 | this.setState({ 36 | childParentMap: getRandomChildParentMap(nodeCount), 37 | hiddenNodes: new Set(), 38 | nodeCount, 39 | selectedNodes: new Set(), 40 | }) 41 | } 42 | 43 | onChangeTree = ({ hiddenNodes, selectedNodes }) => { 44 | this.setState({ 45 | hiddenNodes: hiddenNodes ? hiddenNodes : this.state.hiddenNodes, 46 | selectedNodes: selectedNodes ? selectedNodes : this.state.selectedNodes, 47 | }) 48 | } 49 | 50 | render() { 51 | return ( 52 |
53 | {this.state.mode === modes.text ? ( 54 | 60 | ) : ( 61 | 67 | )} 68 |
78 | {map(modes, mode => ( 79 | 87 | ))} 88 | {' '} 89 | {map(nodeCounts, nodeCount => ( 90 | 98 | ))}{' '} 99 | 103 | GitHub 104 | 105 |
106 |
107 | ) 108 | } 109 | } 110 | 111 | function getRandomChildParentMap(n) { 112 | const dst = shuffle(range(n)) 113 | const src = [] 114 | let childParentMap = new Map() 115 | let a 116 | let b 117 | src.push(dst.pop()) 118 | childParentMap = childParentMap.set(src[0].toString()) 119 | while (!isEmpty(dst)) { 120 | a = src[random(src.length - 1)] 121 | b = dst.pop() 122 | childParentMap = childParentMap.set(b.toString(), a.toString()) 123 | src.push(b) 124 | } 125 | return childParentMap 126 | } 127 | -------------------------------------------------------------------------------- /example/TextTree.js: -------------------------------------------------------------------------------- 1 | const React = require('react') 2 | const getNode = require('../getNode') 3 | const getExampleTree = require('./getExampleTree') 4 | 5 | const colorPurple = '#c5a5c5' 6 | const colorBlue = '#79b6f2' 7 | const colorGreen = '#5fb3b3' 8 | const colorWhite = '#fff' 9 | const a = '├── ' 10 | const b = '└── ' 11 | const c = '│ ' 12 | const d = ' ' 13 | const Tree = getExampleTree( 14 | getNode( 15 | ({ 16 | descendents, 17 | descenders, 18 | hiddenDescendents, 19 | isLastChild, 20 | isSelected, 21 | nodeId, 22 | onEvent, 23 | selectedDescendents, 24 | style, 25 | }) => { 26 | const hasVisibleDescendents = !hiddenDescendents.equals(descendents) 27 | return ( 28 |
29 | 30 | {descenders 31 | .map((hasDescender, i, list) => ( 32 | 33 | {(() => { 34 | if (hasDescender) { 35 | if (i === list.size - 1) { 36 | if (isLastChild) return b 37 | return a 38 | } 39 | return c 40 | } 41 | return d 42 | })()} 43 | 44 | )) 45 | .valueSeq()} 46 | 47 | 53 | {!descendents.isEmpty() ? ( 54 | 55 | 57 | onEvent({ 58 | type: hasVisibleDescendents 59 | ? 'hideDescendents' 60 | : 'showDescendents', 61 | }) 62 | } 63 | style={{ 64 | color: colorBlue, 65 | cursor: 'pointer', 66 | }} 67 | > 68 | {hasVisibleDescendents ? '-' : '+'} 69 | {' '} 70 | 71 | ) : ( 72 | {'- '} 73 | )} 74 | 76 | onEvent({ 77 | type: isSelected ? 'deselectNode' : 'selectNode', 78 | }) 79 | } 80 | style={{ 81 | color: colorGreen, 82 | cursor: 'pointer', 83 | }} 84 | > 85 | [{(() => { 86 | if (isSelected) return 'x' 87 | if (!selectedDescendents.isEmpty()) return '-' 88 | return ' ' 89 | })()}]{' '} 90 | 91 | {nodeId} 92 | 93 |
94 | ) 95 | } 96 | ) 97 | ) 98 | 99 | module.exports = class MyTree extends React.PureComponent { 100 | static displayName = 'TextTree' 101 | 102 | render() { 103 | return ( 104 |
120 |                 
121 |             
122 | ) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /getTree.js: -------------------------------------------------------------------------------- 1 | const { Map, Set } = require('immutable') 2 | const noop = require('lodash/noop') 3 | const PropTypes = require('prop-types') 4 | const React = require('react') 5 | const ImmutablePropTypes = require('react-immutable-proptypes') 6 | const { AutoSizer, CellMeasurer, CellMeasurerCache, List } = require('react-virtualized') 7 | const getParentChildrenMap = require('./getParentChildrenMap') 8 | const getRenderList = require('./getRenderList') 9 | const getRoots = require('./getRoots') 10 | const getSortedParentChildrenMap = require('./getSortedParentChildrenMap') 11 | 12 | module.exports = function getTree(Node) { 13 | return class Tree extends React.PureComponent { 14 | static defaultProps = { 15 | disabledNodes: new Set(), 16 | hiddenNodes: new Set(), 17 | nodePropsMap: new Map(), 18 | onEvent: noop, 19 | selectedNodes: new Set(), 20 | unselectableNodes: new Set(), 21 | } 22 | static displayName = 'Tree' 23 | static propTypes = { 24 | childParentMap: ImmutablePropTypes.map.isRequired, 25 | comparator: PropTypes.func, 26 | disabledNodes: ImmutablePropTypes.set, 27 | hiddenNodes: ImmutablePropTypes.set, 28 | nodePropsMap: ImmutablePropTypes.map, 29 | onEvent: PropTypes.func, 30 | selectedNodes: ImmutablePropTypes.set, 31 | unselectableNodes: ImmutablePropTypes.set, 32 | } 33 | 34 | constructor(props) { 35 | super(props) 36 | const parentChildrenMap = getParentChildrenMap(props.childParentMap) 37 | this.state = { 38 | parentChildrenMap: parentChildrenMap, 39 | roots: getRoots(parentChildrenMap), 40 | sortedParentChildrenMap: getSortedParentChildrenMap( 41 | parentChildrenMap, 42 | props.comparator 43 | ), 44 | } 45 | this.cellMeasurerCache = new CellMeasurerCache({ fixedWidth: true }) 46 | } 47 | 48 | componentWillReceiveProps(nextProps) { 49 | if (nextProps.childParentMap !== this.props.childParentMap) { 50 | const parentChildrenMap = getParentChildrenMap(nextProps.childParentMap) 51 | this.setState({ 52 | parentChildrenMap: parentChildrenMap, 53 | roots: getRoots(parentChildrenMap), 54 | sortedParentChildrenMap: getSortedParentChildrenMap( 55 | parentChildrenMap, 56 | nextProps.comparator 57 | ), 58 | }) 59 | } 60 | } 61 | 62 | onEvent = options => { 63 | this.props.onEvent({ 64 | parentChildrenMap: this.state.parentChildrenMap, 65 | ...options, 66 | }) 67 | } 68 | 69 | render() { 70 | const { disabledNodes, hiddenNodes, selectedNodes } = this.props 71 | const { roots, sortedParentChildrenMap } = this.state 72 | const renderList = getRenderList({ 73 | disabledNodes, 74 | hiddenNodes, 75 | roots, 76 | selectedNodes, 77 | sortedParentChildrenMap, 78 | }) 79 | if (renderList.size > 100) 80 | return ( 81 | 82 | {({ height, width }) => ( 83 | 91 | )} 92 | 93 | ) 94 | return ( 95 |
96 | {renderList.map(this.renderStaticRow.bind(this, { renderList })).valueSeq()} 97 |
98 | ) 99 | } 100 | 101 | renderVirtualRow({ renderList }, { index, key, parent, style }) { 102 | return ( 103 | 109 | {this.renderRow({ renderList }, { index, style })} 110 | 111 | ) 112 | } 113 | 114 | renderStaticRow({ renderList }, renderNode, index) { 115 | return this.renderRow({ renderList }, { index }) 116 | } 117 | 118 | renderRow({ renderList }, { index, style }) { 119 | const renderNode = renderList.get(index) 120 | const nodeId = renderNode.get('nodeId') 121 | const { disabledNodes, selectedNodes, unselectableNodes } = this.props 122 | return ( 123 | 140 | ) 141 | } 142 | } 143 | } 144 | --------------------------------------------------------------------------------