UNPKG

react-infinite-tree

Version:
337 lines (278 loc) 10.4 kB
import PropTypes from 'prop-types'; import React, { Component } from 'react'; import InfiniteTree from 'infinite-tree'; import VirtualList from 'react-tiny-virtual-list'; const lcfirst = (str) => { str += ''; return str.charAt(0).toLowerCase() + str.substr(1); }; export default class extends Component { static displayName = 'InfiniteTree'; static propTypes = { // Whether to open all nodes when tree is loaded. autoOpen: PropTypes.bool, // Whether or not a node is selectable in the tree. selectable: PropTypes.bool, // Specifies the tab order to make tree focusable. tabIndex: PropTypes.number, // Tree data structure, or a collection of tree data structures. data: PropTypes.oneOfType([ PropTypes.array, PropTypes.object ]), // Width of the tree. width: PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]).isRequired, // Height of the tree. height: PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]).isRequired, // Either a fixed height, an array containing the heights of all the rows, or a function that returns the height of a row given its index: `(index: number): number` rowHeight: PropTypes.oneOfType([ PropTypes.number, PropTypes.array, PropTypes.func ]).isRequired, // A row renderer for rendering a tree node. rowRenderer: PropTypes.func, // Loads nodes on demand. loadNodes: PropTypes.func, // Provides a function to determine if a node can be selected or deselected. The function must return `true` or `false`. This function will not take effect if `selectable` is not `true`. shouldSelectNode: PropTypes.func, // Controls the scroll offset. scrollOffset: PropTypes.number, // Node index to scroll to. scrollToIndex: PropTypes.number, // Callback invoked whenever the scroll offset changes. onScroll: PropTypes.func, // Callback invoked before updating the tree. onContentWillUpdate: PropTypes.func, // Callback invoked when the tree is updated. onContentDidUpdate: PropTypes.func, // Callback invoked when a node is opened. onOpenNode: PropTypes.func, // Callback invoked when a node is closed. onCloseNode: PropTypes.func, // Callback invoked when a node is selected or deselected. onSelectNode: PropTypes.func, // Callback invoked before opening a node. onWillOpenNode: PropTypes.func, // Callback invoked before closing a node. onWillCloseNode: PropTypes.func, // Callback invoked before selecting or deselecting a node. onWillSelectNode: PropTypes.func }; static defaultProps = { autoOpen: false, selectable: true, tabIndex: 0, data: [], width: '100%' }; tree = null; virtualListRef = React.createRef(); state = { nodes: [] }; eventHandlers = { onContentWillUpdate: null, onContentDidUpdate: null, onOpenNode: null, onCloseNode: null, onSelectNode: null, onWillOpenNode: null, onWillCloseNode: null, onWillSelectNode: null }; constructor(props) { super(props); const { children, // eslint-disable-line className, // eslint-disable-line style, // eslint-disable-line el, // elint-disable-line ...options } = props; options.rowRenderer = () => ''; this.tree = new InfiniteTree(options); // Filters nodes. // https://github.com/cheton/infinite-tree/wiki/Functions:-Tree#filterpredicate-options const treeFilter = this.tree.filter.bind(this.tree); this.tree.filter = (...args) => { setTimeout(() => { const virtualList = this.virtualListRef.current; if (virtualList) { virtualList.recomputeSizes(0); } }, 0); return treeFilter(...args); }; // Unfilter nodes. // https://github.com/cheton/infinite-tree/wiki/Functions:-Tree#unfilter const treeUnfilter = this.tree.unfilter.bind(this.tree); this.tree.unfilter = (...args) => { setTimeout(() => { const virtualList = this.virtualListRef.current; if (virtualList) { virtualList.recomputeSizes(0); } }, 0); return treeUnfilter(...args); }; // Sets the current scroll position to this node. // @param {Node} node The Node object. // @return {boolean} Returns true on success, false otherwise. this.tree.scrollToNode = (node) => { const virtualList = this.virtualListRef.current; if (!this.tree || !virtualList) { return false; } const nodeIndex = this.tree.nodes.indexOf(node); if (nodeIndex < 0) { return false; } const offset = virtualList.getOffsetForIndex(nodeIndex); virtualList.scrollTo(offset); return true; }; // Gets (or sets) the current vertical position of the scroll bar. // @param {number} [value] If the value is specified, indicates the new position to set the scroll bar to. // @return {number} Returns the vertical scroll position. this.tree.scrollTop = (value) => { const virtualList = this.virtualListRef.current; if (!this.tree || !virtualList) { return; } if (value !== undefined) { virtualList.scrollTo(Number(value)); } return virtualList.getNodeOffset(); }; // Updates the tree. this.tree.update = () => { this.tree.emit('contentWillUpdate'); this.setState(state => ({ nodes: this.tree.nodes }), () => { this.tree.emit('contentDidUpdate'); }); }; Object.keys(this.eventHandlers).forEach(key => { if (!this.props[key]) { return; } const eventName = lcfirst(key.substr(2)); // e.g. onContentWillUpdate -> contentWillUpdate this.eventHandlers[key] = this.props[key]; this.tree.on(eventName, this.eventHandlers[key]); }); } componentWillUnmount() { Object.keys(this.eventHandlers).forEach(key => { if (!this.eventHandlers[key]) { return; } const eventName = lcfirst(key.substr(2)); // e.g. onUpdate -> update this.tree.removeListener(eventName, this.eventHandlers[key]); this.eventHandlers[key] = null; }); this.tree.destroy(); this.tree = null; } render() { const { autoOpen, selectable, tabIndex, data, width, height, rowHeight, rowRenderer, shouldLoadNodes, loadNodes, shouldSelectNode, scrollOffset, scrollToIndex, onScroll, onContentWillUpdate, onContentDidUpdate, onOpenNode, onCloseNode, onSelectNode, onWillOpenNode, onWillCloseNode, onWillSelectNode, style, children, ...props } = this.props; const render = (typeof children === 'function') ? children : rowRenderer; const count = this.tree ? this.tree.nodes.length : 0; // VirtualList const virtualListProps = {}; if ((scrollOffset !== undefined) && (count > 0)) { virtualListProps.scrollOffset = scrollOffset; } if ((scrollToIndex !== undefined) && (scrollToIndex >= 0) && (scrollToIndex < count)) { virtualListProps.scrollToIndex = scrollToIndex; } if (typeof onScroll === 'function') { virtualListProps.onScroll = onScroll; } return ( <div {...props} style={{ outline: 'none', ...style }} tabIndex={tabIndex} > <VirtualList ref={this.virtualListRef} width={width} height={height} itemCount={count} itemSize={(index) => { const node = this.tree.nodes[index]; if (node && node.state.filtered === false) { return 0; } if (typeof rowHeight === 'function') { return rowHeight({ node: this.tree.nodes[index], tree: this.tree }); } return rowHeight; // Number or Array }} renderItem={({ index, style }) => { let row = null; if (typeof render === 'function') { const node = this.tree.nodes[index]; if (node && node.state.filtered !== false) { row = render({ node: this.tree.nodes[index], tree: this.tree }); } } return ( <div key={index} style={style}> {row} </div> ); }} {...virtualListProps} /> </div> ); } };