virtual-scroller 1.7.7 → 1.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitlab-ci.yml +1 -1
- package/CHANGELOG.md +24 -1
- package/README.md +139 -33
- package/babel.config.js +25 -0
- package/babel.js +5 -0
- package/bundle/index-bypass.html +1 -1
- package/bundle/index-dom.html +1 -1
- package/bundle/index-grid.html +1 -2
- package/bundle/index-scrollableContainer.html +1 -1
- package/bundle/index-tbody-scrollableContainer.html +2 -0
- package/bundle/index-tbody.html +2 -0
- package/bundle/virtual-scroller-dom.js +1 -1
- package/bundle/virtual-scroller-dom.js.map +1 -1
- package/bundle/virtual-scroller-react.js +1 -1
- package/bundle/virtual-scroller-react.js.map +1 -1
- package/bundle/virtual-scroller.js +1 -1
- package/bundle/virtual-scroller.js.map +1 -1
- package/commonjs/BeforeResize.js +319 -0
- package/commonjs/BeforeResize.js.map +1 -0
- package/commonjs/DOM/Engine.js +46 -0
- package/commonjs/DOM/Engine.js.map +1 -0
- package/commonjs/DOM/ItemsContainer.js +78 -0
- package/commonjs/DOM/ItemsContainer.js.map +1 -0
- package/commonjs/DOM/{WaitForStylesToLoad.js → ListTopOffsetWatcher.js} +56 -35
- package/commonjs/DOM/ListTopOffsetWatcher.js.map +1 -0
- package/commonjs/DOM/ScrollableContainer.js +56 -81
- package/commonjs/DOM/ScrollableContainer.js.map +1 -1
- package/commonjs/DOM/VirtualScroller.js +20 -15
- package/commonjs/DOM/VirtualScroller.js.map +1 -1
- package/commonjs/DOM/tbody.js +2 -2
- package/commonjs/ItemHeights.js +22 -29
- package/commonjs/ItemHeights.js.map +1 -1
- package/commonjs/Layout.js +588 -215
- package/commonjs/Layout.js.map +1 -1
- package/commonjs/Layout.test.js +191 -0
- package/commonjs/Layout.test.js.map +1 -0
- package/commonjs/ListHeightChangeWatcher.js +126 -0
- package/commonjs/ListHeightChangeWatcher.js.map +1 -0
- package/commonjs/Resize.js +22 -21
- package/commonjs/Resize.js.map +1 -1
- package/commonjs/Scroll.js +148 -88
- package/commonjs/Scroll.js.map +1 -1
- package/commonjs/VirtualScroller.js +1269 -390
- package/commonjs/VirtualScroller.js.map +1 -1
- package/commonjs/getItemCoordinates.js.map +1 -1
- package/commonjs/getItemsDiff.js.map +1 -1
- package/commonjs/getVerticalSpacing.js +8 -8
- package/commonjs/getVerticalSpacing.js.map +1 -1
- package/commonjs/react/VirtualScroller.js +31 -37
- package/commonjs/react/VirtualScroller.js.map +1 -1
- package/commonjs/utility/debounce.js +26 -4
- package/commonjs/utility/debounce.js.map +1 -1
- package/commonjs/utility/debug.js +51 -12
- package/commonjs/utility/debug.js.map +1 -1
- package/commonjs/utility/getStateSnapshot.js +50 -0
- package/commonjs/utility/getStateSnapshot.js.map +1 -0
- package/commonjs/utility/px.js +1 -1
- package/commonjs/utility/px.js.map +1 -1
- package/commonjs/utility/px.test.js +14 -0
- package/commonjs/utility/px.test.js.map +1 -0
- package/commonjs/utility/shallowEqual.js +1 -1
- package/commonjs/utility/shallowEqual.js.map +1 -1
- package/commonjs/utility/throttle.js.map +1 -1
- package/dom/index.d.ts +23 -0
- package/index.d.ts +84 -0
- package/modules/BeforeResize.js +310 -0
- package/modules/BeforeResize.js.map +1 -0
- package/modules/DOM/Engine.js +27 -0
- package/modules/DOM/Engine.js.map +1 -0
- package/modules/DOM/ItemsContainer.js +71 -0
- package/modules/DOM/ItemsContainer.js.map +1 -0
- package/modules/DOM/{WaitForStylesToLoad.js → ListTopOffsetWatcher.js} +57 -35
- package/modules/DOM/ListTopOffsetWatcher.js.map +1 -0
- package/modules/DOM/ScrollableContainer.js +55 -80
- package/modules/DOM/ScrollableContainer.js.map +1 -1
- package/modules/DOM/VirtualScroller.js +15 -14
- package/modules/DOM/VirtualScroller.js.map +1 -1
- package/modules/ItemHeights.js +17 -28
- package/modules/ItemHeights.js.map +1 -1
- package/modules/Layout.js +582 -213
- package/modules/Layout.js.map +1 -1
- package/modules/Layout.test.js +185 -0
- package/modules/Layout.test.js.map +1 -0
- package/modules/ListHeightChangeWatcher.js +119 -0
- package/modules/ListHeightChangeWatcher.js.map +1 -0
- package/modules/Resize.js +21 -20
- package/modules/Resize.js.map +1 -1
- package/modules/Scroll.js +148 -87
- package/modules/Scroll.js.map +1 -1
- package/modules/VirtualScroller.js +1263 -390
- package/modules/VirtualScroller.js.map +1 -1
- package/modules/getItemCoordinates.js.map +1 -1
- package/modules/getItemsDiff.js.map +1 -1
- package/modules/getVerticalSpacing.js +8 -8
- package/modules/getVerticalSpacing.js.map +1 -1
- package/modules/react/VirtualScroller.js +31 -37
- package/modules/react/VirtualScroller.js.map +1 -1
- package/modules/utility/debounce.js +26 -4
- package/modules/utility/debounce.js.map +1 -1
- package/modules/utility/debug.js +47 -10
- package/modules/utility/debug.js.map +1 -1
- package/modules/utility/getStateSnapshot.js +43 -0
- package/modules/utility/getStateSnapshot.js.map +1 -0
- package/modules/utility/px.js +1 -1
- package/modules/utility/px.js.map +1 -1
- package/modules/utility/px.test.js +9 -0
- package/modules/utility/px.test.js.map +1 -0
- package/modules/utility/shallowEqual.js +1 -1
- package/modules/utility/shallowEqual.js.map +1 -1
- package/modules/utility/throttle.js.map +1 -1
- package/package.json +24 -22
- package/react/index.d.ts +27 -0
- package/source/BeforeResize.js +317 -0
- package/source/DOM/Engine.js +32 -0
- package/source/DOM/ItemsContainer.js +48 -0
- package/source/DOM/{WaitForStylesToLoad.js → ListTopOffsetWatcher.js} +48 -22
- package/source/DOM/ScrollableContainer.js +39 -56
- package/source/DOM/VirtualScroller.js +6 -7
- package/source/ItemHeights.js +19 -24
- package/source/Layout.js +626 -252
- package/source/Layout.test.js +171 -0
- package/source/ListHeightChangeWatcher.js +94 -0
- package/source/Resize.js +23 -15
- package/source/Scroll.js +139 -78
- package/source/VirtualScroller.js +1243 -286
- package/source/getVerticalSpacing.js +7 -7
- package/source/react/VirtualScroller.js +2 -18
- package/source/utility/debounce.js +20 -3
- package/source/utility/debug.js +34 -3
- package/source/utility/getStateSnapshot.js +36 -0
- package/source/utility/px.js +1 -1
- package/source/utility/px.test.js +9 -0
- package/website/index-bypass.html +195 -0
- package/website/index-grid.html +0 -1
- package/website/index-scrollableContainer.html +208 -0
- package/website/index-tbody-scrollableContainer.html +68 -0
- package/website/index-tbody.html +55 -0
- package/commonjs/DOM/RenderingEngine.js +0 -33
- package/commonjs/DOM/RenderingEngine.js.map +0 -1
- package/commonjs/DOM/Screen.js +0 -87
- package/commonjs/DOM/Screen.js.map +0 -1
- package/commonjs/DOM/WaitForStylesToLoad.js.map +0 -1
- package/commonjs/RestoreScroll.js +0 -118
- package/commonjs/RestoreScroll.js.map +0 -1
- package/modules/DOM/RenderingEngine.js +0 -19
- package/modules/DOM/RenderingEngine.js.map +0 -1
- package/modules/DOM/Screen.js +0 -80
- package/modules/DOM/Screen.js.map +0 -1
- package/modules/DOM/WaitForStylesToLoad.js.map +0 -1
- package/modules/RestoreScroll.js +0 -111
- package/modules/RestoreScroll.js.map +0 -1
- package/source/DOM/RenderingEngine.js +0 -22
- package/source/DOM/Screen.js +0 -51
- package/source/RestoreScroll.js +0 -86
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import Layout from './Layout'
|
|
2
|
+
|
|
3
|
+
import Engine from '../test/Engine'
|
|
4
|
+
|
|
5
|
+
describe('Layout', function() {
|
|
6
|
+
it('should work', function() {
|
|
7
|
+
const SCREEN_HEIGHT = 400
|
|
8
|
+
|
|
9
|
+
const scrollableContainer = {
|
|
10
|
+
width: 800,
|
|
11
|
+
height: SCREEN_HEIGHT
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const ITEM_WIDTH = scrollableContainer.width
|
|
15
|
+
const ITEM_HEIGHT = 200
|
|
16
|
+
|
|
17
|
+
const VERTICAL_SPACING = 100
|
|
18
|
+
|
|
19
|
+
const items = new Array(9).fill(ITEM_WIDTH * ITEM_HEIGHT)
|
|
20
|
+
|
|
21
|
+
const layout = new Layout({
|
|
22
|
+
getPrerenderMargin: () => SCREEN_HEIGHT,
|
|
23
|
+
getVerticalSpacing: () => VERTICAL_SPACING,
|
|
24
|
+
getColumnsCount: () => 1,
|
|
25
|
+
getItemHeight: (i) => items[i] / scrollableContainer.width,
|
|
26
|
+
getBeforeResizeItemsCount: () => 0,
|
|
27
|
+
getAverageItemHeight: () => ITEM_HEIGHT,
|
|
28
|
+
getScrollableContainerHeight: () => scrollableContainer.height
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
// Initial render.
|
|
32
|
+
layout.getShownItemIndexes({
|
|
33
|
+
itemsCount: items.length,
|
|
34
|
+
visibleAreaTop: 0,
|
|
35
|
+
visibleAreaBottom: SCREEN_HEIGHT
|
|
36
|
+
}).should.deep.equal({
|
|
37
|
+
firstShownItemIndex: 0,
|
|
38
|
+
lastShownItemIndex: 2
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
// The first item is almost hidden.
|
|
42
|
+
layout.getShownItemIndexes({
|
|
43
|
+
itemsCount: items.length,
|
|
44
|
+
visibleAreaTop: SCREEN_HEIGHT + ITEM_HEIGHT - 1,
|
|
45
|
+
visibleAreaBottom: (SCREEN_HEIGHT + ITEM_HEIGHT - 1) + SCREEN_HEIGHT
|
|
46
|
+
}).should.deep.equal({
|
|
47
|
+
firstShownItemIndex: 0,
|
|
48
|
+
lastShownItemIndex: 4
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
// The first item is hidden.
|
|
52
|
+
layout.getShownItemIndexes({
|
|
53
|
+
itemsCount: items.length,
|
|
54
|
+
visibleAreaTop: SCREEN_HEIGHT + ITEM_HEIGHT,
|
|
55
|
+
visibleAreaBottom: (SCREEN_HEIGHT + ITEM_HEIGHT) + SCREEN_HEIGHT
|
|
56
|
+
}).should.deep.equal({
|
|
57
|
+
firstShownItemIndex: 1,
|
|
58
|
+
lastShownItemIndex: 4
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
// A new item at the bottom is almost visible.
|
|
62
|
+
layout.getShownItemIndexes({
|
|
63
|
+
itemsCount: items.length,
|
|
64
|
+
visibleAreaTop: (ITEM_HEIGHT + VERTICAL_SPACING) * 5 - SCREEN_HEIGHT * 2,
|
|
65
|
+
visibleAreaBottom: (ITEM_HEIGHT + VERTICAL_SPACING) * 5 - SCREEN_HEIGHT
|
|
66
|
+
}).should.deep.equal({
|
|
67
|
+
firstShownItemIndex: 1,
|
|
68
|
+
lastShownItemIndex: 4
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
// A new item at the bottom is visible.
|
|
72
|
+
layout.getShownItemIndexes({
|
|
73
|
+
itemsCount: items.length,
|
|
74
|
+
visibleAreaTop: (ITEM_HEIGHT + VERTICAL_SPACING) * 5 + 1 - SCREEN_HEIGHT * 2,
|
|
75
|
+
visibleAreaBottom: (ITEM_HEIGHT + VERTICAL_SPACING) * 5 + 1 - SCREEN_HEIGHT
|
|
76
|
+
}).should.deep.equal({
|
|
77
|
+
firstShownItemIndex: 1,
|
|
78
|
+
lastShownItemIndex: 5
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('should update layout for items incremental change', function() {
|
|
83
|
+
const scrollableContainer = {
|
|
84
|
+
width: 800,
|
|
85
|
+
height: 400
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const ITEM_WIDTH = scrollableContainer.width
|
|
89
|
+
const ITEM_HEIGHT = 200
|
|
90
|
+
|
|
91
|
+
const items = new Array(9).fill(ITEM_WIDTH * ITEM_HEIGHT)
|
|
92
|
+
|
|
93
|
+
const VERTICAL_SPACING = 100
|
|
94
|
+
|
|
95
|
+
const layout = new Layout({
|
|
96
|
+
getPrerenderMargin: () => scrollableContainer.height,
|
|
97
|
+
getVerticalSpacing: () => VERTICAL_SPACING,
|
|
98
|
+
getColumnsCount: () => 1,
|
|
99
|
+
getItemHeight: (i) => ITEM_HEIGHT,
|
|
100
|
+
getBeforeResizeItemsCount: () => 0,
|
|
101
|
+
getAverageItemHeight: () => ITEM_HEIGHT,
|
|
102
|
+
getScrollableContainerHeight: () => scrollableContainer.height
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
layout.getLayoutUpdateForItemsDiff(
|
|
106
|
+
{
|
|
107
|
+
firstShownItemIndex: 3,
|
|
108
|
+
lastShownItemIndex: 5,
|
|
109
|
+
beforeItemsHeight: 3 * (ITEM_HEIGHT + VERTICAL_SPACING),
|
|
110
|
+
afterItemsHeight: 3 * (ITEM_HEIGHT + VERTICAL_SPACING)
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
prependedItemsCount: 5,
|
|
114
|
+
appendedItemsCount: 5
|
|
115
|
+
}, {
|
|
116
|
+
itemsCount: 5 + 5 + items.length,
|
|
117
|
+
columnsCount: 1
|
|
118
|
+
}
|
|
119
|
+
).should.deep.equal({
|
|
120
|
+
firstShownItemIndex: 5 + 3,
|
|
121
|
+
lastShownItemIndex: 5 + 5,
|
|
122
|
+
beforeItemsHeight: (5 + 3) * (ITEM_HEIGHT + VERTICAL_SPACING),
|
|
123
|
+
afterItemsHeight: (3 + 5) * (ITEM_HEIGHT + VERTICAL_SPACING)
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('should update layout for items incremental change (rows get rebalanced)', function() {
|
|
128
|
+
const scrollableContainer = {
|
|
129
|
+
width: 800,
|
|
130
|
+
height: 400
|
|
131
|
+
}
|
|
132
|
+
const ITEM_WIDTH = scrollableContainer.width
|
|
133
|
+
const ITEM_HEIGHT = 400
|
|
134
|
+
|
|
135
|
+
const items = new Array(9).fill(ITEM_WIDTH * ITEM_HEIGHT)
|
|
136
|
+
|
|
137
|
+
const VERTICAL_SPACING = 100
|
|
138
|
+
|
|
139
|
+
const layout = new Layout({
|
|
140
|
+
getPrerenderMargin: () => scrollableContainer.height,
|
|
141
|
+
getVerticalSpacing: () => VERTICAL_SPACING,
|
|
142
|
+
getColumnsCount: () => 4,
|
|
143
|
+
getItemHeight: () => ITEM_HEIGHT,
|
|
144
|
+
getBeforeResizeItemsCount: () => 0,
|
|
145
|
+
getAverageItemHeight: () => ITEM_HEIGHT,
|
|
146
|
+
getScrollableContainerHeight: () => scrollableContainer.height
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
layout.getLayoutUpdateForItemsDiff(
|
|
150
|
+
{
|
|
151
|
+
firstShownItemIndex: 3,
|
|
152
|
+
lastShownItemIndex: 5,
|
|
153
|
+
beforeItemsHeight: 3 * (ITEM_HEIGHT + VERTICAL_SPACING),
|
|
154
|
+
afterItemsHeight: 3 * (ITEM_HEIGHT + VERTICAL_SPACING)
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
prependedItemsCount: 5,
|
|
158
|
+
appendedItemsCount: 5
|
|
159
|
+
}, {
|
|
160
|
+
itemsCount: 5 + 5 + items.length,
|
|
161
|
+
columnsCount: 4,
|
|
162
|
+
shouldRestoreScrollPosition: true
|
|
163
|
+
}
|
|
164
|
+
).should.deep.equal({
|
|
165
|
+
firstShownItemIndex: 0,
|
|
166
|
+
lastShownItemIndex: 5 + 5,
|
|
167
|
+
beforeItemsHeight: 0,
|
|
168
|
+
afterItemsHeight: 5 * (ITEM_HEIGHT + VERTICAL_SPACING)
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
})
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
export default class ListHeightChangeWatcher {
|
|
2
|
+
constructor({
|
|
3
|
+
itemsContainer,
|
|
4
|
+
getListTopOffset
|
|
5
|
+
}) {
|
|
6
|
+
this.itemsContainer = itemsContainer
|
|
7
|
+
this.getListTopOffset = getListTopOffset
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* `<ReactVirtualScroller/>` calls this method.
|
|
12
|
+
* @param {any[]} previousItems
|
|
13
|
+
* @param {any[]} newItems
|
|
14
|
+
* @param {number} prependedItemsCount
|
|
15
|
+
*/
|
|
16
|
+
snapshot({
|
|
17
|
+
previousItems,
|
|
18
|
+
newItems,
|
|
19
|
+
prependedItemsCount
|
|
20
|
+
}) {
|
|
21
|
+
// If there were no items in the list
|
|
22
|
+
// then there's no point in restoring scroll position.
|
|
23
|
+
if (previousItems.length === 0) {
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
// If no items were prepended then no need to restore scroll position.
|
|
27
|
+
if (prependedItemsCount === 0) {
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
// The first item is supposed to be shown when the user clicks
|
|
31
|
+
// "Show previous items" button. If it isn't shown though,
|
|
32
|
+
// could still calculate the first item's top position using
|
|
33
|
+
// the values from `itemHeights` and `verticalSpacing`.
|
|
34
|
+
// But that would be a weird non-realistic scenario.
|
|
35
|
+
// if (firstShownItemIndex > 0) {
|
|
36
|
+
// let i = firstShownItemIndex - 1
|
|
37
|
+
// while (i >= 0) {
|
|
38
|
+
// firstItemTopOffset += itemHeights[i] + verticalSpacing
|
|
39
|
+
// i--
|
|
40
|
+
// }
|
|
41
|
+
// }
|
|
42
|
+
// If the scroll position has already been captured for restoration,
|
|
43
|
+
// then don't capture it the second time.
|
|
44
|
+
// Capturing scroll position could happen when using `<ReactVirtualScroller/>`
|
|
45
|
+
// because it calls `ListHeightChangeWatcher.snapshot()` inside `ReactVirtualScroller.render()`
|
|
46
|
+
// which is followed by `<VirtualScroller/>`'s `.componentDidUpdate()`
|
|
47
|
+
// that also calls `ListHeightChangeWatcher.snapshot()` with the same arguments,
|
|
48
|
+
// so that second call to `ListHeightChangeWatcher.snapshot()` is ignored.
|
|
49
|
+
// Calling `ListHeightChangeWatcher.snapshot()` inside `ReactVirtualScroller.render()`
|
|
50
|
+
// is done to prevent scroll Y position from jumping
|
|
51
|
+
// when showing the first page of the "Previous items".
|
|
52
|
+
// See the long section of comments in `ReactVirtualScroller.render()`
|
|
53
|
+
// method for more info on why is `ListHeightChangeWatcher.snapshot()` called there.
|
|
54
|
+
if (this._snapshot &&
|
|
55
|
+
this._snapshot.previousItems === previousItems &&
|
|
56
|
+
this._snapshot.newItems === newItems) {
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
this._snapshot = {
|
|
60
|
+
previousItems,
|
|
61
|
+
newItems,
|
|
62
|
+
itemIndex: prependedItemsCount,
|
|
63
|
+
itemTopOffset: this.itemsContainer.getNthRenderedItemTopOffset(0),
|
|
64
|
+
// Snapshot list top offset inside the scrollable container too
|
|
65
|
+
// because it's common to hide the "Show previous items" button
|
|
66
|
+
// when the user has browsed to the top of the list, which causes
|
|
67
|
+
// the list's top position to shift upwards due to the button
|
|
68
|
+
// no longer being rendered. Tracking list top offset doesn't
|
|
69
|
+
// fit here that well, but it makes sense in real-world applications.
|
|
70
|
+
listTopOffset: this.getListTopOffset()
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
getAnchorItemIndex() {
|
|
75
|
+
return this._snapshot.itemIndex
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
hasSnapshot() {
|
|
79
|
+
return this._snapshot !== undefined
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
getListBottomOffsetChange() {
|
|
83
|
+
const { itemIndex, itemTopOffset, listTopOffset } = this._snapshot
|
|
84
|
+
// `firstShownItemIndex` is supposed to be `0` at this point,
|
|
85
|
+
// so `renderedElementIndex` would be the same as the `itemIndex`.
|
|
86
|
+
const itemTopOffsetNew = this.itemsContainer.getNthRenderedItemTopOffset(itemIndex)
|
|
87
|
+
const listTopOffsetNew = this.getListTopOffset()
|
|
88
|
+
return (itemTopOffsetNew - itemTopOffset) + (listTopOffsetNew - listTopOffset)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
reset() {
|
|
92
|
+
this._snapshot = undefined
|
|
93
|
+
}
|
|
94
|
+
}
|
package/source/Resize.js
CHANGED
|
@@ -1,19 +1,28 @@
|
|
|
1
|
-
import { LAYOUT_REASON } from './Layout'
|
|
2
1
|
import debounce from './utility/debounce'
|
|
2
|
+
import log from './utility/debug'
|
|
3
3
|
|
|
4
4
|
export default class Resize {
|
|
5
5
|
constructor({
|
|
6
6
|
bypass,
|
|
7
7
|
scrollableContainer,
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
onStart,
|
|
9
|
+
onStop,
|
|
10
|
+
onHeightChange,
|
|
11
|
+
onWidthChange,
|
|
12
|
+
onNoChange
|
|
11
13
|
}) {
|
|
12
14
|
this.bypass = bypass
|
|
13
15
|
this.scrollableContainer = scrollableContainer
|
|
14
|
-
|
|
15
|
-
this.
|
|
16
|
-
this.
|
|
16
|
+
|
|
17
|
+
this.onHeightChange = onHeightChange
|
|
18
|
+
this.onWidthChange = onWidthChange
|
|
19
|
+
this.onNoChange = onNoChange
|
|
20
|
+
|
|
21
|
+
this.onResize = debounce(
|
|
22
|
+
this._onResize,
|
|
23
|
+
SCROLLABLE_CONTAINER_RESIZE_DEBOUNCE_INTERVAL,
|
|
24
|
+
{ onStart, onStop }
|
|
25
|
+
)
|
|
17
26
|
}
|
|
18
27
|
|
|
19
28
|
listen() {
|
|
@@ -23,22 +32,21 @@ export default class Resize {
|
|
|
23
32
|
this.isRendered = true
|
|
24
33
|
this.scrollableContainerWidth = this.scrollableContainer.getWidth()
|
|
25
34
|
this.scrollableContainerHeight = this.scrollableContainer.getHeight()
|
|
26
|
-
this.scrollableContainerUnlistenResize = this.scrollableContainer.onResize(this.onResize
|
|
27
|
-
container: this.getContainerElement()
|
|
28
|
-
})
|
|
35
|
+
this.scrollableContainerUnlistenResize = this.scrollableContainer.onResize(this.onResize)
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
stop() {
|
|
32
39
|
this.isRendered = false
|
|
33
40
|
if (this.scrollableContainerUnlistenResize) {
|
|
34
41
|
this.scrollableContainerUnlistenResize()
|
|
42
|
+
this.scrollableContainerUnlistenResize = undefined
|
|
35
43
|
}
|
|
36
44
|
}
|
|
37
45
|
|
|
38
46
|
/**
|
|
39
47
|
* On scrollable container resize.
|
|
40
48
|
*/
|
|
41
|
-
|
|
49
|
+
_onResize = () => {
|
|
42
50
|
// If `VirtualScroller` has been unmounted
|
|
43
51
|
// while `debounce()`'s `setTimeout()` was waiting, then exit.
|
|
44
52
|
if (!this.isRendered) {
|
|
@@ -52,21 +60,21 @@ export default class Resize {
|
|
|
52
60
|
if (this.scrollableContainerHeight === prevScrollableContainerHeight) {
|
|
53
61
|
// The dimensions of the container didn't change,
|
|
54
62
|
// so there's no need to re-layout anything.
|
|
55
|
-
|
|
63
|
+
this.onNoChange()
|
|
56
64
|
} else {
|
|
57
65
|
// Scrollable container height has changed,
|
|
58
66
|
// so just recalculate shown item indexes.
|
|
59
67
|
// No need to perform a re-layout from scratch.
|
|
60
|
-
this.
|
|
68
|
+
this.onHeightChange(prevScrollableContainerHeight, this.scrollableContainerHeight)
|
|
61
69
|
}
|
|
62
70
|
} else {
|
|
63
71
|
// Reset item heights, because if scrollable container's width (or height)
|
|
64
72
|
// has changed, then the list width (or height) most likely also has changed,
|
|
65
73
|
// and also some CSS `@media()` rules might have been added or removed.
|
|
66
74
|
// So re-render the list entirely.
|
|
67
|
-
this.
|
|
75
|
+
this.onWidthChange(prevScrollableContainerWidth, this.scrollableContainerWidth)
|
|
68
76
|
}
|
|
69
|
-
}
|
|
77
|
+
}
|
|
70
78
|
}
|
|
71
79
|
|
|
72
80
|
const SCROLLABLE_CONTAINER_RESIZE_DEBOUNCE_INTERVAL = 250
|
package/source/Scroll.js
CHANGED
|
@@ -4,39 +4,39 @@
|
|
|
4
4
|
// https://github.com/bvaughn/react-virtualized/issues/722
|
|
5
5
|
import { setTimeout, clearTimeout } from 'request-animation-frame-timeout'
|
|
6
6
|
|
|
7
|
-
import { LAYOUT_REASON } from './Layout'
|
|
8
7
|
import log from './utility/debug'
|
|
9
8
|
|
|
10
9
|
export default class Scroll {
|
|
11
10
|
constructor({
|
|
12
11
|
bypass,
|
|
13
12
|
scrollableContainer,
|
|
14
|
-
|
|
13
|
+
itemsContainer,
|
|
14
|
+
onScroll,
|
|
15
15
|
initialScrollPosition,
|
|
16
16
|
onScrollPositionChange,
|
|
17
17
|
isImmediateLayoutScheduled,
|
|
18
18
|
hasNonRenderedItemsAtTheTop,
|
|
19
19
|
hasNonRenderedItemsAtTheBottom,
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
getLatestLayoutVisibleArea,
|
|
21
|
+
getListTopOffset,
|
|
22
|
+
getPrerenderMargin,
|
|
23
|
+
onScrolledToTop,
|
|
24
|
+
waitForScrollingToStop
|
|
22
25
|
}) {
|
|
23
26
|
this.bypass = bypass
|
|
24
27
|
this.scrollableContainer = scrollableContainer
|
|
25
|
-
this.
|
|
28
|
+
this.itemsContainer = itemsContainer
|
|
29
|
+
this.onScroll = onScroll
|
|
26
30
|
this.initialScrollPosition = initialScrollPosition
|
|
27
31
|
this.onScrollPositionChange = onScrollPositionChange
|
|
28
32
|
this.isImmediateLayoutScheduled = isImmediateLayoutScheduled
|
|
29
33
|
this.hasNonRenderedItemsAtTheTop = hasNonRenderedItemsAtTheTop
|
|
30
34
|
this.hasNonRenderedItemsAtTheBottom = hasNonRenderedItemsAtTheBottom
|
|
31
|
-
this.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
scrollableContainerContentHeight: scrollableContainer.getContentHeight()
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
35
|
+
this.getLatestLayoutVisibleArea = getLatestLayoutVisibleArea
|
|
36
|
+
this.getListTopOffset = getListTopOffset
|
|
37
|
+
this.getPrerenderMargin = getPrerenderMargin
|
|
38
|
+
this.onScrolledToTop = onScrolledToTop
|
|
39
|
+
this.waitForScrollingToStop = waitForScrollingToStop
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
listen() {
|
|
@@ -44,32 +44,36 @@ export default class Scroll {
|
|
|
44
44
|
this.scrollToY(this.initialScrollPosition)
|
|
45
45
|
}
|
|
46
46
|
if (this.onScrollPositionChange) {
|
|
47
|
-
this.
|
|
48
|
-
this.removeScrollPositionListener = this.scrollableContainer.addScrollListener(this.updateScrollPosition)
|
|
49
|
-
}
|
|
50
|
-
if (!this.bypass) {
|
|
51
|
-
this.removeScrollListener = this.scrollableContainer.addScrollListener(this.onScroll)
|
|
52
|
-
}
|
|
53
|
-
if (this.preserveScrollPositionOfTheBottomOfTheListOnMount) {
|
|
54
|
-
this.scrollToY(this.getScrollY() + (this.scrollableContainer.getContentHeight() - this.preserveScrollPositionOfTheBottomOfTheListOnMount.scrollableContainerContentHeight))
|
|
47
|
+
this.onScrollPositionChange(this.getScrollY())
|
|
55
48
|
}
|
|
49
|
+
this.stopListeningToScroll = this.scrollableContainer.onScroll(this.onScrollListener)
|
|
56
50
|
}
|
|
57
51
|
|
|
58
52
|
stop() {
|
|
59
|
-
if (this.
|
|
60
|
-
this.
|
|
53
|
+
if (this.stopReportingScrollPositionChange) {
|
|
54
|
+
this.stopReportingScrollPositionChange()
|
|
55
|
+
this.stopReportingScrollPositionChange = undefined
|
|
56
|
+
}
|
|
57
|
+
if (this.stopListeningToScroll) {
|
|
58
|
+
this.stopListeningToScroll()
|
|
59
|
+
this.stopListeningToScroll = undefined
|
|
60
|
+
}
|
|
61
|
+
if (this.onStopScrollingListener) {
|
|
62
|
+
this.onStopScrollingListener = undefined
|
|
61
63
|
}
|
|
62
|
-
if (this.
|
|
63
|
-
this.
|
|
64
|
+
if (this.onScrollOnStopScrolling) {
|
|
65
|
+
this.onScrollOnStopScrolling = undefined
|
|
64
66
|
}
|
|
65
|
-
this.
|
|
67
|
+
this.cancelOnStopScrollingTimer()
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
scrollToY(scrollY) {
|
|
71
|
+
this.ignoreScrollEvents = true
|
|
69
72
|
this.scrollableContainer.scrollToY(scrollY)
|
|
73
|
+
this.ignoreScrollEvents = undefined
|
|
70
74
|
}
|
|
71
75
|
|
|
72
|
-
scrollByY(scrollByY) {
|
|
76
|
+
scrollByY = (scrollByY) => {
|
|
73
77
|
this.scrollToY(this.getScrollY() + scrollByY)
|
|
74
78
|
}
|
|
75
79
|
|
|
@@ -77,86 +81,143 @@ export default class Scroll {
|
|
|
77
81
|
return this.scrollableContainer.getScrollY()
|
|
78
82
|
}
|
|
79
83
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
this.onScrollPositionChange(this.getScrollY())
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
cancelOnUserStopsScrollingTimer() {
|
|
88
|
-
if (this.onUserStopsScrollingTimer) {
|
|
89
|
-
clearTimeout(this.onUserStopsScrollingTimer)
|
|
90
|
-
this.onUserStopsScrollingTimer = undefined
|
|
84
|
+
cancelOnStopScrollingTimer() {
|
|
85
|
+
if (this.onStopScrollingTimer) {
|
|
86
|
+
clearTimeout(this.onStopScrollingTimer)
|
|
87
|
+
this.onStopScrollingTimer = undefined
|
|
91
88
|
}
|
|
92
89
|
}
|
|
93
90
|
|
|
94
|
-
|
|
91
|
+
cancelScheduledLayout() {
|
|
95
92
|
// Cancel a "re-layout when user stops scrolling" timer.
|
|
96
|
-
this.
|
|
93
|
+
this.cancelOnStopScrollingTimer()
|
|
97
94
|
}
|
|
98
95
|
|
|
99
|
-
|
|
96
|
+
onScrollListener = () => {
|
|
97
|
+
if (this.onScrollPositionChange) {
|
|
98
|
+
this.onScrollPositionChange(this.getScrollY())
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// If the user has scrolled up to the top of the items container.
|
|
102
|
+
// (this option isn't currently used)
|
|
103
|
+
if (this.onScrolledToTop) {
|
|
104
|
+
if (this.getScrollY() < this.getListTopOffset()) {
|
|
105
|
+
this.onScrolledToTop()
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (this.bypass) {
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (this.ignoreScrollEvents) {
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
|
|
100
117
|
// Prefer not performing a re-layout while the user is scrolling (if possible).
|
|
101
118
|
// If the user doesn't scroll too far and then stops for a moment,
|
|
102
119
|
// then a mid-scroll re-layout could be delayed until such a brief stop:
|
|
103
120
|
// presumably, this results in better (smoother) scrolling performance,
|
|
104
121
|
// delaying the work to when it doesn't introduce any stutter or "jank".
|
|
105
122
|
|
|
106
|
-
// Reset `this.
|
|
107
|
-
this.
|
|
123
|
+
// Reset `this.onStopScrollingTimer` (will be re-created below).
|
|
124
|
+
this.cancelOnStopScrollingTimer()
|
|
108
125
|
|
|
109
|
-
// See
|
|
110
|
-
//
|
|
126
|
+
// See if the latest "layout" (the currently rendered set of items)
|
|
127
|
+
// is still sufficient in order to show all the items that're
|
|
128
|
+
// currently inside the viewport. If there're some non-rendered items
|
|
129
|
+
// that're visible in the current viewport, then those items
|
|
130
|
+
// should be rendered "immediately" rather than waiting until
|
|
131
|
+
// the user stops scrolling.
|
|
111
132
|
const forceUpdate =
|
|
112
133
|
// If the items have been rendered at least once
|
|
113
|
-
this.
|
|
134
|
+
this.getLatestLayoutVisibleArea() && (
|
|
114
135
|
(
|
|
115
|
-
// If the user has scrolled up past the
|
|
116
|
-
|
|
117
|
-
//
|
|
136
|
+
// If the user has scrolled up past the "prerender margin"
|
|
137
|
+
// and there're some non-rendered items at the top,
|
|
138
|
+
// then force a re-layout.
|
|
139
|
+
//
|
|
140
|
+
// (during these calculations we assume that the list's top coordinate
|
|
141
|
+
// hasn't changed since previous layout; even if that's not exactly true,
|
|
142
|
+
// the items will be re-layout when the user stops scrolling anyway)
|
|
143
|
+
//
|
|
144
|
+
(this.getScrollY() < this.getLatestLayoutVisibleArea().top - this.getPrerenderMargin()) &&
|
|
118
145
|
this.hasNonRenderedItemsAtTheTop()
|
|
119
146
|
)
|
|
120
147
|
||
|
|
121
148
|
(
|
|
122
|
-
// If the user has scrolled down past the
|
|
123
|
-
|
|
124
|
-
//
|
|
149
|
+
// If the user has scrolled down past the "prerender margin"
|
|
150
|
+
// and there're any non-rendered items left at the end,
|
|
151
|
+
// then force a re-layout.
|
|
152
|
+
//
|
|
153
|
+
// (during these calculations we assume that the list's top coordinate
|
|
154
|
+
// hasn't changed since previous layout; even if that's not exactly true,
|
|
155
|
+
// the items will be re-layout when the user stops scrolling anyway)
|
|
156
|
+
//
|
|
157
|
+
(this.getScrollY() + this.scrollableContainer.getHeight() > this.getLatestLayoutVisibleArea().bottom + this.getPrerenderMargin()) &&
|
|
125
158
|
this.hasNonRenderedItemsAtTheBottom()
|
|
126
159
|
)
|
|
127
160
|
)
|
|
128
161
|
|
|
129
162
|
if (forceUpdate) {
|
|
130
|
-
log('The user has scrolled far enough:
|
|
163
|
+
log('The user has scrolled far enough: perform a re-layout')
|
|
131
164
|
} else {
|
|
132
|
-
log('The user
|
|
165
|
+
log('The user is scrolling: perform a re-layout when they stop scrolling')
|
|
133
166
|
}
|
|
134
167
|
|
|
135
|
-
if (
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
() => {
|
|
143
|
-
this.onUserStopsScrollingTimer = undefined
|
|
144
|
-
this.updateLayout({ reason: LAYOUT_REASON.STOPPED_SCROLLING })
|
|
145
|
-
},
|
|
146
|
-
// "scroll" events are usually dispatched every 16 milliseconds
|
|
147
|
-
// for 60fps refresh rate, so waiting for 100 milliseconds feels
|
|
148
|
-
// reasonable: that would be about 6 frames of inactivity period,
|
|
149
|
-
// which could mean that either the user has stopped scrolling
|
|
150
|
-
// (for a moment) or the browser is lagging and stuttering
|
|
151
|
-
// (skipping frames due to high load).
|
|
152
|
-
// If the user continues scrolling then this timeout is constantly
|
|
153
|
-
// refreshed (cancelled and then re-created).
|
|
154
|
-
WAIT_FOR_USER_TO_STOP_SCROLLING_TIMEOUT
|
|
155
|
-
)
|
|
168
|
+
if (forceUpdate || this.waitForScrollingToStop === false) {
|
|
169
|
+
return this.onScroll()
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// If a re-layout is already scheduled at the next "frame",
|
|
173
|
+
// don't schedule a "re-layout when user stops scrolling" timer.
|
|
174
|
+
if (this.isImmediateLayoutScheduled()) {
|
|
156
175
|
return
|
|
157
176
|
}
|
|
158
177
|
|
|
159
|
-
this.
|
|
178
|
+
this.onScrollOnStopScrolling = true
|
|
179
|
+
this.watchOnStopScrolling()
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
watchOnStopScrolling() {
|
|
183
|
+
this.onStopScrollingTimer = setTimeout(
|
|
184
|
+
() => {
|
|
185
|
+
this.onStopScrollingTimer = undefined
|
|
186
|
+
|
|
187
|
+
if (this.onScrollOnStopScrolling) {
|
|
188
|
+
this.onScrollOnStopScrolling = undefined
|
|
189
|
+
this.onScroll({ delayed: true })
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (this.onStopScrollingListener) {
|
|
193
|
+
const onStopScrollingListener = this.onStopScrollingListener
|
|
194
|
+
this.onStopScrollingListener = undefined
|
|
195
|
+
// `onStopScrollingListener()` may hypothetically schedule
|
|
196
|
+
// another `onStopScrolling()` listener, so set
|
|
197
|
+
// `this.onStopScrollingListener` to `undefined` before
|
|
198
|
+
// calling it rather than after.
|
|
199
|
+
log('~ The user has stopped scrolling ~')
|
|
200
|
+
onStopScrollingListener()
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
// "scroll" events are usually dispatched every 16 milliseconds
|
|
204
|
+
// for 60fps refresh rate, so waiting for 100 milliseconds feels
|
|
205
|
+
// reasonable: that would be about 6 frames of inactivity period,
|
|
206
|
+
// which could mean that either the user has stopped scrolling
|
|
207
|
+
// (for a moment) or the browser is lagging and stuttering
|
|
208
|
+
// (skipping frames due to high load).
|
|
209
|
+
// If the user continues scrolling then this timeout is constantly
|
|
210
|
+
// refreshed (cancelled and then re-created).
|
|
211
|
+
ON_STOP_SCROLLING_INACTIVE_PERIOD
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// (this function isn't currently used)
|
|
216
|
+
onStopScrolling(onStopScrollingListener) {
|
|
217
|
+
this.onStopScrollingListener = onStopScrollingListener
|
|
218
|
+
if (!this.onStopScrollingTimer) {
|
|
219
|
+
this.watchOnStopScrolling()
|
|
220
|
+
}
|
|
160
221
|
}
|
|
161
222
|
|
|
162
223
|
/**
|
|
@@ -174,4 +235,4 @@ export default class Scroll {
|
|
|
174
235
|
}
|
|
175
236
|
}
|
|
176
237
|
|
|
177
|
-
const
|
|
238
|
+
const ON_STOP_SCROLLING_INACTIVE_PERIOD = 100
|