react-virtualized-sticky-tree
Advanced tools
@@ -36,7 +36,8 @@ 'use strict'; | ||
_this.state = { | ||
scrollTop: 0, | ||
currNodePos: 0 | ||
}; | ||
_this.scrollTop = 0; | ||
_this.nodePosCache = []; | ||
_this.rowRenderRange = undefined; | ||
return _this; | ||
@@ -78,3 +79,2 @@ } | ||
nodeInfo.children = []; | ||
for (var i = 0; i < children.length; i++) { | ||
@@ -112,2 +112,5 @@ // Need to reset parentIndex here as we are recursive. | ||
} | ||
if (newProps.scrollTop !== this.scrollTop) { | ||
this.elem.scrollTop = newProps.scrollTop; | ||
} | ||
} | ||
@@ -118,2 +121,22 @@ | ||
}, { | ||
key: 'componentDidUpdate', | ||
value: function () { | ||
function componentDidUpdate(prevProps, prevState) { | ||
if (this.props.onRowsRendered !== undefined && prevState.currNodePos !== this.props.currNodePos) { | ||
var range = this.rowRenderRange; | ||
this.props.onRowsRendered({ | ||
overscanStartIndex: range.start, | ||
overscanStopIndex: range.end, | ||
startIndex: range.visibleStart, | ||
stopIndex: range.visibleEnd, | ||
startNode: this.nodePosCache[range.visibleStart].node, | ||
endNode: this.nodePosCache[range.visibleEnd].node | ||
}); | ||
} | ||
} | ||
return componentDidUpdate; | ||
}() | ||
}, { | ||
key: 'getChildContainerStyle', | ||
@@ -131,4 +154,4 @@ value: function () { | ||
function renderParentTree() { | ||
var rowRenderRange = this.getRenderRowRange(); | ||
var path = this.getParentPath(rowRenderRange.start); | ||
this.rowRenderRange = this.getRenderRowRange(); | ||
var path = this.getParentPath(this.rowRenderRange.start); | ||
@@ -142,3 +165,3 @@ // Parent nodes to the current range. | ||
// The rest of the nodes within the range. | ||
for (var _i = rowRenderRange.start; _i <= rowRenderRange.end; _i++) { | ||
for (var _i = this.rowRenderRange.start; _i <= this.rowRenderRange.end; _i++) { | ||
indexesToRender.add(this.nodePosCache[_i].index); | ||
@@ -233,9 +256,9 @@ } | ||
} | ||
var end = this.state.currNodePos + 1; | ||
var visibleEnd = this.state.currNodePos + 1; | ||
while (this.nodePosCache[end] && this.nodePosCache[end].top < this.state.scrollTop + this.props.height) { | ||
end++; | ||
while (this.nodePosCache[visibleEnd] && this.nodePosCache[visibleEnd].top < this.scrollTop + this.props.height) { | ||
visibleEnd++; | ||
} | ||
end = end + this.props.overscanRowCount; | ||
var end = visibleEnd + this.props.overscanRowCount; | ||
if (end > this.nodePosCache.length - 1) { | ||
@@ -245,3 +268,3 @@ end = this.nodePosCache.length - 1; | ||
return { start: start, end: end }; | ||
return { start: start, end: end, visibleStart: this.state.currNodePos, visibleEnd: visibleEnd }; | ||
} | ||
@@ -316,5 +339,5 @@ | ||
var pos = void 0; | ||
if (scrollTop > this.state.scrollTop) { | ||
if (scrollTop > this.scrollTop) { | ||
pos = this.forwardSearch(scrollTop); | ||
} else if (scrollTop < this.state.scrollTop) { | ||
} else if (scrollTop < this.scrollTop) { | ||
pos = this.backwardSearch(scrollTop); | ||
@@ -335,3 +358,7 @@ } | ||
this.findClosestNode(scrollTop); | ||
this.setState({ scrollTop: scrollTop }); | ||
this.scrollTop = scrollTop; | ||
if (this.props.onScroll !== undefined) { | ||
this.props.onScroll({ scrollTop: this.scrollTop }); | ||
} | ||
} | ||
@@ -361,5 +388,13 @@ | ||
function render() { | ||
var _this3 = this; | ||
return _react2['default'].createElement( | ||
'div', | ||
{ className: 'rv-sticky-tree', style: this.getStyle(), onScroll: this.onScroll }, | ||
{ ref: function () { | ||
function ref(elem) { | ||
return _this3.elem = elem; | ||
} | ||
return ref; | ||
}(), className: 'rv-sticky-tree', style: this.getStyle(), onScroll: this.onScroll }, | ||
this.renderParentTree() | ||
@@ -384,3 +419,13 @@ ); | ||
width: _propTypes2['default'].number, | ||
renderRoot: _propTypes2['default'].bool | ||
renderRoot: _propTypes2['default'].bool, | ||
/** | ||
* Called whenever the scrollTop position changes. | ||
*/ | ||
onScroll: _propTypes2['default'].func, | ||
/** | ||
* Called to indicate that a new render range for rows has been rendered. | ||
*/ | ||
onRowsRendered: _propTypes2['default'].func | ||
}; | ||
@@ -387,0 +432,0 @@ StickyTree.defaultProps = { |
@@ -36,7 +36,8 @@ 'use strict'; | ||
_this.state = { | ||
scrollTop: 0, | ||
currNodePos: 0 | ||
}; | ||
_this.scrollTop = 0; | ||
_this.nodePosCache = []; | ||
_this.rowRenderRange = undefined; | ||
return _this; | ||
@@ -78,3 +79,2 @@ } | ||
nodeInfo.children = []; | ||
for (var i = 0; i < children.length; i++) { | ||
@@ -112,2 +112,5 @@ // Need to reset parentIndex here as we are recursive. | ||
} | ||
if (newProps.scrollTop !== this.scrollTop) { | ||
this.elem.scrollTop = newProps.scrollTop; | ||
} | ||
} | ||
@@ -118,2 +121,22 @@ | ||
}, { | ||
key: 'componentDidUpdate', | ||
value: function () { | ||
function componentDidUpdate(prevProps, prevState) { | ||
if (this.props.onRowsRendered !== undefined && prevState.currNodePos !== this.props.currNodePos) { | ||
var range = this.rowRenderRange; | ||
this.props.onRowsRendered({ | ||
overscanStartIndex: range.start, | ||
overscanStopIndex: range.end, | ||
startIndex: range.visibleStart, | ||
stopIndex: range.visibleEnd, | ||
startNode: this.nodePosCache[range.visibleStart].node, | ||
endNode: this.nodePosCache[range.visibleEnd].node | ||
}); | ||
} | ||
} | ||
return componentDidUpdate; | ||
}() | ||
}, { | ||
key: 'getChildContainerStyle', | ||
@@ -131,4 +154,4 @@ value: function () { | ||
function renderParentTree() { | ||
var rowRenderRange = this.getRenderRowRange(); | ||
var path = this.getParentPath(rowRenderRange.start); | ||
this.rowRenderRange = this.getRenderRowRange(); | ||
var path = this.getParentPath(this.rowRenderRange.start); | ||
@@ -142,3 +165,3 @@ // Parent nodes to the current range. | ||
// The rest of the nodes within the range. | ||
for (var _i = rowRenderRange.start; _i <= rowRenderRange.end; _i++) { | ||
for (var _i = this.rowRenderRange.start; _i <= this.rowRenderRange.end; _i++) { | ||
indexesToRender.add(this.nodePosCache[_i].index); | ||
@@ -233,9 +256,9 @@ } | ||
} | ||
var end = this.state.currNodePos + 1; | ||
var visibleEnd = this.state.currNodePos + 1; | ||
while (this.nodePosCache[end] && this.nodePosCache[end].top < this.state.scrollTop + this.props.height) { | ||
end++; | ||
while (this.nodePosCache[visibleEnd] && this.nodePosCache[visibleEnd].top < this.scrollTop + this.props.height) { | ||
visibleEnd++; | ||
} | ||
end = end + this.props.overscanRowCount; | ||
var end = visibleEnd + this.props.overscanRowCount; | ||
if (end > this.nodePosCache.length - 1) { | ||
@@ -245,3 +268,3 @@ end = this.nodePosCache.length - 1; | ||
return { start: start, end: end }; | ||
return { start: start, end: end, visibleStart: this.state.currNodePos, visibleEnd: visibleEnd }; | ||
} | ||
@@ -316,5 +339,5 @@ | ||
var pos = void 0; | ||
if (scrollTop > this.state.scrollTop) { | ||
if (scrollTop > this.scrollTop) { | ||
pos = this.forwardSearch(scrollTop); | ||
} else if (scrollTop < this.state.scrollTop) { | ||
} else if (scrollTop < this.scrollTop) { | ||
pos = this.backwardSearch(scrollTop); | ||
@@ -335,3 +358,7 @@ } | ||
this.findClosestNode(scrollTop); | ||
this.setState({ scrollTop: scrollTop }); | ||
this.scrollTop = scrollTop; | ||
if (this.props.onScroll !== undefined) { | ||
this.props.onScroll({ scrollTop: this.scrollTop }); | ||
} | ||
} | ||
@@ -361,5 +388,13 @@ | ||
function render() { | ||
var _this3 = this; | ||
return _react2['default'].createElement( | ||
'div', | ||
{ className: 'rv-sticky-tree', style: this.getStyle(), onScroll: this.onScroll }, | ||
{ ref: function () { | ||
function ref(elem) { | ||
return _this3.elem = elem; | ||
} | ||
return ref; | ||
}(), className: 'rv-sticky-tree', style: this.getStyle(), onScroll: this.onScroll }, | ||
this.renderParentTree() | ||
@@ -384,3 +419,13 @@ ); | ||
width: _propTypes2['default'].number, | ||
renderRoot: _propTypes2['default'].bool | ||
renderRoot: _propTypes2['default'].bool, | ||
/** | ||
* Called whenever the scrollTop position changes. | ||
*/ | ||
onScroll: _propTypes2['default'].func, | ||
/** | ||
* Called to indicate that a new render range for rows has been rendered. | ||
*/ | ||
onRowsRendered: _propTypes2['default'].func | ||
}; | ||
@@ -387,0 +432,0 @@ StickyTree.defaultProps = { |
{ | ||
"name": "react-virtualized-sticky-tree", | ||
"description": "A React component for efficiently rendering tree like structures with support for position: sticky", | ||
"version": "1.0.2", | ||
"version": "1.0.3", | ||
"author": "Marc McIntyre <marchaos@gmail.com>", | ||
@@ -6,0 +6,0 @@ "license": "MIT", |
# react-virtualized-sticky-tree | ||
A React component for efficiently rendering tree like structures with support for position: sticky. `react-virtualized-sticky-tree` uses a similar API to [react-virtualized](https://github.com/bvaughn/react-virtualized). | ||
## Demo | ||
https://marchaos.github.io/react-virtualized-sticky-tree/ | ||
## Getting Started | ||
@@ -37,2 +41,4 @@ | ||
root="root" | ||
width={width} | ||
height={height} | ||
getChildren={getChildren} | ||
@@ -47,2 +53,22 @@ getHeight={getHeight} | ||
## Nested Sticky Header Styles | ||
sticky header components are rendered directly via your rowRenderer() function where styles are used to make the component sticky. StickyTree renders the component within a nested structure so that the header's position may be 'stuck' at different levels (see [demo](https://marchaos.github.io/react-virtualized-sticky-tree/)). | ||
Every nested sticky level should have a top which is at the bottom of the sticky level above it. For example. If your root node is 30px high and has a top of 0, the next sticky node should have a top of 30px. The z-index of the node should also be lower than the nodes above it (so that it is scrolled out of view underneath its parent node). If your root node is z-index 4, then the node below could be 3, below that 2 and so on. | ||
An implementation of this would look like: | ||
```js | ||
const rowRenderer = (id) => { | ||
let style = {}; | ||
if (nodeShouldBeSticky(id)) { | ||
const depth = mytree[id].depth; | ||
const nodeHeight = 30; | ||
style = { position: 'sticky', top: depth * nodeHeight, zIndex: 4 - depth }; | ||
} | ||
return <div className="row" style={style}>{mytree[id].name}</div>; | ||
}; | ||
``` | ||
## Dynamic Height Container | ||
@@ -54,2 +80,3 @@ | ||
as a HOC: | ||
```js | ||
@@ -71,5 +98,28 @@ const MeasuredTree = withContentRect('bounds')(({ measureRef, measure, contentRect }) => ( | ||
``` | ||
or within render() | ||
```js | ||
<Measure | ||
bounds={true} | ||
onResize={(contentRect) => {this.setState({ dimensions: contentRect.bounds });}} | ||
> | ||
{({ measureRef }) => | ||
<div ref={measureRef} className="sticky-tree-wrapper"> | ||
<StickyTree | ||
width={this.state.dimensions.width} | ||
height={this.state.dimensions.height} | ||
root={0} | ||
renderRoot={true} | ||
rowRenderer={this.rowRenderer} | ||
getChildren={this.getChildren} | ||
getHeight={() => 30} | ||
overscanRowCount={20} | ||
/> | ||
</div> | ||
} | ||
</Measure> | ||
``` | ||
## Supported Browsers | ||
Rendering tree structures is supported in all modern browsers. position: sticky has only been tested in Chrome 59 and Firefox 54, but should work in Edge, Safari and Opera. See http://caniuse.com/#search=position%3Asticky |
38553
16.56%740
11.78%122
69.44%