├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── __mocks__ └── react-virtualized.js ├── config ├── pages.js └── test │ ├── cssTransform.js │ ├── fileTransform.js │ └── test.js ├── demo └── src │ ├── Home.js │ ├── NavBar.css │ ├── NavBar.js │ ├── containers │ ├── DocumentsContainer.js │ ├── ExamplesContainer.css │ └── ExamplesContainer.js │ ├── docs │ ├── Doc.js │ ├── extensions.md │ ├── filtering.md │ ├── index.js │ └── renderers.md │ ├── examples │ ├── Basic │ │ ├── DraggableRenderer.js │ │ ├── ItemTypes.js │ │ ├── RendererDragContainer.js │ │ └── index.js │ ├── ChangeRenderers.js │ ├── Extensions.js │ ├── Filterable.js │ ├── KeyboardNavigation.js │ ├── LargeCollection.js │ ├── NodeMeasure.js │ ├── Renderers.js │ ├── WorldCup.js │ └── index.js │ ├── index.css │ ├── index.html │ ├── index.js │ └── toolbelt.js ├── index.d.ts ├── nwb.config.js ├── package.json ├── src ├── FilteringContainer.js ├── Tree.js ├── TreeContainer.js ├── UnstableFastTree.js ├── __tests__ │ ├── FilteringContainer.test.js │ ├── Tree │ │ ├── __snapshots__ │ │ │ └── render.test.js.snap │ │ ├── extensions.test.js │ │ ├── filtering.test.js │ │ ├── render.test.js │ │ └── renderers.test.js │ ├── __snapshots__ │ │ └── FilteringContainer.test.js.snap │ └── eventWrappers.test.js ├── contants.js ├── eventWrappers.js ├── filtering │ ├── DefaultGroupRenderer.js │ └── __tests__ │ │ ├── DefaultGroupRenderer.test.js │ │ └── __snapshots__ │ │ └── DefaultGroupRenderer.test.js.snap ├── index.js ├── main.css ├── renderers │ ├── Deletable.js │ ├── Expandable.js │ ├── Favorite.js │ ├── __tests__ │ │ ├── Deletable.test.js │ │ ├── Expandable.test.js │ │ └── Favorite.test.js │ └── index.js ├── selectors │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ ├── filtering.test.js.snap │ │ │ ├── getFlattenedTree.test.js.snap │ │ │ └── nodes.test.js.snap │ │ ├── filtering.test.js │ │ ├── getFlattenedTree.test.js │ │ └── nodes.test.js │ ├── filtering.js │ ├── getFlattenedTree.js │ └── nodes.js ├── setupTests.js ├── shapes │ ├── nodeShapes.js │ └── rendererShapes.js └── state │ ├── TreeState.js │ ├── TreeStateModifiers.js │ └── __tests__ │ ├── TreeState.test.js │ ├── TreeStateModifiers.test.js │ └── __snapshots__ │ ├── TreeState.test.js.snap │ └── TreeStateModifiers.test.js.snap ├── testData └── sampleTree.js ├── tests └── NodeDiff.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /demo/dist 3 | /es 4 | /lib 5 | /node_modules 6 | /umd 7 | npm-debug.log* 8 | /gh-pages 9 | package-lock.json 10 | yarn-error.log -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: node_js 4 | node_js: 5 | - 8 6 | 7 | before_install: 8 | - yarn add coveralls -D 9 | 10 | script: 11 | - yarn run build 12 | - yarn run test:coverage 13 | 14 | after_success: 15 | - cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage 16 | - npx semantic-release 17 | 18 | branches: 19 | only: 20 | - master 21 | 22 | env: 23 | global: 24 | - CI: true 25 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | #### Caption 2 | - :lollipop: - Refactor/improvment 3 | - :rocket: - Feature 4 | - :bug: - Bug fix 5 | 6 | ### 2.0.1 7 | - :lollipop: Nwb update to the latest release ([diogofcunha](https://github.com/diogofcunha) - [#41](https://github.com/diogofcunha/react-virtualized-tree/pull/41)) 8 | 9 | - :lollipop: React 16.3 update ([diogofcunha](https://github.com/diogofcunha) - [#40](https://github.com/diogofcunha/react-virtualized-tree/pull/40)) 10 | 11 | - :lollipop:/:rocket: Optimize bundle ([diogofcunha](https://github.com/diogofcunha) - [#38](https://github.com/diogofcunha/react-virtualized-tree/pull/38)) 12 | > react-dom, react-virtualized and full lodash were included in the build. umd module size 322.58 KB (:worried:) to 28.05 KB (:relaxed:) 13 | 14 | - :lollipop: Add a basic screenshot. Update the example link. ([justinlawrence](https://github.com/justinlawrence) - [#32](https://github.com/diogofcunha/react-virtualized-tree/pull/32)) 15 | 16 | - :lollipop: Store filter mappings to improve future update performance. ([diogofcunha](https://github.com/diogofcunha) - [#28](https://github.com/diogofcunha/react-virtualized-tree/pull/28)) 17 | 18 | - :lollipop: Internal renames. ([diogofcunha](https://github.com/diogofcunha) - [#27](https://github.com/diogofcunha/react-virtualized-tree/pull/27)) 19 | 20 | - :rocket: Exposing Left Margin as a User Modifiable Property. ([blakfeld](https://github.com/blakfeld) - [#25](https://github.com/diogofcunha/react-virtualized-tree/pull/25)) 21 | > Exposing tree node left margin as a prop 22 | 23 | - :bug: Fixing typo. ([blakfeld](https://github.com/blakfeld) - [#24](https://github.com/diogofcunha/react-virtualized-tree/pull/24)) 24 | > Fixed typo udpateNode 25 | 26 | 27 | - :rocket: Dynamic node height. ([diogofcunha](https://github.com/diogofcunha) - [#22](https://github.com/diogofcunha/react-virtualized-tree/pull/22)) 28 | > Using CellMeasurer to have dynamic row heigths, example added at https://diogofcunha.github.io/react-virtualized-tree/#/examples/node-measure 29 | 30 | - :lollipop:/:rocket: Improve node replace algorithm. ([diogofcunha](https://github.com/diogofcunha) - [#20](https://github.com/diogofcunha/react-virtualized-tree/pull/20)) 31 | 32 | > Refactor of replacement algorithm, with 230k nodes, replacing a node ~500ms (:worried:) to ~10ms (:relaxed:) 33 | 34 | 35 | ##### 1.2.5 36 | 37 | - Add typescript type definitions. ([diogofcunha](https://github.com/diogofcunha) - [#19](https://github.com/diogofcunha/react-virtualized-tree/pull/19)) 38 | - Node ids can now be strings. ([diogofcunha](https://github.com/diogofcunha) - [#16](https://github.com/diogofcunha/react-virtualized-tree/pull/16)) 39 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Prerequisites 2 | 3 | [Node.js](http://nodejs.org/) >= v4 must be installed. 4 | 5 | ## Installation 6 | 7 | - Running `npm install` in the components's root directory will install everything you need for development. 8 | 9 | ## Demo Development Server 10 | 11 | - `npm start` will run a development server with the component's demo app at [http://localhost:3000](http://localhost:3000) with hot module reloading. 12 | 13 | ## Running Tests 14 | 15 | - `npm test` will run the tests once. 16 | 17 | - `npm run test:coverage` will run the tests and produce a coverage report in `coverage/`. 18 | 19 | - `npm run test:watch` will run the tests on every change. 20 | 21 | ## Building 22 | 23 | - `npm run build` will build the component for publishing to npm and also bundle the demo app. 24 | 25 | - `npm run clean` will delete built resources. 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Diogo Cunha 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-virtualized-tree 2 | 3 | [![Travis][build-badge]][build] 4 | [![npm package][npm-badge]][npm] 5 | [![Coveralls][coveralls-badge]][coveralls] 6 | [![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg)](https://gitter.im/react-virtualized-tree/Lobby) 7 | [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) 8 | 9 | [build-badge]: https://img.shields.io/travis/diogofcunha/react-virtualized-tree/master.png?style=flat-square 10 | [build]: https://travis-ci.org/diogofcunha/react-virtualized-tree 11 | [npm-badge]: https://img.shields.io/npm/v/react-virtualized-tree.png?style=flat-square 12 | [npm]: https://www.npmjs.com/package/react-virtualized-tree 13 | [coveralls-badge]: https://img.shields.io/coveralls/diogofcunha/react-virtualized-tree/master.png?style=flat-square 14 | [coveralls]: https://coveralls.io/github/diogofcunha/react-virtualized-tree 15 | 16 |
17 | 18 |
19 | 20 | ## Introduction 21 | 22 | **react-virtualized-tree** is a tree view react library built on top of [react-virtualized](https://bvaughn.github.io/react-virtualized/#/components/List) 23 | 24 | Its main goal is to display tree like data in a beautiful and fast way. Being a reactive library it uses children functions to achieve maximum extensibility. The core idea behind it is that anyone using it is enable to create a tree as they intent just by rendering their own components or components exported by the tree. 25 | 26 | Demo and docs can be found [in here](https://diogofcunha.github.io/react-virtualized-tree/#/examples/basic-tree). 27 | 28 | ## Installation 29 | 30 | You can install via npm or yarn. 31 | `npm i react-virtualized-tree --save` 32 | 33 | or 34 | 35 | `yarn add react-virtualized-tree` 36 | 37 | To get the basic styles for free you need to import react-virtualized styles only once. 38 | 39 | ``` 40 | import 'react-virtualized/styles.css' 41 | import 'react-virtualized-tree/lib/main.css' 42 | ``` 43 | 44 | If you want to use the icons in the default renderers do the same for material icons. 45 | 46 | `import 'material-icons/css/material-icons.css'` 47 | 48 | ## Usage 49 | 50 | To use the standalone tree 51 | 52 | `import Tree from 'react-virtualized-tree'` 53 | 54 | To use the FilteringContainer 55 | 56 | `import { FilteringContainer } from 'react-virtualized-tree'` 57 | 58 | ## Dependencies 59 | 60 | Most react-virtualized-tree Dependencies are managed internally, the only required peerDependencies are **react**, **react-dom** and **react-virtualized**. 61 | -------------------------------------------------------------------------------- /__mocks__/react-virtualized.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export class CellMeasurerCache {} 4 | 5 | export const AutoSizer = ({children}) => {children({width: 1000, height: 1000})}; 6 | // const mockInifiteLoader = ({children}) => ( 7 | // {children({registerChild: jest.fn(), onRowsRendered: jest.fn()})} 8 | // ); 9 | 10 | export const CellMeasurer = ({children}) => {children({measure: jest.fn()})}; 11 | 12 | export const List = p => 13 | Array.from({length: p.rowCount}).map((_, i) => 14 | p.rowRenderer({ 15 | key: String(i), 16 | columnIndex: 0, 17 | index: i, 18 | rowIndex: i, 19 | isScrolling: false, 20 | isVisible: true, 21 | parent: {}, 22 | style: {}, 23 | }), 24 | ); 25 | -------------------------------------------------------------------------------- /config/pages.js: -------------------------------------------------------------------------------- 1 | const ghpages = require('gh-pages'); 2 | const path = require('path'); 3 | 4 | ghpages.publish(path.join(__dirname, '../demo/dist'), err => { 5 | if (err) throw err; 6 | }); 7 | -------------------------------------------------------------------------------- /config/test/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /config/test/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | // This is a custom Jest transformer turning file imports into filenames. 6 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 7 | 8 | module.exports = { 9 | process(src, filename) { 10 | return `module.exports = ${JSON.stringify(path.basename(filename))};`; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /config/test/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | process.env.BABEL_ENV = 'test'; 4 | process.env.NODE_ENV = 'test'; 5 | process.env.PUBLIC_URL = ''; 6 | 7 | process.on('unhandledRejection', err => { 8 | throw err; 9 | }); 10 | 11 | const jest = require('jest'); 12 | const argv = process.argv.slice(2); 13 | 14 | // Watch unless on CI or in coverage mode 15 | if (!process.env.CI && argv.indexOf('--coverage') < 0) { 16 | argv.push('--watch'); 17 | } 18 | 19 | jest.run(argv); 20 | -------------------------------------------------------------------------------- /demo/src/Home.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Segment, Header} from 'semantic-ui-react'; 3 | 4 | export default () => ( 5 |
6 |
Introduction
7 | 8 |

