Compare commits

...

9 Commits

4 changed files with 57 additions and 18 deletions

View File

@ -62,7 +62,7 @@ export function virtualScrollDriver(props, oldState, getRenderedItemHeight)
if (!lastItemSize)
{
// Some required items in the end are missing
return newState;
lastItemSize = 0;
}
lastItemsHeight += lastItemSize < props.minRowHeight ? props.minRowHeight : lastItemSize;
lastVisibleItems++;

View File

@ -5,12 +5,12 @@ of visible items and skip items that are offscreen. You may also have heard abou
"buffered render" or "windowed render" - it's the same.
There are plenty of virtual scroll implementations for JS.
Some of them are part of a larger UI library (ag-grid, ExtJS and so on),
some of them are more standalone (react-virtualized, react-window, ngx-virtual-scroller, react-dynamic-virtual-list).
Some of them are part of a larger UI library (ag-grid, ExtJS and so on), some of them are more
standalone (react-virtualized, react-window, ngx-virtual-scroller, ngx-ui-scroll, react-dynamic-virtual-list).
However, there is a thing that they all miss: dynamic (and unknown apriori) row heights.
Some implementations allow to set different row heights for items, but you must calculate
all heights before rendering; some allow dynamic row heights, but have bugs and do not really work;
all heights before rendering; some allow dynamic row heights, but have bugs and act weird or don't really work;
others just force you to use fixed row height. Most implementations are also tied to some specific
UI component or framework and are unusable with other ones.

View File

@ -5,6 +5,7 @@
*/
import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import { virtualScrollDriver } from 'dynamic-virtual-scroll';
@ -33,16 +34,40 @@ export class VirtualScrollList extends React.Component
scrollTimes: 0,
}
setItemRef = []
itemRefs = []
itemRefCount = []
makeRef(i)
{
this.setItemRef[i] = (e) =>
{
// If the new row instance is mounted before unmouting the old one,
// we get called 2 times in wrong order: first with the new instance,
// then with null telling us that the old one is unmounted.
// We track reference count to workaround it.
this.itemRefCount[i] = (this.itemRefCount[i]||0) + (e ? 1 : -1);
if (e || !this.itemRefCount[i])
{
this.itemRefs[i] = e;
}
};
}
renderItems(start, count, is_end)
{
let r = [];
for (let i = 0; i < count; i++)
{
r.push(
<div data-item={i+start} key={i+start}>
{this.props.renderItem(i+start)}
</div>
);
let item = this.props.renderItem(i+start);
if (item)
{
if (!this.setItemRef[i+start])
{
this.makeRef(i+start);
}
r.push(<item.type {...item.props} key={i+start} ref={this.setItemRef[i+start]} />);
}
}
return r;
}
@ -50,9 +75,10 @@ export class VirtualScrollList extends React.Component
render()
{
if (this.state.totalItems && this.props.totalItems != this.state.totalItems &&
this.state.scrollTimes <= 0)
this.state.scrollTimes <= 0 && this.viewport && this.viewport.offsetParent)
{
// Automatically preserve scroll position when item count changes
// Automatically preserve scroll position when item count changes...
// But only when the list is on-screen! We'll end up with an infinite update loop if it's off-screen.
this.state.scrollTo = this.getItemScrollPos();
this.state.scrollTimes = 2;
}
@ -73,7 +99,7 @@ export class VirtualScrollList extends React.Component
onScroll={this.onScroll}>
{this.props.header}
{this.state.targetHeight > 0
? <div key="target" style={{position: 'absolute', left: '-5px', width: '1px', height: this.state.targetHeight+'px'}}></div>
? <div key="target" style={{position: 'absolute', top: 0, left: '-5px', width: '1px', height: this.state.targetHeight+'px'}}></div>
: null}
{this.state.topPlaceholderHeight
? <div style={{height: this.state.topPlaceholderHeight+'px'}} key="top"></div>
@ -93,12 +119,15 @@ export class VirtualScrollList extends React.Component
getRenderedItemHeight = (index) =>
{
if (this.viewport)
if (this.itemRefs[index])
{
const e = this.viewport.querySelector('div[data-item="'+index+'"]');
const e = ReactDOM.findDOMNode(this.itemRefs[index]);
if (e)
{
return e.getBoundingClientRect().height;
// MSIE sometimes manages to report non-integer element heights for elements of an integer height...
// Non-integer element sizes are allowed in getBoundingClientRect, one notable example of them
// are collapsed table borders. But we still ignore less than 1/100 of a pixel difference.
return Math.round(e.getBoundingClientRect().height*100)/100;
}
}
return 0;
@ -107,12 +136,17 @@ export class VirtualScrollList extends React.Component
onScroll = () =>
{
this.driver();
if (this.props.onScroll)
{
this.props.onScroll(this.viewport);
}
}
componentDidUpdate = () =>
{
let changed = this.driver();
if (!changed && this.state.scrollTimes > 0 && this.props.totalItems > 0)
if (!changed && this.state.scrollTimes > 0 && this.props.totalItems > 0 &&
this.viewport && this.viewport.offsetParent)
{
// FIXME: It would be better to find a way to put this logic back into virtual-scroll-driver
let pos = this.state.scrollTo;
@ -127,7 +161,7 @@ export class VirtualScrollList extends React.Component
}
else
{
const el = this.viewport.querySelector('div[data-item="'+Math.floor(pos)+'"]');
const el = ReactDOM.findDOMNode(this.itemRefs[Math.floor(pos)]);
if (el)
{
this.viewport.scrollTop = el.offsetTop - (this.props.headerHeight||0) + el.offsetHeight*(pos-Math.floor(pos));
@ -168,6 +202,11 @@ export class VirtualScrollList extends React.Component
driver = () =>
{
if (!this.viewport || !this.viewport.offsetParent)
{
// Fool tolerance - do nothing if we are hidden
return false;
}
const newState = virtualScrollDriver(
{
totalItems: this.props.totalItems,

View File

@ -1,6 +1,6 @@
{
"name": "dynamic-virtual-scroll",
"version": "1.0.7",
"version": "1.0.15",
"author": {
"name": "Vitaliy Filippov",
"email": "vitalif@yourcmc.ru",