Compare commits
No commits in common. "master" and "very-old" have entirely different histories.
42
.eslintrc.js
42
.eslintrc.js
|
@ -1,42 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
"parser": "babel-eslint",
|
|
||||||
"env": {
|
|
||||||
"es6": true,
|
|
||||||
"browser": true
|
|
||||||
},
|
|
||||||
"extends": [
|
|
||||||
"eslint:recommended",
|
|
||||||
"plugin:react/recommended"
|
|
||||||
],
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaVersion": 6,
|
|
||||||
"sourceType": "module",
|
|
||||||
"ecmaFeatures": {
|
|
||||||
"experimentalObjectRestSpread": true,
|
|
||||||
"jsx": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"plugins": [
|
|
||||||
"react"
|
|
||||||
],
|
|
||||||
"rules": {
|
|
||||||
"indent": [
|
|
||||||
"error",
|
|
||||||
4
|
|
||||||
],
|
|
||||||
"linebreak-style": [
|
|
||||||
"error",
|
|
||||||
"unix"
|
|
||||||
],
|
|
||||||
"semi": [
|
|
||||||
"error",
|
|
||||||
"always"
|
|
||||||
],
|
|
||||||
"no-control-regex": [
|
|
||||||
"off"
|
|
||||||
],
|
|
||||||
"no-empty": [
|
|
||||||
"off"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,151 +0,0 @@
|
||||||
/**
|
|
||||||
* Virtual scroll driver for dynamic row heights
|
|
||||||
*
|
|
||||||
* License: GNU LGPLv3.0+
|
|
||||||
* (c) Vitaliy Filippov 2018+
|
|
||||||
*
|
|
||||||
* @param props { totalItems, minRowHeight, viewportHeight, scrollTop }
|
|
||||||
* @param oldState - previous state object
|
|
||||||
* @param getRenderedItemHeight = (itemIndex) => height
|
|
||||||
* this function MUST return the height of currently rendered item or 0 if it's not currently rendered
|
|
||||||
* the returned height MUST be >= props.minRowHeight
|
|
||||||
* the function MAY cache heights of rendered items if you want your list to be more responsive
|
|
||||||
* @returns new state object
|
|
||||||
* you MUST re-render your list when any state values change
|
|
||||||
* you MUST preserve all keys in the state object and pass it back via `oldState` on the next run
|
|
||||||
* you MUST use the following keys for rendering:
|
|
||||||
* newState.targetHeight - height of the 1px wide invisible div you should render in the scroll container
|
|
||||||
* newState.topPlaceholderHeight - height of the first (top) placeholder. omit placeholder if it is 0
|
|
||||||
* newState.firstMiddleItem - first item to be rendered after top placeholder
|
|
||||||
* newState.middleItemCount - item count to be renderer after top placeholder. omit items if it is 0
|
|
||||||
* newState.middlePlaceholderHeight - height of the second (middle) placeholder. omit placeholder if it is 0
|
|
||||||
* newState.lastItemCount - item count to be rendered in the end of the list
|
|
||||||
*/
|
|
||||||
export function virtualScrollDriver(props, oldState, getRenderedItemHeight)
|
|
||||||
{
|
|
||||||
const viewportHeight = props.viewportHeight;
|
|
||||||
const viewportItemCount = Math.ceil(viewportHeight/props.minRowHeight); // +border?
|
|
||||||
const newState = {
|
|
||||||
viewportHeight,
|
|
||||||
viewportItemCount,
|
|
||||||
totalItems: props.totalItems,
|
|
||||||
scrollHeightInItems: oldState.scrollHeightInItems,
|
|
||||||
avgRowHeight: oldState.avgRowHeight,
|
|
||||||
targetHeight: 0,
|
|
||||||
topPlaceholderHeight: 0,
|
|
||||||
firstMiddleItem: 0,
|
|
||||||
middleItemCount: 0,
|
|
||||||
middlePlaceholderHeight: 0,
|
|
||||||
lastItemCount: props.totalItems,
|
|
||||||
lastItemsTotalHeight: oldState.lastItemsTotalHeight,
|
|
||||||
};
|
|
||||||
if (!oldState.viewportHeight)
|
|
||||||
{
|
|
||||||
oldState = { ...oldState };
|
|
||||||
for (let k in newState)
|
|
||||||
{
|
|
||||||
oldState[k] = oldState[k] || 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (2*newState.viewportItemCount >= props.totalItems)
|
|
||||||
{
|
|
||||||
// We need at least 2*viewportItemCount to perform virtual scrolling
|
|
||||||
return newState;
|
|
||||||
}
|
|
||||||
newState.lastItemCount = newState.viewportItemCount;
|
|
||||||
{
|
|
||||||
let lastItemsHeight = 0, lastVisibleItems = 0;
|
|
||||||
let lastItemSize;
|
|
||||||
while (lastItemsHeight < viewportHeight)
|
|
||||||
{
|
|
||||||
lastItemSize = getRenderedItemHeight(props.totalItems - 1 - lastVisibleItems);
|
|
||||||
if (!lastItemSize)
|
|
||||||
{
|
|
||||||
// Some required items in the end are missing
|
|
||||||
lastItemSize = 0;
|
|
||||||
}
|
|
||||||
lastItemsHeight += lastItemSize < props.minRowHeight ? props.minRowHeight : lastItemSize;
|
|
||||||
lastVisibleItems++;
|
|
||||||
}
|
|
||||||
newState.scrollHeightInItems = props.totalItems - lastVisibleItems + (lastItemsHeight-viewportHeight) / lastItemSize;
|
|
||||||
// Calculate heights of the rest of items
|
|
||||||
while (lastVisibleItems < newState.viewportItemCount)
|
|
||||||
{
|
|
||||||
lastItemsHeight += getRenderedItemHeight(props.totalItems - 1 - lastVisibleItems);
|
|
||||||
lastVisibleItems++;
|
|
||||||
}
|
|
||||||
newState.lastItemsTotalHeight = lastItemsHeight;
|
|
||||||
newState.avgRowHeight = lastItemsHeight / lastVisibleItems;
|
|
||||||
newState.avgRowHeight = !oldState.avgRowHeight || newState.avgRowHeight > oldState.avgRowHeight
|
|
||||||
? newState.avgRowHeight
|
|
||||||
: oldState.avgRowHeight;
|
|
||||||
}
|
|
||||||
newState.targetHeight = newState.avgRowHeight * newState.scrollHeightInItems + newState.viewportHeight;
|
|
||||||
const scrollTop = props.scrollTop;
|
|
||||||
let scrollPos = scrollTop / (newState.targetHeight - newState.viewportHeight);
|
|
||||||
if (scrollPos > 1)
|
|
||||||
{
|
|
||||||
// Rare case - avgRowHeight isn't enough and we need more
|
|
||||||
// avgRowHeight will be corrected after rendering all items
|
|
||||||
scrollPos = 1;
|
|
||||||
}
|
|
||||||
let firstVisibleItem = scrollPos * newState.scrollHeightInItems;
|
|
||||||
const firstVisibleItemOffset = firstVisibleItem - Math.floor(firstVisibleItem);
|
|
||||||
// FIXME: Render some items before current for smoothness
|
|
||||||
firstVisibleItem = Math.floor(firstVisibleItem);
|
|
||||||
let firstVisibleItemHeight = getRenderedItemHeight(firstVisibleItem) || newState.avgRowHeight;
|
|
||||||
newState.topPlaceholderHeight = scrollTop - firstVisibleItemHeight*firstVisibleItemOffset;
|
|
||||||
if (newState.topPlaceholderHeight < 0)
|
|
||||||
{
|
|
||||||
newState.topPlaceholderHeight = 0;
|
|
||||||
}
|
|
||||||
if (firstVisibleItem + newState.viewportItemCount >= props.totalItems - newState.viewportItemCount)
|
|
||||||
{
|
|
||||||
// Only one placeholder is required
|
|
||||||
newState.lastItemCount = props.totalItems - firstVisibleItem;
|
|
||||||
let sum = 0, count = props.totalItems - newState.viewportItemCount - firstVisibleItem;
|
|
||||||
count = count > 0 ? count : 0;
|
|
||||||
for (let i = 0; i < count; i++)
|
|
||||||
{
|
|
||||||
const itemSize = getRenderedItemHeight(i+newState.firstMiddleItem);
|
|
||||||
if (!itemSize)
|
|
||||||
{
|
|
||||||
// Some required items in the middle are missing
|
|
||||||
return newState;
|
|
||||||
}
|
|
||||||
sum += itemSize;
|
|
||||||
}
|
|
||||||
const correctedAvg = (sum + newState.lastItemsTotalHeight) / (count + newState.viewportItemCount);
|
|
||||||
if (correctedAvg > newState.avgRowHeight)
|
|
||||||
{
|
|
||||||
newState.avgRowHeight = correctedAvg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
newState.firstMiddleItem = firstVisibleItem;
|
|
||||||
newState.middleItemCount = newState.viewportItemCount;
|
|
||||||
let sum = 0;
|
|
||||||
for (let i = 0; i < newState.middleItemCount; i++)
|
|
||||||
{
|
|
||||||
const itemSize = getRenderedItemHeight(i+newState.firstMiddleItem);
|
|
||||||
if (!itemSize)
|
|
||||||
{
|
|
||||||
// Some required items in the middle are missing
|
|
||||||
return newState;
|
|
||||||
}
|
|
||||||
sum += itemSize;
|
|
||||||
}
|
|
||||||
newState.middlePlaceholderHeight = newState.targetHeight - sum - newState.lastItemsTotalHeight - newState.topPlaceholderHeight;
|
|
||||||
if (newState.middlePlaceholderHeight < 0)
|
|
||||||
{
|
|
||||||
newState.middlePlaceholderHeight = 0;
|
|
||||||
}
|
|
||||||
const correctedAvg = (sum + newState.lastItemsTotalHeight) / (newState.middleItemCount + newState.viewportItemCount);
|
|
||||||
if (correctedAvg > newState.avgRowHeight)
|
|
||||||
{
|
|
||||||
newState.avgRowHeight = correctedAvg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return newState;
|
|
||||||
}
|
|
|
@ -1,132 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { virtualScrollDriver } from './DynamicVirtualScroll.js';
|
|
||||||
|
|
||||||
export class DynamicVirtualScrollExample extends React.PureComponent
|
|
||||||
{
|
|
||||||
useFixedHeader = true
|
|
||||||
|
|
||||||
constructor()
|
|
||||||
{
|
|
||||||
super();
|
|
||||||
const items = [];
|
|
||||||
for (let i = 0; i < 1000; i++)
|
|
||||||
{
|
|
||||||
items[i] = 30 + Math.round(Math.random()*50);
|
|
||||||
}
|
|
||||||
this.state = { items };
|
|
||||||
}
|
|
||||||
|
|
||||||
getRenderedItemHeight_MemoryExample = (index) =>
|
|
||||||
{
|
|
||||||
// Just for example: imitating renderer not knowing about off-screen items
|
|
||||||
if (index >= this.state.firstMiddleItem && index < this.state.firstMiddleItem+this.state.middleItemCount ||
|
|
||||||
index >= this.state.items.length - this.state.lastItemCount)
|
|
||||||
{
|
|
||||||
return this.state.items[index];
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
getRenderedItemHeight_DOMExample = (index) =>
|
|
||||||
{
|
|
||||||
// DOM example. As smooth as the previous one (memory example), even without caching
|
|
||||||
if (this.itemElements[index])
|
|
||||||
{
|
|
||||||
return this.itemElements[index].getBoundingClientRect().height;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
getRenderedItemHeight = this.getRenderedItemHeight_DOMExample
|
|
||||||
|
|
||||||
renderItems(start, count)
|
|
||||||
{
|
|
||||||
return this.state.items.slice(start, start+count).map((item, index) => (<div
|
|
||||||
key={'i'+(index+start)}
|
|
||||||
ref={e => this.itemElements[index+start] = e}
|
|
||||||
style={{height: item+'px', color: 'white', textAlign: 'center', lineHeight: item+'px', background: 'rgb('+Math.round(item*255/80)+',0,0)'}}>
|
|
||||||
№ {index+start}: {item}px
|
|
||||||
</div>));
|
|
||||||
}
|
|
||||||
|
|
||||||
render()
|
|
||||||
{
|
|
||||||
this.itemElements = [];
|
|
||||||
return (<div style={{position: 'relative', width: '400px'}}>
|
|
||||||
<div style={{overflowY: 'scroll', height: '400px', width: '400px', overflowAnchor: 'none', outline: 'none'}}
|
|
||||||
tabIndex="1"
|
|
||||||
ref={e => this.viewport = e}
|
|
||||||
onScroll={this.driver}>
|
|
||||||
<div style={{height: this.state.targetHeight+'px'}}>
|
|
||||||
{this.useFixedHeader
|
|
||||||
? <div style={{height: '30px'}}></div>
|
|
||||||
: null}
|
|
||||||
{this.state.topPlaceholderHeight
|
|
||||||
? <div style={{height: this.state.topPlaceholderHeight+'px'}}></div>
|
|
||||||
: null}
|
|
||||||
{this.state.middleItemCount
|
|
||||||
? this.renderItems(this.state.firstMiddleItem, this.state.middleItemCount)
|
|
||||||
: null}
|
|
||||||
{this.state.middlePlaceholderHeight
|
|
||||||
? <div style={{height: this.state.middlePlaceholderHeight+'px'}}></div>
|
|
||||||
: null}
|
|
||||||
{this.state.lastItemCount
|
|
||||||
? this.renderItems(this.state.items.length-this.state.lastItemCount, this.state.lastItemCount)
|
|
||||||
: null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{this.useFixedHeader ? <div style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
right: this.state.scrollbarWidth+'px',
|
|
||||||
height: '30px',
|
|
||||||
background: '#0080c0',
|
|
||||||
color: 'white',
|
|
||||||
textAlign: 'center',
|
|
||||||
lineHeight: '30px'}}>
|
|
||||||
fixed header
|
|
||||||
</div> : null}
|
|
||||||
</div>);
|
|
||||||
}
|
|
||||||
|
|
||||||
driver = () =>
|
|
||||||
{
|
|
||||||
const newState = virtualScrollDriver(
|
|
||||||
{
|
|
||||||
totalItems: this.state.items.length,
|
|
||||||
minRowHeight: 30,
|
|
||||||
viewportHeight: this.viewport.clientHeight - (this.useFixedHeader ? 30 : 0),
|
|
||||||
scrollTop: this.viewport.scrollTop,
|
|
||||||
},
|
|
||||||
this.state,
|
|
||||||
this.getRenderedItemHeight
|
|
||||||
);
|
|
||||||
newState.scrollbarWidth = this.viewport ? this.viewport.offsetWidth-this.viewport.clientWidth : 12;
|
|
||||||
this.setStateIfDiffers(newState);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate = () =>
|
|
||||||
{
|
|
||||||
this.driver();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount()
|
|
||||||
{
|
|
||||||
this.componentDidUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
setStateIfDiffers(state, cb)
|
|
||||||
{
|
|
||||||
for (const k in state)
|
|
||||||
{
|
|
||||||
if (this.state[k] != state[k])
|
|
||||||
{
|
|
||||||
this.setState(state, cb);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
161
README.md
161
README.md
|
@ -1,161 +0,0 @@
|
||||||
# Dynamic Virtual Scroll Driver
|
|
||||||
|
|
||||||
Virtual scrolling is a technique for displaying long lists or tables when you render only a small number
|
|
||||||
of visible items and skip items that are offscreen. You may also have heard about it like
|
|
||||||
"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, 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 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.
|
|
||||||
|
|
||||||
Good news, everyone: we have a solution!
|
|
||||||
|
|
||||||
It is render-agnostic and implemented in this library. Basically, this library only does the maths for you
|
|
||||||
while letting you render your component yourself. You can use it with React, Angular, pure JS or any other
|
|
||||||
framework you want to. You can implement lists and grids (tables) with it. It works smoothly, does not break
|
|
||||||
built-in browser scrolling and even works on mobile devices.
|
|
||||||
|
|
||||||
# Usage
|
|
||||||
|
|
||||||
The library exports a single function:
|
|
||||||
|
|
||||||
```
|
|
||||||
import { virtualScrollDriver } from 'dynamic-virtual-scroll';
|
|
||||||
|
|
||||||
const newState = virtualScrollDriver(
|
|
||||||
{ totalItems, minRowHeight, viewportHeight, scrollTop },
|
|
||||||
oldState,
|
|
||||||
function getRenderedItemHeight(itemIndex) { ... }
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
You must call it after each render and also when the viewport, scroll position or items change.
|
|
||||||
|
|
||||||
Description of parameters:
|
|
||||||
|
|
||||||
* `totalItems` - total number of items in your list
|
|
||||||
* `minRowHeight` - minimum item height
|
|
||||||
* `viewportHeight` - current viewport height (take from DOM)
|
|
||||||
* `scrollTop` - current scroll position (take from DOM)
|
|
||||||
* `oldState` - previous state object as returned from previous `virtualScrollDriver()` call
|
|
||||||
* `getRenderedItemHeight = (itemIndex) => height`:
|
|
||||||
* this function MUST return the height of currently rendered item or 0 if it's not currently rendered
|
|
||||||
* the returned height MUST be >= props.minRowHeight
|
|
||||||
* the function MAY cache heights of rendered items if you want your list to be more responsive
|
|
||||||
* WARNING: you SHOULD NOT use `element.offsetHeight` for measuring. Either use `element.getBoundingClientRect().height`
|
|
||||||
or use some pre-computed heights, because `offsetHeight` may truncate the height to -1px when
|
|
||||||
browser scale is not 100%. Also it gives incorrect results with CSS transforms.
|
|
||||||
|
|
||||||
Returned object is `newState`. It contains the render parameters for you and also some internal state variables.
|
|
||||||
What to do with it:
|
|
||||||
|
|
||||||
* you MUST re-render your list when any state values change
|
|
||||||
* you MUST preserve all keys in the state object and pass it back via `oldState` on the next run
|
|
||||||
* you MUST base your rendering on the following keys:
|
|
||||||
* `newState.targetHeight` - height of the 1px wide invisible div you should render in the scroll container
|
|
||||||
* `newState.topPlaceholderHeight` - height of the first (top) placeholder. omit placeholder if it is 0
|
|
||||||
* `newState.firstMiddleItem` - first item to be rendered after top placeholder
|
|
||||||
* `newState.middleItemCount` - item count to be renderer after top placeholder. omit items if it is 0
|
|
||||||
* `newState.middlePlaceholderHeight` - height of the second (middle) placeholder. omit placeholder if it is 0
|
|
||||||
* `newState.lastItemCount` - item count to be rendered in the end of the list
|
|
||||||
* also note that you MUST set `overflow-anchor: none` on your scroll container. You'll end up with
|
|
||||||
`virtualScrollDriver()` not able to finish updating in Chrome if you don't.
|
|
||||||
|
|
||||||
# Usage with React
|
|
||||||
|
|
||||||
There is a reference virtual list implementation for React.
|
|
||||||
|
|
||||||
It is usually sufficient for almost everything, including grids. Sad thing about grids (virtualized tables)
|
|
||||||
in HTML is that automatic table-like layout is slow in browsers, so in fact the best way to implement
|
|
||||||
them is via simple virtualized lists of \<div>'s with absolutely positioned cells inside.
|
|
||||||
|
|
||||||
```
|
|
||||||
import React from 'react';
|
|
||||||
import { VirtualScrollList } from 'dynamic-virtual-scroll/VirtualScrollList.es5.js';
|
|
||||||
|
|
||||||
class MyList extends React.Component
|
|
||||||
{
|
|
||||||
renderItem = (i) =>
|
|
||||||
{
|
|
||||||
if (!this.items[i])
|
|
||||||
return null;
|
|
||||||
return <div style={{minHeight: '20px'}}>{this.items[i].title}</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
render()
|
|
||||||
{
|
|
||||||
return <VirtualScrollList
|
|
||||||
totalItems={this.items.length}
|
|
||||||
renderItem={this.renderItem}
|
|
||||||
minRowHeight={20}
|
|
||||||
/>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Description of VirtualScrollList parameters:
|
|
||||||
|
|
||||||
- totalItems: required, total number of items in the list.
|
|
||||||
- minRowHeight: required, minimal possible item height in the list.
|
|
||||||
- renderItem: required, function that renders item by index as React element(s).
|
|
||||||
- viewportHeight: optional, viewport height to use for virtual scrolling.
|
|
||||||
May be used in case when it can't be determined automatically by VirtualScroll,
|
|
||||||
for example inside an animated element with css maxHeight.
|
|
||||||
- header: optional, React element(s) to unconditionally render in the beginning of
|
|
||||||
the list. The intended usage is to render fixed header row with CSS position: sticky
|
|
||||||
over the scrolled content.
|
|
||||||
- headerHeight: optional. In case there is a fixed header, this must be its height
|
|
||||||
in pixels.
|
|
||||||
- All other parameters (className, style, onClick, etc) are passed as-is to the
|
|
||||||
underlying root \<div> of the list.
|
|
||||||
|
|
||||||
VirtualScrollList contains some extra shenanigans to make sure the scroll position
|
|
||||||
preserves when the total number of items changes. Also it has two extra methods:
|
|
||||||
|
|
||||||
- `list.scrollToItem(itemIndex)` - positions the list at `itemIndex`. The index may
|
|
||||||
contain fractional part, in that case the list will be positioned at the corresponding
|
|
||||||
% of height of the item.
|
|
||||||
- `list.getItemScrollPos()` - returns current scroll position in items. The returned
|
|
||||||
index may contain fractional part and may be used as input to `list.scrollToItem()`.
|
|
||||||
|
|
||||||
# Simpler React example
|
|
||||||
|
|
||||||
See `DynamicVirtualScrollExample.js`.
|
|
||||||
|
|
||||||
How to test it:
|
|
||||||
|
|
||||||
* Clone this repository
|
|
||||||
* `npm install`
|
|
||||||
* `npm run build`
|
|
||||||
* Open `index.html` in your browser
|
|
||||||
|
|
||||||
# Demo
|
|
||||||
|
|
||||||
http://yourcmc.ru/dynamic-virtual-scroll/
|
|
||||||
|
|
||||||
# Algorithm
|
|
||||||
|
|
||||||
* Use reasonable fixed minimum row height
|
|
||||||
* Always render `screen/minHeight` last rows
|
|
||||||
* Find maximum possible viewport start in units of (item number + % of item)
|
|
||||||
* Measure average height of last rows
|
|
||||||
* `avgHeight = max(minHeight, lastRowAvgHeight)`
|
|
||||||
* `targetHeight = avgHeight*rowCount`
|
|
||||||
* Total scroll view height will be `targetHeight`
|
|
||||||
* `scrollPos = targetHeight > offsetHeight ? min(1, scrollTop / (targetHeight - offsetHeight)) : 0`
|
|
||||||
* First visible item will be `Math.floor(scrollPos*maxPossibleViewportStart)`
|
|
||||||
* Additional scroll offset will be `itemOffset = (scrollPos*maxPossibleViewportStart - Math.floor(scrollPos*maxPossibleViewportStart))*firstVisibleItemHeight`
|
|
||||||
* First (top) placeholder height will be `scrollTop-itemOffset`
|
|
||||||
* Second (middle) placeholder height will be `avgHeight*nodeCount - sum(heights of all rendered rows) - (first placeholder height)`
|
|
||||||
|
|
||||||
# License and author
|
|
||||||
|
|
||||||
Author: Vitaliy Filippov, 2018+
|
|
||||||
|
|
||||||
License: GNU LGPLv3.0 or newer
|
|
|
@ -1,239 +0,0 @@
|
||||||
/**
|
|
||||||
* A simple React list with virtual scrolling based on dynamic-virtual-scroll driver
|
|
||||||
* USUALLY sufficient for everything including grids (using absolute sizing of cells).
|
|
||||||
* Just because browsers can't do virtualized grid or table layouts efficiently.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { virtualScrollDriver } from 'dynamic-virtual-scroll';
|
|
||||||
|
|
||||||
export class VirtualScrollList extends React.Component
|
|
||||||
{
|
|
||||||
static propTypes = {
|
|
||||||
className: PropTypes.string,
|
|
||||||
style: PropTypes.object,
|
|
||||||
totalItems: PropTypes.number.isRequired,
|
|
||||||
minRowHeight: PropTypes.number.isRequired,
|
|
||||||
viewportHeight: PropTypes.number,
|
|
||||||
header: PropTypes.any,
|
|
||||||
headerHeight: PropTypes.number,
|
|
||||||
renderItem: PropTypes.func.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
state = {
|
|
||||||
targetHeight: 0,
|
|
||||||
topPlaceholderHeight: 0,
|
|
||||||
firstMiddleItem: 0,
|
|
||||||
middleItemCount: 0,
|
|
||||||
middlePlaceholderHeight: 0,
|
|
||||||
lastItemCount: 0,
|
|
||||||
scrollTo: 0,
|
|
||||||
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++)
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
render()
|
|
||||||
{
|
|
||||||
if (this.state.totalItems && this.props.totalItems != this.state.totalItems &&
|
|
||||||
this.state.scrollTimes <= 0 && this.viewport && this.viewport.offsetParent)
|
|
||||||
{
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
const props = { ...this.props };
|
|
||||||
for (const k in VirtualScrollList.propTypes)
|
|
||||||
{
|
|
||||||
delete props[k];
|
|
||||||
}
|
|
||||||
return (<div
|
|
||||||
{...props}
|
|
||||||
className={this.props.className}
|
|
||||||
style={{
|
|
||||||
position: 'relative',
|
|
||||||
...(this.props.style||{}),
|
|
||||||
overflowAnchor: 'none',
|
|
||||||
}}
|
|
||||||
ref={this.setViewport}
|
|
||||||
onScroll={this.onScroll}>
|
|
||||||
{this.props.header}
|
|
||||||
{this.state.targetHeight > 0
|
|
||||||
? <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>
|
|
||||||
: null}
|
|
||||||
{this.renderItems(this.state.firstMiddleItem, this.state.middleItemCount)}
|
|
||||||
{this.state.middlePlaceholderHeight
|
|
||||||
? <div style={{height: this.state.middlePlaceholderHeight+'px'}} key="mid"></div>
|
|
||||||
: null}
|
|
||||||
{this.renderItems(this.props.totalItems-this.state.lastItemCount, this.state.lastItemCount, true)}
|
|
||||||
</div>);
|
|
||||||
}
|
|
||||||
|
|
||||||
setViewport = (e) =>
|
|
||||||
{
|
|
||||||
this.viewport = e;
|
|
||||||
}
|
|
||||||
|
|
||||||
getRenderedItemHeight = (index) =>
|
|
||||||
{
|
|
||||||
if (this.itemRefs[index])
|
|
||||||
{
|
|
||||||
const e = ReactDOM.findDOMNode(this.itemRefs[index]);
|
|
||||||
if (e)
|
|
||||||
{
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 &&
|
|
||||||
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;
|
|
||||||
if (pos > this.state.scrollHeightInItems)
|
|
||||||
{
|
|
||||||
pos = this.state.scrollHeightInItems;
|
|
||||||
}
|
|
||||||
if (this.state.targetHeight)
|
|
||||||
{
|
|
||||||
this.viewport.scrollTop = Math.round((this.state.targetHeight - this.state.viewportHeight)*pos/this.state.scrollHeightInItems);
|
|
||||||
this.setState({ scrollTimes: this.state.scrollTimes - 1 });
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
this.setState({ scrollTimes: 0 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount()
|
|
||||||
{
|
|
||||||
this.driver();
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollToItem = (pos) =>
|
|
||||||
{
|
|
||||||
// Scroll position must be recalculated twice, because first render
|
|
||||||
// may change the average row height. In fact, it must be repeated
|
|
||||||
// until average row height stops changing, but twice is usually sufficient
|
|
||||||
this.setState({ scrollTo: pos, scrollTimes: 2 });
|
|
||||||
}
|
|
||||||
|
|
||||||
getItemScrollPos = () =>
|
|
||||||
{
|
|
||||||
if (this.state.targetHeight)
|
|
||||||
{
|
|
||||||
// Virtual scroll is active
|
|
||||||
let pos = this.viewport.scrollTop / (this.state.targetHeight - this.state.viewportHeight);
|
|
||||||
return pos * this.state.scrollHeightInItems;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Virtual scroll is inactive
|
|
||||||
let avgr = this.viewport.scrollHeight / this.state.totalItems;
|
|
||||||
return this.viewport.scrollTop / avgr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
driver = () =>
|
|
||||||
{
|
|
||||||
if (!this.viewport || !this.viewport.offsetParent)
|
|
||||||
{
|
|
||||||
// Fool tolerance - do nothing if we are hidden
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const newState = virtualScrollDriver(
|
|
||||||
{
|
|
||||||
totalItems: this.props.totalItems,
|
|
||||||
minRowHeight: this.props.minRowHeight,
|
|
||||||
viewportHeight: this.props.viewportHeight || (this.viewport.clientHeight-(this.props.headerHeight||0)),
|
|
||||||
scrollTop: this.viewport.scrollTop,
|
|
||||||
},
|
|
||||||
this.state,
|
|
||||||
this.getRenderedItemHeight
|
|
||||||
);
|
|
||||||
if (newState.viewportHeight || this.state.viewportHeight)
|
|
||||||
{
|
|
||||||
return this.setStateIfDiffers(newState);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
setStateIfDiffers(state, cb)
|
|
||||||
{
|
|
||||||
for (const k in state)
|
|
||||||
{
|
|
||||||
if (this.state[k] != state[k])
|
|
||||||
{
|
|
||||||
this.setState(state, cb);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
12
index.html
12
index.html
|
@ -1,12 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<title>Dynamic Virtual Scroll Driver Demo</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="app">
|
|
||||||
</div>
|
|
||||||
<script type="text/javascript" src="dist/main.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
8
main.js
8
main.js
|
@ -1,8 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
|
|
||||||
import { DynamicVirtualScrollExample } from './DynamicVirtualScrollExample.js';
|
|
||||||
|
|
||||||
ReactDOM.render(
|
|
||||||
<DynamicVirtualScrollExample />, document.getElementById('app')
|
|
||||||
);
|
|
69
package.json
69
package.json
|
@ -1,69 +0,0 @@
|
||||||
{
|
|
||||||
"name": "dynamic-virtual-scroll",
|
|
||||||
"version": "1.0.15",
|
|
||||||
"author": {
|
|
||||||
"name": "Vitaliy Filippov",
|
|
||||||
"email": "vitalif@yourcmc.ru",
|
|
||||||
"url": "http://yourcmc.ru/wiki/"
|
|
||||||
},
|
|
||||||
"description": "Render-agnostic Dynamic Virtual Scroll Driver",
|
|
||||||
"keywords": [
|
|
||||||
"virtual",
|
|
||||||
"scrolling",
|
|
||||||
"list",
|
|
||||||
"grid",
|
|
||||||
"windowed",
|
|
||||||
"dynamic",
|
|
||||||
"virtual-list",
|
|
||||||
"virtual-scroll",
|
|
||||||
"lazy-rendering",
|
|
||||||
"react"
|
|
||||||
],
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/vitalif/dynamic-virtual-scroll"
|
|
||||||
},
|
|
||||||
"homepage": "https://github.com/vitalif/dynamic-virtual-scroll",
|
|
||||||
"bugs": {
|
|
||||||
"url": "https://github.com/vitalif/dynamic-virtual-scroll/issues"
|
|
||||||
},
|
|
||||||
"license": "LGPL",
|
|
||||||
"maintainers": [
|
|
||||||
{
|
|
||||||
"name": "vitalif",
|
|
||||||
"email": "vitalif@yourcmc.ru"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"files": [
|
|
||||||
"DynamicVirtualScroll.es5.js",
|
|
||||||
"DynamicVirtualScroll.js",
|
|
||||||
"VirtualScrollList.es5.js",
|
|
||||||
"VirtualScrollList.js",
|
|
||||||
"DynamicVirtualScrollExample.js",
|
|
||||||
"README.md"
|
|
||||||
],
|
|
||||||
"main": "./DynamicVirtualScroll.es5.js",
|
|
||||||
"module": "./DynamicVirtualScroll.js",
|
|
||||||
"dependencies": {},
|
|
||||||
"devDependencies": {
|
|
||||||
"babel-cli": "^6.26.0",
|
|
||||||
"babel-core": "^6.26.3",
|
|
||||||
"babel-eslint": "^10.0.1",
|
|
||||||
"babel-loader": "^7.1.5",
|
|
||||||
"babel-polyfill": "^6.26.0",
|
|
||||||
"babel-preset-env": "^1.7.0",
|
|
||||||
"babel-preset-react": "^6.24.1",
|
|
||||||
"babel-preset-stage-1": "^6.24.1",
|
|
||||||
"eslint": "^5.1.0",
|
|
||||||
"eslint-plugin-react": "^7.10.0",
|
|
||||||
"react": "^16.4.1",
|
|
||||||
"react-dom": "^16.4.1",
|
|
||||||
"webpack": "^4.20.2",
|
|
||||||
"webpack-cli": "^3.1.2"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"build": "eslint DynamicVirtualScroll.js && webpack --mode=production --optimize-minimize && (babel DynamicVirtualScroll.js > DynamicVirtualScroll.es5.js) && (babel VirtualScrollList.js > VirtualScrollList.es5.js)",
|
|
||||||
"watch-dev": "NODE_ENV=development webpack --mode=development -w",
|
|
||||||
"watch": "webpack --mode=production -w --optimize-minimize"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,230 @@
|
||||||
|
<div tabindex="1" id="scroll" style="overflow-y: scroll; width: 300px; position: relative; height: 300px; border: 1px solid gray;" onscroll="enqueue(scrollother)">
|
||||||
|
<div style="height: 1500px" id="content"></div>
|
||||||
|
<div id="realcontent" style="z-index: 1; width: 100%; position: absolute; overflow: hidden"></div>
|
||||||
|
</div>
|
||||||
|
<style>
|
||||||
|
.item { border: 1px solid gray; box-sizing: border-box; text-align: center; }
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
/*<!--*/
|
||||||
|
//http://stackoverflow.com/questions/11730596/non-linear-scrolling
|
||||||
|
var s = document.getElementById('scroll');
|
||||||
|
var c = document.getElementById('content');
|
||||||
|
var rc = document.getElementById('realcontent');
|
||||||
|
|
||||||
|
var minheight = 100;
|
||||||
|
|
||||||
|
var api = (function() {
|
||||||
|
var items = [];
|
||||||
|
for (var i = 0; i < 100; i++)
|
||||||
|
items.push(30+Math.round(Math.random()*120));
|
||||||
|
items = [31,109,107,97,140,133,49,69,123,43,137,128,31,101,120,148,119,30,31,37,43,119,105,106,91,148,36,85,76,97,148,31,53,126,138,51,85,65,53,78,39,49,133,49,103,146,143,148,115,49,55,98,44,84,57,127,98,98,104,116,150,82,123,62,134,109,89,71,36,50,146,146,53,35,86,143,86,138,39,81,72,106,88,117,127,97,127,147,102,42,124,99,32,51,84,45,106,139,140,69];
|
||||||
|
// Аналог API получения элементов
|
||||||
|
return [ items.length, function(start, count, callback)
|
||||||
|
{
|
||||||
|
setTimeout(function() { callback(items.slice(start, start+count)); }, 200);
|
||||||
|
} ]
|
||||||
|
})();
|
||||||
|
var totalItems = api[0];
|
||||||
|
var getItems = api[1];
|
||||||
|
|
||||||
|
c.style.height = (totalItems*minheight)+'px';
|
||||||
|
|
||||||
|
var lastFirst = 0;
|
||||||
|
var curFirst = 0, curOffset = 0, curCount = 0, curPos = null;
|
||||||
|
var pagesize = Math.floor(s.offsetHeight/30);
|
||||||
|
|
||||||
|
rc.parentNode.addEventListener('keydown', handlekey);
|
||||||
|
c.addEventListener('keydown', handlekey, true);
|
||||||
|
rc.addEventListener('keydown', handlekey, true);
|
||||||
|
//rc.parentNode.addEventListener('DOMMouseScroll', mouse_wheel);
|
||||||
|
//rc.parentNode.onmousewheel = mouse_wheel;
|
||||||
|
rc.parentNode.focus();
|
||||||
|
|
||||||
|
function mouse_wheel(event)
|
||||||
|
{
|
||||||
|
event = event||window.event;
|
||||||
|
var direction = ((event.wheelDelta) ? event.wheelDelta/120 : event.detail/-3) || false;
|
||||||
|
if (direction)
|
||||||
|
{
|
||||||
|
if (event.preventDefault)
|
||||||
|
event.preventDefault();
|
||||||
|
event.returnValue = false;
|
||||||
|
enqueue(scrollbypx, -direction * 30);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlekey(ev)
|
||||||
|
{
|
||||||
|
if (ev.keyCode == 38 || ev.keyCode == 40 || ev.keyCode == 33 || ev.keyCode == 34) // up, down, pgup, pgdown
|
||||||
|
{
|
||||||
|
var px;
|
||||||
|
if (ev.keyCode == 38) px = -30;
|
||||||
|
else if (ev.keyCode == 40) px = 30;
|
||||||
|
else if (ev.keyCode == 33) px = -270;
|
||||||
|
else if (ev.keyCode == 34) px = 270;
|
||||||
|
enqueue(scrollbypx, px);
|
||||||
|
}
|
||||||
|
else if (ev.keyCode == 36)
|
||||||
|
s.scrollTop = 0;
|
||||||
|
else if (ev.keyCode == 35)
|
||||||
|
s.scrollTop = s.scrollHeight-s.offsetHeight+1;
|
||||||
|
else
|
||||||
|
return;
|
||||||
|
ev.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
var rq = [];
|
||||||
|
var loadedItems = [];
|
||||||
|
|
||||||
|
function enqueue(fn, arg)
|
||||||
|
{
|
||||||
|
if (!rq.length)
|
||||||
|
fn(arg);
|
||||||
|
else if (arg)
|
||||||
|
rq.push(function() { fn(arg) });
|
||||||
|
else if (rq[rq.length-1] != fn)
|
||||||
|
rq.push(fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadfrom(start, count)
|
||||||
|
{
|
||||||
|
console.log('load from '+start+': '+count+' items');
|
||||||
|
getItems(start, count, function(partItems)
|
||||||
|
{
|
||||||
|
var h, i;
|
||||||
|
for (i = 0; i < partItems.length; i++)
|
||||||
|
loadedItems[start+i] = partItems[i];
|
||||||
|
rq.length && (rq.shift())();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// скроллинг в пикселях (в пределах видимости - можно использовать для up/down, pgup/pgdown, колеса мыши)
|
||||||
|
function scrollbypx(px)
|
||||||
|
{
|
||||||
|
if (px < 0)
|
||||||
|
{
|
||||||
|
var cpx = px;
|
||||||
|
cpx += curOffset*loadedItems[curFirst];
|
||||||
|
var nf = curFirst;
|
||||||
|
while (nf > 0 && cpx < 0)
|
||||||
|
{
|
||||||
|
nf--;
|
||||||
|
if (!loadedItems[nf])
|
||||||
|
{
|
||||||
|
rq.unshift(function() { scrollbypx(px) });
|
||||||
|
loadfrom(nf+1 > pagesize ? nf+1-pagesize : 0, pagesize);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cpx += loadedItems[nf];
|
||||||
|
}
|
||||||
|
newFirst = nf+(cpx/loadedItems[nf]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
px -= loadedItems[curFirst]*(1-curOffset);
|
||||||
|
var nf = curFirst;
|
||||||
|
while (nf < totalItems-1 && px > 0)
|
||||||
|
{
|
||||||
|
nf++;
|
||||||
|
px -= loadedItems[nf];
|
||||||
|
}
|
||||||
|
nfo = (px >= 0 ? loadedItems[nf] : px+loadedItems[nf]);
|
||||||
|
newFirst = nf+(nfo/loadedItems[nf]);
|
||||||
|
}
|
||||||
|
s.scrollTop = newFirst/lastFirst*(s.scrollHeight-s.offsetHeight+1);
|
||||||
|
rq.length && (rq.shift())();
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadnext()
|
||||||
|
{
|
||||||
|
var h, i;
|
||||||
|
for (h = 0, i = curFirst; i < totalItems && h < s.offsetHeight; i++)
|
||||||
|
{
|
||||||
|
if (!loadedItems[i])
|
||||||
|
{
|
||||||
|
var start = i;
|
||||||
|
while (start < totalItems && !loadedItems[start] && start < i+pagesize)
|
||||||
|
start++;
|
||||||
|
start = start-pagesize;
|
||||||
|
start = start < 0 ? 0 : start;
|
||||||
|
rq.unshift(loadnext);
|
||||||
|
loadfrom(start, pagesize);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
h += (i == curFirst ? loadedItems[i]*(1-curOffset) : loadedItems[i]);
|
||||||
|
}
|
||||||
|
curCount = i-curFirst;
|
||||||
|
rq.length && (rq.shift())();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderitems()
|
||||||
|
{
|
||||||
|
var ih = '', h = 0;
|
||||||
|
for (var i = 0; i < curCount; i++)
|
||||||
|
{
|
||||||
|
ih += '<div class="item" style="height: '+loadedItems[curFirst+i]+'px">'+(curFirst+i)+'</div>';
|
||||||
|
h += loadedItems[curFirst+i];
|
||||||
|
}
|
||||||
|
// curPos, чтобы при плавном скролле вниз загруженное в середину не было видно
|
||||||
|
var t = Math.round(-loadedItems[curFirst]*curOffset+curPos);
|
||||||
|
rc.style.height = '1px'; // на случай, если браузеру вздумается где-то посередине перерисовать
|
||||||
|
rc.style.top = t+'px';
|
||||||
|
rc.style.height = (t+h < s.scrollHeight-1 ? h : s.scrollHeight-1-t)+'px';
|
||||||
|
rc.innerHTML = ih;
|
||||||
|
// Тут можно убить лишние элементы
|
||||||
|
var q0 = curFirst-pagesize;
|
||||||
|
q0 = q0 - q0%pagesize;
|
||||||
|
var q1 = curFirst+curCount+pagesize;
|
||||||
|
q1 = q1 + (pagesize-q1%pagesize);
|
||||||
|
var destroyed = 0;
|
||||||
|
for (var i in loadedItems)
|
||||||
|
{
|
||||||
|
if (i < q0 || i >= q1)
|
||||||
|
{
|
||||||
|
destroyed++;
|
||||||
|
delete loadedItems[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (destroyed > 0)
|
||||||
|
console.log('destroy '+destroyed+' items');
|
||||||
|
rq.length && (rq.shift())();
|
||||||
|
}
|
||||||
|
|
||||||
|
// нелинейный скроллинг (в % от элементов)
|
||||||
|
function scrollother()
|
||||||
|
{
|
||||||
|
var i, h;
|
||||||
|
if (s.scrollTop == curPos)
|
||||||
|
return;
|
||||||
|
var newFirst = s.scrollTop / (s.scrollHeight-s.offsetHeight+1) * lastFirst;
|
||||||
|
if (newFirst > lastFirst)
|
||||||
|
newFirst = lastFirst;
|
||||||
|
curFirst = Math.floor(newFirst);
|
||||||
|
curOffset = newFirst-curFirst;
|
||||||
|
curPos = s.scrollTop;
|
||||||
|
rq.unshift(renderitems);
|
||||||
|
loadnext();
|
||||||
|
}
|
||||||
|
|
||||||
|
function initscroll()
|
||||||
|
{
|
||||||
|
var h = 0, i;
|
||||||
|
for (i = totalItems-1; i >= 0 && h < s.offsetHeight; i--)
|
||||||
|
{
|
||||||
|
if (!loadedItems[i])
|
||||||
|
{
|
||||||
|
rq.unshift(initscroll);
|
||||||
|
loadfrom(i+1-pagesize, pagesize);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
h += loadedItems[i];
|
||||||
|
}
|
||||||
|
lastFirst = i+1+(h-s.offsetHeight)/loadedItems[i+1];
|
||||||
|
rq.length && (rq.shift())();
|
||||||
|
}
|
||||||
|
|
||||||
|
rq.push(scrollother);
|
||||||
|
initscroll();
|
||||||
|
//-->
|
||||||
|
</script>
|
|
@ -1,49 +0,0 @@
|
||||||
const webpack = require('webpack');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
entry: {
|
|
||||||
main: [ "babel-polyfill", './main.js' ]
|
|
||||||
},
|
|
||||||
context: __dirname,
|
|
||||||
output: {
|
|
||||||
path: __dirname,
|
|
||||||
filename: 'dist/[name].js'
|
|
||||||
},
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /.jsx?$/,
|
|
||||||
loader: 'babel-loader',
|
|
||||||
exclude: /node_modules(?!\/react-toolbox\/components)/
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.css$/,
|
|
||||||
use: [
|
|
||||||
"style-loader",
|
|
||||||
{
|
|
||||||
loader: "css-loader",
|
|
||||||
options: {
|
|
||||||
modules: true, // default is false
|
|
||||||
sourceMap: true,
|
|
||||||
importLoaders: 1,
|
|
||||||
localIdentName: "[name]--[local]--[hash:base64:8]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"postcss-loader"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
new webpack.DefinePlugin({
|
|
||||||
"process.env": {
|
|
||||||
NODE_ENV: JSON.stringify(process.env.NODE_ENV || "production")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
],
|
|
||||||
performance: {
|
|
||||||
maxEntrypointSize: 3000000,
|
|
||||||
maxAssetSize: 3000000
|
|
||||||
}
|
|
||||||
};
|
|
Loading…
Reference in New Issue