react-infinite-tree
Version:
The infinite-tree library for React.
337 lines (278 loc) • 10.4 kB
JSX
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>
);
}
};