9 | react-virtualized-tree is a react library built on top of{' '} 10 | react-virtualized. 11 |

12 |

Its main goal is to display tree like data in a beautiful and fast way.

13 |

Being a reactive library it uses children functions to achieve maximum extensibility

14 |

15 | The core idea behind it is that anyone using it is enable to create a tree as they wich just by rendering their 16 | own components or components exported by the tree 17 |

18 |
19 |
Installation
20 | 21 |

You can install via npm or yarn.

22 | 23 | npm i react-virtualized-tree --save 24 | 25 | 26 | yarn add react-virtualized-tree 27 | 28 | To get the basic styles for free you need to import react-virtualized styles only once. 29 | 30 | import 'react-virtualized/styles.css'; 31 | 32 | 33 | import 'react-virtualized-tree/lib/main.css'; 34 | 35 |

If you want to use the icons in the default renderers do the same for material icons.

36 | 37 | import 'material-icons/css/material-icons.css'; 38 | 39 |
40 |
Dependencies
41 | 42 |

43 | Most react-virtualized-tree Dependencies are managed internally, the only required peerDependencies are{' '} 44 | react, react-dom and react-virtualized. 45 |

46 |
47 |
48 | ); 49 | -------------------------------------------------------------------------------- /demo/src/NavBar.css: -------------------------------------------------------------------------------- 1 | .header { 2 | margin-bottom: 20px; 3 | } 4 | 5 | .header-text { 6 | text-align: center; 7 | font-size: 70px; 8 | margin-top: 50px; 9 | } 10 | 11 | .content { 12 | padding: 20px; 13 | } -------------------------------------------------------------------------------- /demo/src/NavBar.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Sidebar, Segment, Menu, Image, Grid} from 'semantic-ui-react'; 3 | import {Link} from 'react-router-dom'; 4 | 5 | import {getRepoPath} from './toolbelt'; 6 | import './NavBar.css'; 7 | 8 | class NavBar extends Component { 9 | render() { 10 | return ( 11 |
12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | react-virtualized-tree 21 | 22 | 23 |
24 | {this.props.children} 25 |
26 | 27 | 37 | 38 | Setup 39 | 40 | 41 | Documentation 42 | 43 | 44 | Examples 45 | 46 | 47 | GitHub 48 | 49 | 50 | 51 |
52 |
53 | ); 54 | } 55 | } 56 | 57 | export default NavBar; 58 | -------------------------------------------------------------------------------- /demo/src/containers/DocumentsContainer.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Grid, Menu, Segment, Header} from 'semantic-ui-react'; 3 | import {Link, Route} from 'react-router-dom'; 4 | 5 | import documents from '../docs'; 6 | import Doc from '../docs/Doc'; 7 | import {getDocumentsPath} from '../toolbelt'; 8 | import './ExamplesContainer.css'; 9 | 10 | export default class ExamplesContainer extends Component { 11 | render() { 12 | return ( 13 |
14 | 15 | 16 | 17 | {Object.keys(documents).map(path => ( 18 | 19 | 20 | 21 | ))} 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /demo/src/containers/ExamplesContainer.css: -------------------------------------------------------------------------------- 1 | .jump-to-source { 2 | float: right; 3 | color: #87CEFA; 4 | } -------------------------------------------------------------------------------- /demo/src/containers/ExamplesContainer.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Grid, Menu, Segment, Header} from 'semantic-ui-react'; 3 | import {Route} from 'react-router'; 4 | import examples from '../examples'; 5 | import {Link} from 'react-router-dom'; 6 | import {getExamplePath} from '../toolbelt'; 7 | import './ExamplesContainer.css'; 8 | 9 | export default class ExamplesContainer extends Component { 10 | render() { 11 | return ( 12 |
13 | 14 | 15 | 16 | {Object.keys(examples).map(path => ( 17 | 18 | 19 | 20 | ))} 21 | 22 | 23 | 24 | 25 | { 28 | const selectedExample = examples[p.match.params.example]; 29 | const {component: Component, name, description, fileName} = selectedExample; 30 | 31 | return ( 32 |
33 | 34 | Jump to source 35 | 36 |
{name}
37 | {description && {description}} 38 |
39 | 40 |
41 |
42 | ); 43 | }} 44 | /> 45 |
46 |
47 |
48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /demo/src/docs/Doc.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Loader} from 'semantic-ui-react'; 3 | import ReactMarkdown from 'react-markdown'; 4 | 5 | import documents from './'; 6 | import {getDocumentFetchUrl} from '../toolbelt'; 7 | 8 | import {polyfill} from 'react-lifecycles-compat'; 9 | 10 | class Doc extends Component { 11 | state = { 12 | doc: null, 13 | }; 14 | 15 | componentDidMount() { 16 | const { 17 | match: { 18 | params: {document}, 19 | }, 20 | } = this.props; 21 | 22 | this.getDocument(document); 23 | } 24 | 25 | getDocument = doc => { 26 | return fetch(getDocumentFetchUrl(doc)) 27 | .then(r => r.text()) 28 | .then(doc => { 29 | this.setState({doc}); 30 | }); 31 | }; 32 | 33 | UNSAFE_componentWillReceiveProps({ 34 | match: { 35 | params: {document}, 36 | }, 37 | }) { 38 | const { 39 | match: { 40 | params: {document: selectedDocument}, 41 | }, 42 | } = this.props; 43 | 44 | if (document !== selectedDocument) { 45 | this.setState({doc: null}, () => { 46 | this.getDocument(document); 47 | }); 48 | } 49 | } 50 | 51 | render() { 52 | const {doc} = this.state; 53 | 54 | return !doc ? : ; 55 | } 56 | } 57 | 58 | polyfill(Doc); 59 | 60 | export default Doc; 61 | -------------------------------------------------------------------------------- /demo/src/docs/extensions.md: -------------------------------------------------------------------------------- 1 | # Extensions 2 | The entry component supports extensions to the components original functionality. The currently available extensions are: 3 | - updateTypeHandlers 4 | 5 | ## updateTypeHandlers 6 | ### () => { [updateType: number]: (nodes: Node[], updatedNode: Node) => Nodes[] 7 | Allows you to override or create new handlers, can be of use in case you either want to extend currently supported functionality (for example if you wish to flag updated nodes with a boolean property to help you filter out data before sending to the server side) or to create your own update types and handlers to support bussiness requirements. 8 | 9 | The existing update types are exported by the module inside a named export (import { constants } from 'react-virtualized-tree') 10 | -------------------------------------------------------------------------------- /demo/src/docs/filtering.md: -------------------------------------------------------------------------------- 1 | # FilteringContainer 2 | FilteringComponent is a component that can be used to wrap the Tree in order to get filtering and group functionality for free. 3 | 4 | To use this component you should know a little bit about [using functions as children](https://codedaily.io/tutorials/6/Using-Functions-as-Children-and-Render-Props-in-React-Components). 5 | 6 | ## Props 7 | 8 | | Name | Type | Description | 9 | | --------------------- | -------------| ----------- | 10 | | nodes | Node[] | List of original nodes | 11 | | children | ({ nodes: Node[] }) => JSX | A renderer for the tree| 12 | | debouncer? | (setFilter: Func, timeout) => void| Debounce search results, default to lodash debouncer with 300ms timeout| 13 | | groups? | { [groupKey: string]: *Group } | Groups with selectors to quickly filter results | 14 | | selectedGroup? | string | Selected group key | 15 | | groupRenderer? | Component<*RendererProps> | Custom group renderer | 16 | | onSelectedGroupChange?| (groupKey: string) => void | A handler invoked when group selection changes | 17 | 18 | 19 | ## Props Injected in children function 20 | | Name | Type | Description | 21 | | ------------- | -------------| ----------- | 22 | | nodes | Node[] | List of nodes after filters being applied | 23 | 24 | ## Created context 25 | | Name | Type | Description | 26 | | ---------------| --------| ----------- | 27 | | unfilteredNodes| Node[] | List of nodes originally injected in the component | 28 | 29 | * Node = { 30 | id: number 31 | name: string, 32 | state: object 33 | } 34 | 35 | * Group = { 36 | name: string, 37 | filter: (node: Node) => boolean 38 | } 39 | 40 | * RendererProps = { 41 | onChange: (groupKey: string) => void, 42 | groups: { [groupKey: string]: *Group }, 43 | selectedGroup: string 44 | } 45 | -------------------------------------------------------------------------------- /demo/src/docs/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | renderers: { 3 | name: 'Renderers', 4 | }, 5 | extensions: { 6 | name: 'Extensions', 7 | }, 8 | filtering: { 9 | name: 'Filtering', 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /demo/src/docs/renderers.md: -------------------------------------------------------------------------------- 1 | # Renderers 2 | 3 | A renderer is a component you can use to build your tree stucture. The power of it is that you can either use the renderers shipped with this package or you can create your own! A few examples on how to achieve it can be found at the examples section. 4 | 5 | ## Injected props 6 | 7 | | Name | Type | Description | 8 | | -------- | ---------------------------------------------- | ------------------------------------------------------------------------------------- | 9 | | onChange | ({ node: Node _, type: UPDATE_TYPE _}) => void | to be invoked with the updated node and the update type | 10 | | node | Node \* | The node being rendered for that row, with some additional info | 11 | | children | JSX | Anything your render as children of the component when using it | 12 | | style | React.CSSProperties | A opt in style property that will auto align your items and apply some default styles | 13 | | measure | () => void | A function that can be used to let the tree know about your node measurements | 14 | 15 | - Node = { 16 | id: number 17 | name: string, 18 | state: object, 19 | deepness: number, 20 | parents: number[] 21 | } 22 | - UPDATE_TYPE = { 23 | ADD: 0, 24 | DELETE: 1, 25 | UPDATE: 2 26 | } 27 | -------------------------------------------------------------------------------- /demo/src/examples/Basic/DraggableRenderer.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {findDOMNode} from 'react-dom'; 4 | import {DragSource, DropTarget} from 'react-dnd'; 5 | import ItemTypes from './ItemTypes'; 6 | import {Label, Icon} from 'semantic-ui-react'; 7 | 8 | const style = { 9 | marginBottom: '.5rem', 10 | cursor: 'move', 11 | width: '50%', 12 | }; 13 | 14 | const cardSource = { 15 | beginDrag(props) { 16 | return { 17 | id: props.id, 18 | index: props.index, 19 | }; 20 | }, 21 | }; 22 | 23 | const cardTarget = { 24 | hover(props, monitor, component) { 25 | const dragIndex = monitor.getItem().index; 26 | const hoverIndex = props.index; 27 | 28 | // Don't replace items with themselves 29 | if (dragIndex === hoverIndex) { 30 | return; 31 | } 32 | 33 | // Determine rectangle on screen 34 | const hoverBoundingRect = findDOMNode(component).getBoundingClientRect(); 35 | 36 | // Get vertical middle 37 | const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2; 38 | 39 | // Determine mouse position 40 | const clientOffset = monitor.getClientOffset(); 41 | 42 | // Get pixels to the top 43 | const hoverClientY = clientOffset.y - hoverBoundingRect.top; 44 | 45 | // Only perform the move when the mouse has crossed half of the items height 46 | // When dragging downwards, only move when the cursor is below 50% 47 | // When dragging upwards, only move when the cursor is above 50% 48 | 49 | // Dragging downwards 50 | if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) { 51 | return; 52 | } 53 | 54 | // Dragging upwards 55 | if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) { 56 | return; 57 | } 58 | 59 | // Time to actually perform the action 60 | props.moveRenderer(dragIndex, hoverIndex); 61 | 62 | // Note: we're mutating the monitor item here! 63 | // Generally it's better to avoid mutations, 64 | // but it's good here for the sake of performance 65 | // to avoid expensive index searches. 66 | monitor.getItem().index = hoverIndex; 67 | }, 68 | }; 69 | 70 | @DropTarget(ItemTypes.RENDERER, cardTarget, connect => ({ 71 | connectDropTarget: connect.dropTarget(), 72 | })) 73 | @DragSource(ItemTypes.RENDERER, cardSource, (connect, monitor) => ({ 74 | connectDragSource: connect.dragSource(), 75 | isDragging: monitor.isDragging(), 76 | })) 77 | export default class RendererCard extends Component { 78 | static propTypes = { 79 | connectDragSource: PropTypes.func.isRequired, 80 | connectDropTarget: PropTypes.func.isRequired, 81 | index: PropTypes.number.isRequired, 82 | isDragging: PropTypes.bool.isRequired, 83 | id: PropTypes.any.isRequired, 84 | moveRenderer: PropTypes.func.isRequired, 85 | }; 86 | 87 | render() { 88 | const {renderer, id, isDragging, connectDragSource, connectDropTarget, handleRendererDeselection} = this.props; 89 | const opacity = isDragging ? 0 : 1; 90 | 91 | const isNodeName = renderer.name === 'NodeNameRenderer'; 92 | const name = isNodeName ? 'Node Name' : renderer.name; 93 | 94 | return connectDragSource( 95 | connectDropTarget( 96 |
97 | 101 |
, 102 | ), 103 | ); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /demo/src/examples/Basic/ItemTypes.js: -------------------------------------------------------------------------------- 1 | export default { 2 | RENDERER: 'renderer', 3 | }; 4 | -------------------------------------------------------------------------------- /demo/src/examples/Basic/RendererDragContainer.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {DragDropContext} from 'react-dnd'; 3 | import HTML5Backend from 'react-dnd-html5-backend'; 4 | import DraggableRenderer from './DraggableRenderer'; 5 | 6 | const style = { 7 | width: 400, 8 | }; 9 | 10 | @DragDropContext(HTML5Backend) 11 | export default class RendererDragContainer extends Component { 12 | render() { 13 | return ( 14 |
15 | {this.props.selectedRenderers.map((card, i) => ( 16 | 24 | ))} 25 |
26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /demo/src/examples/Basic/index.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import reactElementToJSXString from 'react-element-to-jsx-string'; 3 | import {Grid, Header, Label, Icon} from 'semantic-ui-react'; 4 | import update from 'immutability-helper'; 5 | 6 | import Tree from '../../../../src/TreeContainer'; 7 | import Renderers from '../../../../src/renderers'; 8 | import {Nodes} from '../../../../testData/sampleTree'; 9 | import {createEntry} from '../../toolbelt'; 10 | import RendererDragContainer from './RendererDragContainer'; 11 | 12 | const {Deletable, Expandable, Favorite} = Renderers; 13 | 14 | const NodeNameRenderer = ({node: {name}, children}) => ( 15 | 16 | {name} 17 | {children} 18 | 19 | ); 20 | 21 | class BasicTree extends Component { 22 | state = { 23 | nodes: Nodes, 24 | availableRenderers: [Expandable, Deletable, Favorite], 25 | selectedRenderers: [Expandable, NodeNameRenderer], 26 | }; 27 | 28 | handleRendererMove = (dragIndex, hoverIndex) => { 29 | const {selectedRenderers} = this.state; 30 | const dragCard = selectedRenderers[dragIndex]; 31 | 32 | this.setState( 33 | update(this.state, { 34 | selectedRenderers: { 35 | $splice: [[dragIndex, 1], [hoverIndex, 0, dragCard]], 36 | }, 37 | }), 38 | ); 39 | }; 40 | 41 | handleChange = nodes => { 42 | this.setState({nodes}); 43 | }; 44 | 45 | renderNodeDisplay = (display, props, children = []) => React.createElement(display, props, children); 46 | 47 | createNodeRenderer = (nodeDisplay = this.state.nodeDisplay, props) => { 48 | const [nextNode, ...remainingNodes] = nodeDisplay; 49 | 50 | if (remainingNodes.length === 0) { 51 | return this.renderNodeDisplay(nextNode, props); 52 | } 53 | 54 | return this.renderNodeDisplay(nextNode, props, this.createNodeRenderer(remainingNodes, props)); 55 | }; 56 | 57 | getRenderedComponentTree = () => 58 | reactElementToJSXString(this.createNodeRenderer(this.state.selectedRenderers, {node: {name: 'X', id: 0}})) 59 | .split('>') 60 | .filter(c => c) 61 | .map((c, i) => { 62 | const { 63 | selectedRenderers: {length}, 64 | } = this.state; 65 | const isClosingTag = i >= length; 66 | 67 | const marginLeft = !isClosingTag ? 10 * i : 10 * (length - 2 - Math.abs(length - i)); 68 | 69 | return
{c}>
; 70 | }); 71 | 72 | handleRendererDeselection = i => () => { 73 | this.setState(({selectedRenderers}) => ({ 74 | selectedRenderers: [...selectedRenderers.slice(0, i), ...selectedRenderers.slice(i + 1)], 75 | })); 76 | }; 77 | 78 | handleRendererSelection = renderer => () => { 79 | this.setState(({selectedRenderers}) => ({ 80 | selectedRenderers: [...selectedRenderers, renderer], 81 | })); 82 | }; 83 | 84 | render() { 85 | const renderersAvailableForAdd = this.state.availableRenderers.filter( 86 | r => this.state.selectedRenderers.indexOf(r) === -1, 87 | ); 88 | 89 | return ( 90 | 91 | 92 | 93 |
Available Renderers
94 | 95 | 96 | {renderersAvailableForAdd.map((r, i) => { 97 | return ( 98 | 102 | ); 103 | })} 104 | 105 |
106 | 107 |
Ouput tree
108 |
109 | 110 | {({style, ...p}) =>
{this.createNodeRenderer(this.state.selectedRenderers, p)}
} 111 |
112 |
113 |
114 |
115 | 116 | 117 |
Node renderer builder
118 | 119 | 120 | 125 | 126 |
127 | 128 |
JSX
129 | 130 | {this.getRenderedComponentTree()} 131 |
132 |
133 |
134 | ); 135 | } 136 | } 137 | 138 | export default createEntry( 139 | 'basic-tree', 140 | 'Basic/index', 141 | 'Basic Tree', 142 |
143 |

144 | A tree that enables favorite toogle, expansion and deletion, this example only makes use of the default renderers 145 |

146 |
, 147 | BasicTree, 148 | ); 149 | -------------------------------------------------------------------------------- /demo/src/examples/ChangeRenderers.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | 3 | import Tree from '../../../src/TreeContainer'; 4 | import Renderers from '../../../src/renderers'; 5 | import {Nodes} from '../../../testData/sampleTree'; 6 | import {createEntry} from '../toolbelt'; 7 | 8 | const {Expandable} = Renderers; 9 | 10 | class ChangeRenderers extends Component { 11 | state = { 12 | nodes: Nodes, 13 | }; 14 | 15 | handleChange = nodes => { 16 | this.setState({nodes}); 17 | }; 18 | 19 | render() { 20 | return ( 21 | 22 | {({style, node, ...rest}) => ( 23 |
24 | 33 | {node.name} 34 | 35 |
36 | )} 37 |
38 | ); 39 | } 40 | } 41 | 42 | export default createEntry( 43 | 'customize-renderers', 44 | 'ChangeRenderers', 45 | 'Customize default renderers', 46 |
47 |

48 | A good example of a possible customization of a default renderer is customizing the tree to display as a folder 49 | structure. 50 |

51 | 52 |

53 | By exposing iconsClassNameMap it is possible to pass in the styles applied to the Expandable 54 | rendererer, the available style options are: 55 |

56 | {'{ '} 57 | expanded: string; collapsed: string; lastChild: string; 58 | {' }'} 59 |
, 60 | ChangeRenderers, 61 | ); 62 | -------------------------------------------------------------------------------- /demo/src/examples/Extensions.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | import Tree from '../../../src/TreeContainer'; 5 | import Renderers from '../../../src/renderers'; 6 | import {Nodes} from '../../../testData/sampleTree'; 7 | import {createEntry} from '../toolbelt'; 8 | 9 | const {Expandable} = Renderers; 10 | 11 | const SELECT = 3; 12 | 13 | const Selection = ({node, children, onChange}) => { 14 | const {state: {selected} = {}} = node; 15 | const className = classNames({ 16 | 'mi mi-check-box': selected, 17 | 'mi mi-check-box-outline-blank': !selected, 18 | }); 19 | 20 | return ( 21 | 22 | 25 | onChange({ 26 | node: { 27 | ...node, 28 | state: { 29 | ...(node.state || {}), 30 | selected: !selected, 31 | }, 32 | }, 33 | type: SELECT, 34 | }) 35 | } 36 | /> 37 | {children} 38 | 39 | ); 40 | }; 41 | 42 | class Extensions extends Component { 43 | state = { 44 | nodes: Nodes, 45 | }; 46 | 47 | handleChange = nodes => { 48 | this.setState({nodes}); 49 | }; 50 | 51 | selectNodes = (nodes, selected) => 52 | nodes.map(n => ({ 53 | ...n, 54 | children: n.children ? this.selectNodes(n.children, selected) : [], 55 | state: { 56 | ...n.state, 57 | selected, 58 | }, 59 | })); 60 | 61 | nodeSelectionHandler = (nodes, updatedNode) => 62 | nodes.map(node => { 63 | if (node.id === updatedNode.id) { 64 | return { 65 | ...updatedNode, 66 | children: node.children ? this.selectNodes(node.children, updatedNode.state.selected) : [], 67 | }; 68 | } 69 | 70 | if (node.children) { 71 | return {...node, children: this.nodeSelectionHandler(node.children, updatedNode)}; 72 | } 73 | 74 | return node; 75 | }); 76 | 77 | render() { 78 | return ( 79 | 88 | {({style, node, ...rest}) => ( 89 |
90 | 91 | 92 | {node.name} 93 | 94 | 95 |
96 | )} 97 |
98 | ); 99 | } 100 | } 101 | 102 | export default createEntry( 103 | 'extensions', 104 | 'Extensions', 105 | 'Extending behaviour', 106 |
107 |

108 | A good example of a possible extension is creating a new handler to select nodes that automatically selects all 109 | the children nodes. 110 |

111 | 112 |

113 | By injecting extensions prop with an update type handler for node selection that can be achieved. 114 |

115 |
, 116 | Extensions, 117 | ); 118 | -------------------------------------------------------------------------------- /demo/src/examples/Filterable.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import classNames from 'classnames'; 3 | import {Checkbox} from 'semantic-ui-react'; 4 | 5 | import Tree from '../../../src/TreeContainer'; 6 | import Renderers from '../../../src/renderers'; 7 | import {createEntry, constructTree} from '../toolbelt'; 8 | import FilteringContainer from '../../../src/FilteringContainer'; 9 | import Favorite from '../../../src/renderers/Favorite'; 10 | 11 | const {Expandable} = Renderers; 12 | 13 | const MAX_DEEPNESS = 3; 14 | const MAX_NUMBER_OF_CHILDREN = 4; 15 | const MIN_NUMBER_OF_PARENTS = 5; 16 | 17 | const Nodes = constructTree(MAX_DEEPNESS, MAX_NUMBER_OF_CHILDREN, MIN_NUMBER_OF_PARENTS); 18 | 19 | const EXPANDED = 'EXPANDED'; 20 | 21 | class Filterable extends Component { 22 | state = { 23 | nodes: Nodes, 24 | selectedGroup: EXPANDED, 25 | groupsEnabled: true, 26 | }; 27 | 28 | get _groupProps() { 29 | return this.state.groupsEnabled 30 | ? { 31 | groups: { 32 | ALL: { 33 | name: 'All', 34 | filter: node => true, 35 | }, 36 | [EXPANDED]: { 37 | name: 'Expanded', 38 | filter: node => (node.state || {}).expanded, 39 | }, 40 | FAVORITES: { 41 | name: 'Favorites', 42 | filter: node => (node.state || {}).favorite, 43 | }, 44 | }, 45 | selectedGroup: this.state.selectedGroup, 46 | onSelectedGroupChange: this.handleSelectedGroupChange, 47 | } 48 | : {}; 49 | } 50 | 51 | handleChange = nodes => { 52 | this.setState({nodes}); 53 | }; 54 | 55 | handleSelectedGroupChange = selectedGroup => { 56 | this.setState({selectedGroup}); 57 | }; 58 | 59 | handleGroupsToogle = () => { 60 | this.setState({groupsEnabled: !this.state.groupsEnabled}); 61 | }; 62 | 63 | render() { 64 | return ( 65 |
66 | 73 | 74 | {({nodes}) => ( 75 |
76 | 77 | {({style, node, ...rest}) => ( 78 |
79 | 80 | 81 | {node.name} 82 | 83 | 84 |
85 | )} 86 |
87 |
88 | )} 89 |
90 |
91 | ); 92 | } 93 | } 94 | 95 | export default createEntry( 96 | 'filterable', 97 | 'Filterable', 98 | 'Filterable tree', 99 |
100 |

When working with big data collections filtering can be very handy.

101 | 102 |

103 | By wrapping the Tree with the FilteringContainer your tree will only recieve the nodes it needs to 104 | render. 105 |

106 |
, 107 | Filterable, 108 | ); 109 | -------------------------------------------------------------------------------- /demo/src/examples/KeyboardNavigation.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import FocusTrap from 'focus-trap-react'; 3 | import Tree from '../../../src/TreeContainer'; 4 | import {Nodes} from '../../../testData/sampleTree'; 5 | import {createEntry} from '../toolbelt'; 6 | import Renderers from '../../../src/renderers'; 7 | 8 | const {Expandable, Favorite} = Renderers; 9 | 10 | class KeyboardNavigation extends Component { 11 | state = { 12 | nodes: Nodes, 13 | trapFocus: false, 14 | }; 15 | 16 | handleChange = nodes => { 17 | this.setState({nodes}); 18 | }; 19 | 20 | nodeRenderer = ({style, node, ...rest}) => { 21 | return ( 22 |
23 | 24 | 25 | {node.name} 26 | 27 | 28 |
29 | ); 30 | }; 31 | 32 | render() { 33 | return ( 34 | 35 | 36 | {this.nodeRenderer} 37 | 38 | 39 | ); 40 | } 41 | } 42 | 43 | export default createEntry( 44 | 'keyboard-nav', 45 | 'KeyboardNavigation', 46 | 'Keyboard navigation', 47 |
48 |

A tree that supports keyboard navigation

49 |
, 50 | KeyboardNavigation, 51 | ); 52 | -------------------------------------------------------------------------------- /demo/src/examples/LargeCollection.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | 3 | import UnstableFastTree from '../../../src/UnstableFastTree'; 4 | import Renderers from '../../../src/renderers'; 5 | import {createEntry, constructTree} from '../toolbelt'; 6 | import TreeState from '../../../src/state/TreeState'; 7 | 8 | const MIN_NUMBER_OF_PARENTS = 500; 9 | const MAX_NUMBER_OF_CHILDREN = 30; 10 | const MAX_DEEPNESS = 4; 11 | 12 | const {Deletable, Expandable, Favorite} = Renderers; 13 | 14 | const Nodes = constructTree(MAX_DEEPNESS, MAX_NUMBER_OF_CHILDREN, MIN_NUMBER_OF_PARENTS); 15 | const getTotalNumberOfElements = (nodes, counter = 0) => { 16 | return counter + nodes.length + nodes.reduce((acc, n) => getTotalNumberOfElements(n.children, acc), 0); 17 | }; 18 | 19 | const totalNumberOfNodes = getTotalNumberOfElements(Nodes); 20 | 21 | class LargeCollection extends Component { 22 | state = { 23 | nodes: TreeState.createFromTree(Nodes), 24 | }; 25 | 26 | handleChange = nodes => { 27 | this.setState({nodes}); 28 | }; 29 | 30 | render() { 31 | return ( 32 | 33 | {({style, node, ...rest}) => ( 34 |
35 | 36 | {node.name} 37 | 38 | 39 | 40 | 41 |
42 | )} 43 |
44 | ); 45 | } 46 | } 47 | 48 | export default createEntry( 49 | 'large-collection', 50 | 'LargeCollection', 51 | 'Large Data Collection', 52 |
53 |

A tree that renders a large collection of nodes.

54 |

This example is rendering a total of {totalNumberOfNodes} nodes

55 |
, 56 | LargeCollection, 57 | ); 58 | -------------------------------------------------------------------------------- /demo/src/examples/NodeMeasure.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | 3 | import Tree from '../../../src/TreeContainer'; 4 | import Renderers from '../../../src/renderers'; 5 | import {createEntry} from '../toolbelt'; 6 | import {getNodeRenderOptions} from '../../../src/selectors/nodes'; 7 | 8 | const {Expandable} = Renderers; 9 | 10 | const Nodes = [ 11 | { 12 | id: 'arg', 13 | name: 'Argentina', 14 | children: [ 15 | { 16 | id: 'messi', 17 | name: 'Leo Messi', 18 | children: [{id: 'messi-desc', name: ''}], 19 | }, 20 | { 21 | id: 'maradona', 22 | name: 'Diego Maradona', 23 | children: [{id: 'maradona-desc', name: ''}], 24 | }, 25 | ], 26 | }, 27 | { 28 | id: 'pt', 29 | name: 'Portugal', 30 | children: [ 31 | { 32 | id: 'cr', 33 | name: 'Cristiano Ronaldo', 34 | children: [{id: 'cr-desc', name: ''}], 35 | }, 36 | { 37 | id: 'figo', 38 | name: 'Luis Figo', 39 | children: [{id: 'figo-desc', name: ''}], 40 | }, 41 | ], 42 | }, 43 | { 44 | id: 'br', 45 | name: 'Brazil', 46 | children: [ 47 | { 48 | id: 'r', 49 | name: 'Ronaldo', 50 | children: [{id: 'r-desc', name: ''}], 51 | }, 52 | { 53 | id: 'r10', 54 | name: 'Ronaldinho', 55 | children: [{id: 'r10-desc', name: ''}], 56 | }, 57 | { 58 | id: 'pele', 59 | name: 'Pele', 60 | children: [{id: 'pele-desc', name: ''}], 61 | }, 62 | ], 63 | }, 64 | { 65 | id: 'fr', 66 | name: 'France', 67 | children: [ 68 | { 69 | id: 'z', 70 | name: 'Zinedine Zidane', 71 | children: [{id: 'z-desc', name: ''}], 72 | }, 73 | { 74 | id: 'pl', 75 | name: 'Michel Platini', 76 | children: [{id: 'pl-desc', name: ''}], 77 | }, 78 | ], 79 | }, 80 | ]; 81 | 82 | const DESCRIPTIONS = { 83 | fr: 84 | 'France (French: [fʁɑ̃s]), officially the French Republic (French: République française, pronounced [ʁepyblik fʁɑ̃sɛz]), is a country whose territory consists of metropolitan France in western Europe, as well as several overseas regions and territories.[XIII] The metropolitan area of France extends from the Mediterranean Sea to the English Channel and the North Sea, and from the Rhine to the Atlantic Ocean. The overseas territories include French Guiana in South America and several islands in the Atlantic, Pacific and Indian oceans. The countrys 18 integral regions (five of which are situated overseas) span a combined area of 643,801 square kilometres (248,573 sq mi) which, as of October 2017, has a population of 67.15 million people.[10] France is a unitary semi-presidential republic with its capital in Paris, the countrys largest city and main cultural and commercial centre. Other major urban centres include Marseille, Lyon, Lille, Nice, Toulouse and Bordeaux.', 85 | arg: 86 | 'Argentina (/ˌɑːrdʒənˈtiːnə/ (About this sound listen); Spanish: [aɾxenˈtina]), officially the Argentine Republic[A] (Spanish: República Argentina), is a federal republic located mostly in the southern half of South America. Sharing the bulk of the Southern Cone with its neighbor Chile to the west, the country is also bordered by Bolivia and Paraguay to the north, Brazil to the northeast, Uruguay and the South Atlantic Ocean to the east, and the Drake Passage to the south. With a mainland area of 2,780,400 km2 (1,073,500 sq mi),[B] Argentina is the eighth-largest country in the world, the second largest in Latin America, and the largest Spanish-speaking nation. It is subdivided into twenty-three provinces (Spanish: provincias, singular provincia) and one autonomous city (ciudad autónoma), Buenos Aires, which is the federal capital of the nation (Spanish: Capital Federal) as decided by Congress.[11] The provinces and the capital have their own constitutions, but exist under a federal system. Argentina claims sovereignty over part of Antarctica, the Falkland Islands (Spanish: Islas Malvinas), and South Georgia and the South Sandwich Islands.', 87 | pt: 88 | 'Portugal (Portuguese: [puɾtuˈɣaɫ]), officially the Portuguese Republic (Portuguese: República Portuguesa ,[note 1] is a sovereign state located mostly on the Iberian Peninsula in southwestern Europe. It is the westernmost country of mainland Europe, being bordered to the west and south by the Atlantic Ocean and to the north and east by Spain. Its territory also includes the Atlantic archipelagos of the Azores and Madeira, both autonomous regions with their own regional governments. At 1.7 million km2, its Exclusive Economic Zone is the 3rd largest in the European Union and the 11th largest in the world.[10]', 89 | br: 90 | 'Brazil (/brəˈzɪl/ (About this sound listen); Portuguese: Brasil [bɾaˈziw][10]), officially the Federative Republic of Brazil (Portuguese: República Federativa do Brasil, About this sound listen (help·info)[11]), is the largest country in both South America and Latin America. At 8.5 million square kilometers (3.2 million square miles)[12] and with over 208 million people, Brazil is the worlds fifth-largest country by area and the sixth-most populous. The capital is Brasília, and the most-populated city is São Paulo. It is the largest country to have Portuguese as an official language and the only one in the Americas.[13][14] Bounded by the Atlantic Ocean on the east, Brazil has a coastline of 7,491 kilometers (4,655 mi).[15] It borders all other South American countries except Ecuador and Chile and covers 47.3% of the continents land area.[16] Its Amazon River basin includes a vast tropical forest, home to diverse wildlife, a variety of ecological systems, and extensive natural resources spanning numerous protected habitats.[15] This unique environmental heritage makes Brazil one of 17 megadiverse countries, and is the subject of significant global interest and debate regarding deforestation and environmental protection.', 91 | messi: 92 | 'Lionel Andrés Messi Cuccittini[note 1] (Spanish pronunciation: [ljoˈnel anˈdɾez ˈmesi] (About this sound listen);[A] born 24 June 1987) is an Argentine professional footballer who plays as a forward for Spanish club FC Barcelona and the Argentina national team. Often considered the best player in the world and regarded by many as the greatest of all time, Messi has a record-tying five Ballon dOr awards,[note 2] four of which he won consecutively, and a record-tying four European Golden Shoes.[note 3] He has spent his entire professional career with Barcelona, where he has won 29 trophies, including eight La Liga titles, four UEFA Champions League titles, and five Copas del Rey. Both a prolific goalscorer and a creative playmaker, Messi holds the records for most official goals scored in La Liga (368), a La Liga season (50), a club football season in Europe (73), a calendar year (91), El Clásico (26), as well as those for most assists made in La Liga (146) and the Copa América (11). He has scored over 600 senior career goals for club and country.', 93 | maradona: 94 | 'Diego Armando Maradona Franco (Spanish pronunciation: [ˈdjeɣo maɾaˈðona], born 30 October 1960) is an Argentine retired professional footballer and manager. Many in the sport, including football writers, players, and fans, regard Maradona as the greatest football player of all time.[7][8][9][10][11] He was joint FIFA Player of the 20th Century with Pelé.[12][13]', 95 | cr: 96 | 'Cristiano Ronaldo dos Santos Aveiro GOIH, ComM (European Portuguese: [kɾiʃˈtjɐnu ʁoˈnaɫdu]; born 5 February 1985) is a Portuguese professional footballer who plays as a forward for Spanish club Real Madrid and the Portugal national team. Often considered the best player in the world and widely regarded as the greatest of all time,[note 1] Ronaldo has five Ballon dOr awards,[note 2] the most for a European player and is tied for most all-time. He is the first player in history to win four European Golden Shoes. He has won 25 trophies in his career, including five league titles, four UEFA Champions League titles and one UEFA European Championship. A prolific goalscorer, Ronaldo holds the records for most official goals scored in the top five European leagues (380), the UEFA Champions League (116), the UEFA European Championship (29) and the FIFA Club World Cup (7), as well as most goals scored in a UEFA Champions League season (17). He has scored more than 600 senior career goals for club and country.', 97 | figo: 98 | 'Luís Filipe Madeira Caeiro Figo, OIH (Portuguese pronunciation: [luˈiʃ ˈfiɣu]; born 4 November 1972) is a retired Portuguese footballer. He played as a midfielder for Sporting CP, Barcelona, Real Madrid and Internazionale before retiring on 31 May 2009. He won 127 caps for the Portugal national team, a record at the time but later broken by Cristiano Ronaldo.', 99 | r: 100 | 'Ronaldo Luís Nazário de Lima (locally [ʁoˈnawdu ˈlwiʒ nɐˈzaɾju dʒ ˈɫĩmɐ]; born 18 September 1976[2]), commonly known as Ronaldo, is a retired Brazilian professional footballer who played as a striker. Popularly dubbed "O Fenômeno" (The Phenomenon), he is widely considered to be one of the greatest football players of all time.[3][4][5][6][7] In his prime, he was known for his dribbling at speed, feints, and clinical finishing.', 101 | r10: 102 | 'Ronaldo de Assis Moreira (born 21 March 1980), commonly known as Ronaldinho (Brazilian Portuguese: [ʁonawˈdʒĩɲu]) or Ronaldinho Gaúcho,[note 1] is a Brazilian former professional footballer and ambassador for Spanish club Barcelona.[4] He played mostly as an attacking midfielder, but was also deployed as a forward or a winger. He played the bulk of his career at European clubs Paris Saint-Germain, Barcelona and Milan as well as playing for the Brazilian national team. Often considered one of the best players of his generation and regarded by many as one of the greatest of all time,[note 2] Ronaldinho won two FIFA World Player of the Year awards and a Ballon dOr. He was renowned for his technical skills and creativity; due to his agility, pace and dribbling ability, as well as his use of tricks, overhead kicks, no-look passes and accuracy from free-kicks.', 103 | pele: 104 | 'Edson Arantes do Nascimento (Brazilian Portuguese: [ˈɛtsõ (w)ɐˈɾɐ̃tʃiz du nɐsiˈmẽtu]; born 23 October 1940), known as Pelé ([peˈlɛ]), is a Brazilian retired professional footballer who played as a forward. He is widely regarded as the greatest football player of all time. In 1999, he was voted World Player of the Century by the International Federation of Football History & Statistics (IFFHS). That same year, Pelé was elected Athlete of the Century by the International Olympic Committee. According to the IFFHS, Pelé is the most successful league goal-scorer in the world, scoring 1281 goals in 1363 games, which included unofficial friendlies and tour games. During his playing days, Pelé was for a period the best-paid athlete in the world.', 105 | z: 106 | 'Zinedine Yazid Zidane O.L.H., A.O.M.N. (French pronunciation: [zinedin zidan], born 23 June 1972), nicknamed "Zizou", is a French retired professional footballer and current manager of Real Madrid. He played as an attacking midfielder for the France national team, Cannes, Bordeaux, Juventus and Real Madrid.[3][4] An elite playmaker, renowned for his elegance, vision, ball control and technique, Zidane was named the best European footballer of the past 50 years in the UEFA Golden Jubilee Poll in 2004.[5] He is widely regarded as one of the greatest players of all time', 107 | pl: 108 | 'Michel François Platini (born 21 June 1955) is a French former football player, manager and administrator. Nicknamed Le Roi (The King) for his ability and leadership, he is regarded as one of the greatest footballers of all time. Platini won the Ballon dOr three times, in 1983, 1984 and 1985,[3] and came sixth in the FIFA Player of the Century vote.[4] In recognition of his achievements, he was named Chevalier of the Legion of Honour in 1985 and became Officier in 1988.', 109 | }; 110 | 111 | class FootballPlayerRenderer extends React.Component { 112 | componentDidMount() { 113 | this.props.measure(); 114 | } 115 | 116 | render() { 117 | const {node, children} = this.props; 118 | const {id, name} = node; 119 | const {isExpanded} = getNodeRenderOptions(node); 120 | 121 | return ( 122 | 123 | {children} 124 | {name} 125 | {isExpanded &&

{DESCRIPTIONS[id]}

} 126 |
127 | ); 128 | } 129 | } 130 | 131 | class NodeMeasure extends Component { 132 | state = { 133 | nodes: Nodes, 134 | }; 135 | 136 | handleChange = nodes => { 137 | this.setState({nodes}); 138 | }; 139 | 140 | render() { 141 | return ( 142 | 143 | {({style, ...p}) => ( 144 |
145 | 146 | 147 | 148 |
149 | )} 150 |
151 | ); 152 | } 153 | } 154 | 155 | export default createEntry( 156 | 'node-measure', 157 | 'NodeMeasure', 158 | 'Nodes with auto measure', 159 |
160 |

All cells in react-virtualized-tree implement react-virtualized's CellMeasurer

161 |

162 | All nodes receive a measure prop that can be used to measure nodes with different heights like what happens in 163 | this example 164 |

165 |
, 166 | NodeMeasure, 167 | ); 168 | -------------------------------------------------------------------------------- /demo/src/examples/Renderers.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | import Tree from '../../../src/TreeContainer'; 5 | import {Nodes} from '../../../testData/sampleTree'; 6 | import {createEntry} from '../toolbelt'; 7 | 8 | const Deepness = ({node, children}) => { 9 | const deepness = node.deepness + 1; 10 | const className = classNames({ 11 | [`mi mi-filter-${deepness}`]: deepness <= 9, 12 | 'filter-9-plus': deepness > 9, 13 | }); 14 | 15 | return ( 16 | 17 | 18 | {children} 19 | 20 | ); 21 | }; 22 | 23 | class Renderers extends Component { 24 | render() { 25 | return ( 26 | 27 | {({style, node, ...rest}) => ( 28 |
29 | 30 | {node.name} 31 | 32 |
33 | )} 34 |
35 | ); 36 | } 37 | } 38 | 39 | export default createEntry( 40 | 'renderers', 41 | 'Renderers', 42 | 'Create a custom renderer', 43 |
44 |

A tree that makes use of a custom renderer

45 |
, 46 | Renderers, 47 | ); 48 | -------------------------------------------------------------------------------- /demo/src/examples/WorldCup.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import 'flag-icon-css/css/flag-icon.min.css'; 3 | 4 | import Tree from '../../../src/TreeContainer'; 5 | import Renderers from '../../../src/renderers'; 6 | import {createEntry} from '../toolbelt'; 7 | 8 | const {Expandable} = Renderers; 9 | 10 | const groups = { 11 | A: [0, 1, 2, 3], 12 | B: [4, 5, 6, 7], 13 | C: [8, 9, 10, 11], 14 | D: [12, 13, 14, 15], 15 | E: [16, 17, 18, 19], 16 | F: [20, 21, 22, 23], 17 | G: [24, 25, 26, 27], 18 | H: [28, 29, 30, 31], 19 | }; 20 | 21 | const countries = { 22 | 0: {name: 'Russia', flag: 'RU'}, 23 | 1: {name: 'Saudi Arabia', flag: 'SA'}, 24 | 2: {name: 'Egypt', flag: 'EG'}, 25 | 3: {name: 'Uruguay', flag: 'UY'}, 26 | 4: {name: 'Portugal', flag: 'PT'}, 27 | 5: {name: 'Spain', flag: 'ES'}, 28 | 6: {name: 'Morocco', flag: 'MA'}, 29 | 7: {name: 'Iran', flag: 'IR'}, 30 | 8: {name: 'France', flag: 'FR'}, 31 | 9: {name: 'Australia', flag: 'AU'}, 32 | 10: {name: 'Peru', flag: 'PE'}, 33 | 11: {name: 'Denmark', flag: 'DK'}, 34 | 12: {name: 'Argentina', flag: 'AR'}, 35 | 13: {name: 'Iceland', flag: 'IS'}, 36 | 14: {name: 'Croatia', flag: 'HR'}, 37 | 15: {name: 'Nigeria', flag: 'NG'}, 38 | 16: {name: 'Brazil', flag: 'BR'}, 39 | 17: {name: 'Switzerland', flag: 'CH'}, 40 | 18: {name: 'Costa Rica', flag: 'CR'}, 41 | 19: {name: 'Serbia', flag: 'RS'}, 42 | 20: {name: 'Germany', flag: 'DE'}, 43 | 21: {name: 'Mexico', flag: 'MX'}, 44 | 22: {name: 'Sweden', flag: 'SE'}, 45 | 23: {name: 'South Korea', flag: 'KR'}, 46 | 24: {name: 'Belgium', flag: 'BE'}, 47 | 25: {name: 'Panama', flag: 'PA'}, 48 | 26: {name: 'Tunisia', flag: 'TN'}, 49 | 27: {name: 'England', flag: 'GB'}, 50 | 28: {name: 'Poland', flag: 'PL'}, 51 | 29: {name: 'Senegal', flag: 'SN'}, 52 | 30: {name: 'Colombia', flag: 'CO'}, 53 | 31: {name: 'Japan', flag: 'JP'}, 54 | }; 55 | 56 | const worldCup = Object.keys(groups).reduce((wc, g) => { 57 | const groupCountries = groups[g]; 58 | 59 | const group = { 60 | id: Math.random(), 61 | name: `Group ${g}`, 62 | state: { 63 | expanded: true, 64 | }, 65 | children: groupCountries.map(gc => ({ 66 | id: gc, 67 | name: countries[gc].name, 68 | })), 69 | }; 70 | 71 | return [...wc, group]; 72 | }, []); 73 | 74 | class WorldCupExample extends Component { 75 | state = { 76 | nodes: worldCup, 77 | }; 78 | 79 | handleChange = nodes => { 80 | this.setState({nodes}); 81 | }; 82 | 83 | render() { 84 | return ( 85 | 86 | {({style, node, ...rest}) => { 87 | const country = countries[node.id] && countries[node.id].flag.toLowerCase(); 88 | 89 | return ( 90 |
91 | 92 | {country && } 93 | 99 | {node.name} 100 | 101 | 102 |
103 | ); 104 | }} 105 |
106 | ); 107 | } 108 | } 109 | 110 | export default createEntry( 111 | 'world-cup', 112 | 'WorldCup', 113 | 'World cup groups', 114 |
115 |

116 | FIFA world cup is back in 2018, in this special example the tree view is used to display the group stage draw 117 | results! 118 |

119 |

Let the best team win.

120 |
, 121 | WorldCupExample, 122 | ); 123 | -------------------------------------------------------------------------------- /demo/src/examples/index.js: -------------------------------------------------------------------------------- 1 | import LargeCollection from './LargeCollection'; 2 | import Basic from './Basic'; 3 | import Renderers from './Renderers'; 4 | import WorldCup from './WorldCup'; 5 | import ChangeRenderers from './ChangeRenderers'; 6 | import Extensions from './Extensions'; 7 | import Filterable from './Filterable'; 8 | import NodeMeasure from './NodeMeasure'; 9 | import KeyboardNavigation from './KeyboardNavigation'; 10 | 11 | export default { 12 | ...Basic, 13 | ...Renderers, 14 | ...ChangeRenderers, 15 | ...WorldCup, 16 | ...LargeCollection, 17 | ...Extensions, 18 | ...Filterable, 19 | ...NodeMeasure, 20 | ...KeyboardNavigation, 21 | }; 22 | -------------------------------------------------------------------------------- /demo/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /demo/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | react-virtualized-tree demo 14 | 15 | 16 |
17 | 18 | -------------------------------------------------------------------------------- /demo/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import {HashRouter, Route} from 'react-router-dom'; 4 | 5 | import NavBar from './NavBar'; 6 | import ExamplesContainer from './containers/ExamplesContainer'; 7 | import DocumentsContainer from './containers/DocumentsContainer'; 8 | import Home from './Home'; 9 | 10 | import './index.css'; 11 | import 'react-virtualized/styles.css'; 12 | import 'material-icons/css/material-icons.css'; 13 | import '../../src/main.css'; 14 | 15 | ReactDOM.render( 16 | 17 | 18 | 19 | 20 | 21 | 22 | , 23 | document.getElementById('demo'), 24 | ); 25 | -------------------------------------------------------------------------------- /demo/src/toolbelt.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | const pkg = require('../../package'); 4 | 5 | export const getRepoPath = () => pkg.repository; 6 | 7 | export const getExamplePath = name => `${getRepoPath()}/blob/master/demo/src/examples/${name}.js`; 8 | 9 | export const getDocumentFetchUrl = doc => { 10 | const docPath = path.join(getRepoPath(), 'master', `demo/src/docs/${doc}.md`); 11 | 12 | const url = new URL(docPath); 13 | url.hostname = 'raw.githubusercontent.com'; 14 | 15 | return url.href; 16 | }; 17 | 18 | export const createEntry = (key, fileName, name, description, component) => ({ 19 | [key]: { 20 | name, 21 | fileName, 22 | description, 23 | component, 24 | }, 25 | }); 26 | 27 | let ids = {}; 28 | 29 | const getUniqueId = () => { 30 | const candidateId = Math.round(Math.random() * 1000000000); 31 | 32 | if (ids[candidateId]) { 33 | return getUniqueId(); 34 | } 35 | 36 | ids[candidateId] = true; 37 | 38 | return candidateId; 39 | }; 40 | 41 | export const constructTree = (maxDeepness, maxNumberOfChildren, minNumOfNodes, deepness = 1) => { 42 | return new Array(minNumOfNodes).fill(deepness).map((si, i) => { 43 | const id = getUniqueId(); 44 | const numberOfChildren = deepness === maxDeepness ? 0 : Math.round(Math.random() * maxNumberOfChildren); 45 | 46 | return { 47 | id, 48 | name: `Leaf ${id}`, 49 | children: numberOfChildren ? constructTree(maxDeepness, maxNumberOfChildren, numberOfChildren, deepness + 1) : [], 50 | state: { 51 | expanded: numberOfChildren ? Boolean(Math.round(Math.random())) : false, 52 | favorite: Boolean(Math.round(Math.random())), 53 | deletable: Boolean(Math.round(Math.random())), 54 | }, 55 | }; 56 | }); 57 | }; 58 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for react-virtualized-tree 2 | // Definitions by: Diogo Cunha 3 | 4 | import * as React from 'react'; 5 | 6 | type NodeId = number | string; 7 | 8 | interface BasicNode { 9 | id: NodeId; 10 | name: string; 11 | state?: {[stateKey: string]: any}; 12 | } 13 | 14 | export interface Node extends BasicNode { 15 | children?: BasicNode[]; 16 | } 17 | 18 | export interface FlattenedNode extends Node { 19 | deepness: number; 20 | parents: number[]; 21 | } 22 | 23 | interface NodeAction { 24 | type: string; 25 | node: FlattenedNode; 26 | } 27 | 28 | type onChange = (nodes: Node[], node: Node) => Node[]; 29 | 30 | export interface Extensions { 31 | updateTypeHandlers: {[type: number]: onChange}; 32 | } 33 | 34 | export interface TreeProps { 35 | extensions?: Extensions; 36 | nodes: Node[]; 37 | onChange: (nodes: Node[]) => void; 38 | children: (props: RendererProps) => JSX.Element; 39 | nodeMarginLeft?: number; 40 | width?: number; 41 | scrollToId?: number; 42 | scrollToAlignment?: string; 43 | } 44 | 45 | export default class Tree extends React.Component {} 46 | 47 | export type Omit = Pick>; 48 | 49 | export interface RendererProps { 50 | measure: () => void; 51 | index: number; 52 | onChange: (updateParams: NodeAction) => void; 53 | node: FlattenedNode; 54 | iconsClassNameMap?: T; 55 | style: React.CSSProperties; 56 | children?: React.ReactNode; 57 | } 58 | 59 | export type InjectedRendererProps = Omit, 'iconsClassNameMap'>; 60 | export type CustomRendererProps = Omit, 'style'>; 61 | 62 | type DeletableRenderProps = CustomRendererProps<{delete?: string}>; 63 | 64 | type ExpandableRenderProps = CustomRendererProps<{ 65 | expanded?: string; 66 | collapsed?: string; 67 | lastChild?: string; 68 | }>; 69 | 70 | type FavoriteRenderProps = CustomRendererProps<{ 71 | favorite?: string; 72 | notFavorite?: string; 73 | }>; 74 | 75 | declare const Deletable: React.SFC; 76 | declare const Expandable: React.SFC; 77 | declare const Favorite: React.SFC; 78 | 79 | interface Renderers { 80 | Deletable: React.SFC; 81 | Expandable: React.SFC; 82 | Favorite: React.SFC; 83 | } 84 | 85 | export const renderers: Renderers; 86 | 87 | export interface Group { 88 | filter: (node: Node) => boolean; 89 | name: string; 90 | } 91 | 92 | interface GroupRendererProps { 93 | onChange: (c: string) => void; 94 | groups: {[g: string]: Group}; 95 | selectedGroup: string; 96 | } 97 | 98 | export interface FilteringContainerProps { 99 | nodes: Node[]; 100 | children: (params: {nodes: Node[]; nodeParentMappings: {[id: NodeId]: NodeId[]}}) => JSX.Element; 101 | debouncer?: (func: (...p: any[]) => any, timeout: number) => void; 102 | groups?: {[g: string]: Group}; 103 | selectedGroup?: string; 104 | groupRenderer?: React.StatelessComponent | React.Component; 105 | onSelectedGroupChange?: (c: string) => void; 106 | indexSearch: (searchTerm: string, nodes: FlattenedNode[]) => (node: FlattenedNode) => boolean; 107 | } 108 | 109 | export class FilteringContainer extends React.Component {} 110 | 111 | export enum UPDATE_TYPE { 112 | ADD = 0, 113 | DELETE = 1, 114 | UPDATE = 2, 115 | } 116 | 117 | interface Constants { 118 | UPDATE_TYPE: UPDATE_TYPE; 119 | } 120 | 121 | export const constants: Constants; 122 | 123 | interface NodeRenderOptions { 124 | hasChildren: boolean; 125 | isExpanded: boolean; 126 | isFavorite: boolean; 127 | isDeletable: boolean; 128 | } 129 | 130 | export enum NODE_CHANGE_OPERATIONS { 131 | CHANGE_NODE = 'CHANGE_NODE', 132 | DELETE_NODE = 'DELETE_NODE', 133 | } 134 | 135 | interface Selectors { 136 | getNodeRenderOptions: (node: FlattenedNode) => NodeRenderOptions; 137 | replaceNodeFromTree: (nodes: Node[], updatedNode: FlattenedNode, operation?: NODE_CHANGE_OPERATIONS) => Node[]; 138 | deleteNodeFromTree: (nodes: Node[], nodeToDelete: FlattenedNode) => Node[]; 139 | deleteNode: (node: FlattenedNode[]) => NodeAction; 140 | addNode: (node: FlattenedNode[]) => NodeAction; 141 | updateNode: (node: FlattenedNode, state: {[stateKey: string]: any}) => NodeAction; 142 | } 143 | 144 | interface State { 145 | flattenedTree: Array[]; 146 | tree: Node[]; 147 | } 148 | 149 | export interface TreeState { 150 | getNodeAt: (state: State, index: number) => Node; 151 | getTree: (state: State) => Node[]; 152 | createFromTree: (tree: Node[]) => State; 153 | getNumberOfVisibleDescendants: (state: State, index: number) => number; 154 | getNodeDeepness: (state: State, index: number) => number; 155 | } 156 | 157 | export interface TreeStateModifiers { 158 | editNodeAt: (state: State, index: number, updateNode: ((oldNode: Node) => Node) | Node) => State; 159 | deleteNodeAt: (state: State, index: number) => State; 160 | } 161 | 162 | export const selectors: Selectors; 163 | -------------------------------------------------------------------------------- /nwb.config.js: -------------------------------------------------------------------------------- 1 | const StatsPlugin = require('stats-webpack-plugin'); 2 | 3 | module.exports = { 4 | type: 'react-component', 5 | npm: { 6 | esModules: true, 7 | umd: { 8 | global: 'reactVirtualizedTree', 9 | externals: { 10 | react: 'React', 11 | 'react-dom': 'ReactDOM', 12 | 'react-virtualized': 'ReactVirtualized', 13 | }, 14 | }, 15 | }, 16 | webpack: { 17 | extra: { 18 | plugins: [new StatsPlugin('/stats.json')], 19 | }, 20 | uglify: false, 21 | html: { 22 | template: 'demo/src/index.html', 23 | }, 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-virtualized-tree", 3 | "version": "2.0.2", 4 | "description": "react-virtualized-tree React component", 5 | "main": "lib/index.js", 6 | "module": "es/index.js", 7 | "files": [ 8 | "css", 9 | "es", 10 | "lib", 11 | "umd", 12 | "index.d.ts" 13 | ], 14 | "scripts": { 15 | "prepare": "npm run build", 16 | "prettier": "prettier --write '**/*.js'", 17 | "build": "nwb build-react-component --copy-files", 18 | "clean": "nwb clean-module && nwb clean-demo", 19 | "start": "nwb serve-react-demo", 20 | "test": "node config/test/test.js --env=jsdom", 21 | "test:coverage": "node config/test/test.js --env=jsdom --coverage", 22 | "postpublish": "node config/pages", 23 | "stats:analyzer": "webpack-bundle-analyzer umd/stats.json umd" 24 | }, 25 | "dependencies": { 26 | "classnames": "^2.2.5", 27 | "lodash": "^4.17.4", 28 | "lodash.debounce": "^4.0.8", 29 | "lodash.findindex": "^4.6.0", 30 | "lodash.isequal": "^4.5.0", 31 | "lodash.omit": "^4.5.0", 32 | "material-icons": "^0.1.0", 33 | "react-lifecycles-compat": "^3.0.4", 34 | "reselect": "^3.0.1" 35 | }, 36 | "peerDependencies": { 37 | "react": "16.x", 38 | "react-dom": "^16.2.0", 39 | "react-virtualized": "^9.13.0" 40 | }, 41 | "devDependencies": { 42 | "jest": "^24.8.0", 43 | "babel-jest": "^22.0.4", 44 | "babel-preset-es2015": "^6.24.1", 45 | "babel-preset-react": "^6.24.1", 46 | "babel-preset-react-app": "^3.1.0", 47 | "deep-diff": "^1.0.2", 48 | "deep-freeze": "^0.0.1", 49 | "flag-icon-css": "^2.9.0", 50 | "focus-trap-react": "^5.0.1", 51 | "gh-pages": "^1.1.0", 52 | "husky": "^1.0.0-rc.13", 53 | "immutability-helper": "^2.6.4", 54 | "lint-staged": "^7.2.0", 55 | "nwb": "^0.21.5", 56 | "prettier": "1.13.7", 57 | "prop-types": "^15.6.0", 58 | "react": "^16.3.2", 59 | "react-dnd": "^2.5.4", 60 | "react-dnd-html5-backend": "^2.5.4", 61 | "react-dom": "^16.2.0", 62 | "react-element-to-jsx-string": "^13.1.0", 63 | "react-markdown": "^3.1.4", 64 | "react-router": "^4.2.0", 65 | "react-router-dom": "^4.2.2", 66 | "react-test-renderer": "^16.2.0", 67 | "react-testing-library": "^7.0.0", 68 | "react-virtualized": "^9.18.5", 69 | "semantic-release": "^15.6.3", 70 | "semantic-ui-react": "^0.77.1", 71 | "stats-webpack-plugin": "^0.6.2", 72 | "webpack-bundle-analyzer": "^3.3.2" 73 | }, 74 | "jest": { 75 | "collectCoverageFrom": [ 76 | "src/**/*.{js,jsx,mjs}", 77 | "!src/**/index.js" 78 | ], 79 | "setupFiles": [ 80 | "/src/setupTests.js" 81 | ], 82 | "testMatch": [ 83 | "/src/**/__tests__/**/*.{js,jsx,mjs}", 84 | "/src/**/?(*.)(spec|test).{js,jsx,mjs}" 85 | ], 86 | "testEnvironment": "node", 87 | "testURL": "http://localhost", 88 | "transform": { 89 | "^.+\\.(js|jsx|mjs)$": "/node_modules/babel-jest", 90 | "^.+\\.css$": "/config/test/cssTransform.js", 91 | "^(?!.*\\.(js|jsx|mjs|css|json)$)": "/config/test/fileTransform.js" 92 | }, 93 | "transformIgnorePatterns": [ 94 | "[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs)$" 95 | ] 96 | }, 97 | "babel": { 98 | "presets": [ 99 | "react-app" 100 | ] 101 | }, 102 | "husky": { 103 | "hooks": { 104 | "pre-commit": "lint-staged" 105 | } 106 | }, 107 | "lint-staged": { 108 | "**/*.js": [ 109 | "prettier --write", 110 | "git add" 111 | ] 112 | }, 113 | "prettier": { 114 | "printWidth": 120, 115 | "tabWidth": 2, 116 | "useTabs": false, 117 | "semi": true, 118 | "singleQuote": true, 119 | "trailingComma": "all", 120 | "bracketSpacing": false, 121 | "jsxBracketSameLine": false 122 | }, 123 | "author": "Diogo Cunha", 124 | "homepage": "https://diogofcunha.github.io/react-virtualized-tree/", 125 | "license": "MIT", 126 | "repository": "https://github.com/diogofcunha/react-virtualized-tree/", 127 | "keywords": [ 128 | "react", 129 | "tree", 130 | "view", 131 | "react-virtualized", 132 | "react-tree", 133 | "tree-view", 134 | "foder-structure", 135 | "react-virtualized-tree", 136 | "react-tree-view", 137 | "react-component" 138 | ] 139 | } 140 | -------------------------------------------------------------------------------- /src/FilteringContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import debounce from 'lodash.debounce'; 4 | import classNames from 'classnames'; 5 | 6 | import DefaultGroupRenderer from './filtering/DefaultGroupRenderer'; 7 | import {Node} from './shapes/nodeShapes'; 8 | import {filterNodes} from './selectors/filtering'; 9 | 10 | const indexByName = searchTerm => ({name}) => { 11 | const upperCaseName = name.toUpperCase(); 12 | const upperCaseSearchTerm = searchTerm.toUpperCase(); 13 | 14 | return upperCaseName.indexOf(upperCaseSearchTerm.trim()) > -1; 15 | }; 16 | 17 | export default class FilteringContainer extends React.Component { 18 | state = { 19 | filterText: '', 20 | filterTerm: '', 21 | }; 22 | 23 | getChildContext = () => { 24 | return {unfilteredNodes: this.props.nodes}; 25 | }; 26 | 27 | static childContextTypes = { 28 | unfilteredNodes: PropTypes.arrayOf(PropTypes.shape(Node)).isRequired, 29 | }; 30 | 31 | static defaultProps = { 32 | debouncer: debounce, 33 | groupRenderer: DefaultGroupRenderer, 34 | indexSearch: indexByName, 35 | }; 36 | 37 | constructor(props) { 38 | super(props); 39 | 40 | this.setFilterTerm = props.debouncer(this.setFilterTerm, 300); 41 | } 42 | 43 | setFilterTerm() { 44 | this.setState(ps => ({filterTerm: ps.filterText})); 45 | } 46 | 47 | handleFilterTextChange = e => { 48 | const filterText = e.target.value; 49 | 50 | this.setState({filterText}); 51 | 52 | this.setFilterTerm(); 53 | }; 54 | 55 | render() { 56 | const {filterTerm, filterText} = this.state; 57 | const { 58 | nodes, 59 | children: treeRenderer, 60 | groups, 61 | selectedGroup, 62 | groupRenderer: GroupRenderer, 63 | onSelectedGroupChange, 64 | indexSearch, 65 | } = this.props; 66 | 67 | const relevantNodes = 68 | groups && selectedGroup && groups[selectedGroup] 69 | ? filterNodes(groups[selectedGroup].filter, nodes) 70 | : {nodes, nodeParentMappings: {}}; 71 | 72 | const {nodes: filteredNodes, nodeParentMappings} = filterTerm 73 | ? filterNodes(indexSearch(filterTerm, relevantNodes.nodes), relevantNodes.nodes) 74 | : relevantNodes; 75 | 76 | return ( 77 |
78 |
79 | 80 |