virtual-scroller 1.7.9 → 1.8.0
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/CHANGELOG.md +14 -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 +48 -80
- 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 +13 -20
- 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 +47 -79
- 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 +8 -19
- 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 +31 -55
- package/source/DOM/VirtualScroller.js +6 -7
- package/source/ItemHeights.js +10 -15
- 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 +1240 -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
|
@@ -11,42 +11,36 @@ import {
|
|
|
11
11
|
setTbodyPadding
|
|
12
12
|
} from './DOM/tbody'
|
|
13
13
|
|
|
14
|
-
import
|
|
15
|
-
import WaitForStylesToLoad from './DOM/WaitForStylesToLoad'
|
|
14
|
+
import DOMEngine from './DOM/Engine'
|
|
16
15
|
|
|
17
16
|
import Layout, { LAYOUT_REASON } from './Layout'
|
|
18
17
|
import Resize from './Resize'
|
|
18
|
+
import BeforeResize from './BeforeResize'
|
|
19
19
|
import Scroll from './Scroll'
|
|
20
|
-
import
|
|
20
|
+
import ListHeightChangeWatcher from './ListHeightChangeWatcher'
|
|
21
21
|
import ItemHeights from './ItemHeights'
|
|
22
22
|
import getItemsDiff from './getItemsDiff'
|
|
23
23
|
import getVerticalSpacing from './getVerticalSpacing'
|
|
24
|
-
// import getItemCoordinates from './getItemCoordinates'
|
|
25
24
|
|
|
26
25
|
import log, { warn, isDebug, reportError } from './utility/debug'
|
|
27
26
|
import shallowEqual from './utility/shallowEqual'
|
|
27
|
+
import getStateSnapshot from './utility/getStateSnapshot'
|
|
28
28
|
|
|
29
29
|
export default class VirtualScroller {
|
|
30
30
|
/**
|
|
31
|
-
* @param {function}
|
|
31
|
+
* @param {function} getItemsContainerElement — Returns the container DOM `Element`.
|
|
32
32
|
* @param {any[]} items — The list of items.
|
|
33
33
|
* @param {Object} [options] — See README.md.
|
|
34
34
|
* @return {VirtualScroller}
|
|
35
35
|
*/
|
|
36
36
|
constructor(
|
|
37
|
-
|
|
37
|
+
getItemsContainerElement,
|
|
38
38
|
items,
|
|
39
39
|
options = {}
|
|
40
40
|
) {
|
|
41
41
|
const {
|
|
42
|
-
getState,
|
|
43
|
-
setState,
|
|
44
42
|
onStateChange,
|
|
45
43
|
customState,
|
|
46
|
-
// `preserveScrollPositionAtBottomOnMount` option name is deprecated,
|
|
47
|
-
// use `preserveScrollPositionOfTheBottomOfTheListOnMount` option instead.
|
|
48
|
-
preserveScrollPositionAtBottomOnMount,
|
|
49
|
-
preserveScrollPositionOfTheBottomOfTheListOnMount,
|
|
50
44
|
initialScrollPosition,
|
|
51
45
|
onScrollPositionChange,
|
|
52
46
|
measureItemsBatchSize,
|
|
@@ -57,12 +51,18 @@ export default class VirtualScroller {
|
|
|
57
51
|
getItemId,
|
|
58
52
|
tbody,
|
|
59
53
|
_useTimeoutInRenderLoop,
|
|
54
|
+
_waitForScrollingToStop,
|
|
60
55
|
// bypassBatchSize
|
|
61
56
|
} = options
|
|
62
57
|
|
|
58
|
+
let {
|
|
59
|
+
getState,
|
|
60
|
+
setState
|
|
61
|
+
} = options
|
|
62
|
+
|
|
63
63
|
let {
|
|
64
64
|
bypass,
|
|
65
|
-
//
|
|
65
|
+
// prerenderMargin,
|
|
66
66
|
estimatedItemHeight,
|
|
67
67
|
// getItemState,
|
|
68
68
|
onItemInitialRender,
|
|
@@ -70,7 +70,7 @@ export default class VirtualScroller {
|
|
|
70
70
|
onItemFirstRender,
|
|
71
71
|
scrollableContainer,
|
|
72
72
|
state,
|
|
73
|
-
|
|
73
|
+
engine
|
|
74
74
|
} = options
|
|
75
75
|
|
|
76
76
|
log('~ Initialize ~')
|
|
@@ -89,25 +89,48 @@ export default class VirtualScroller {
|
|
|
89
89
|
|
|
90
90
|
// Could support non-DOM rendering engines.
|
|
91
91
|
// For example, React Native, `<canvas/>`, etc.
|
|
92
|
-
if (!
|
|
93
|
-
|
|
92
|
+
if (!engine) {
|
|
93
|
+
engine = DOMEngine
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Sometimes, when `new VirtualScroller()` instance is created,
|
|
97
|
+
// `getItemsContainerElement()` might not be ready to return the "container" DOM Element yet
|
|
98
|
+
// (for example, because it's not rendered yet). That's the reason why it's a getter function.
|
|
99
|
+
// For example, in React `<VirtualScroller/>` component, a `VirtualScroller`
|
|
100
|
+
// instance is created in the React component's `constructor()`, and at that time
|
|
101
|
+
// the container Element is not yet available. The container Element is available
|
|
102
|
+
// in `componentDidMount()`, but `componentDidMount()` is not executed on server,
|
|
103
|
+
// which would mean that React `<VirtualScroller/>` wouldn't render at all
|
|
104
|
+
// on server side, while with the `getItemsContainerElement()` approach, on server side,
|
|
105
|
+
// it still "renders" a list with a predefined amount of items in it by default.
|
|
106
|
+
// (`initiallyRenderedItemsCount`, or `1`).
|
|
107
|
+
this.getItemsContainerElement = getItemsContainerElement
|
|
108
|
+
this.itemsContainer = engine.createItemsContainer(getItemsContainerElement)
|
|
109
|
+
|
|
110
|
+
// Remove any accidental text nodes from container (like whitespace).
|
|
111
|
+
// Also guards against cases when someone accidentally tries
|
|
112
|
+
// using `VirtualScroller` on a non-empty element.
|
|
113
|
+
if (getItemsContainerElement()) {
|
|
114
|
+
this.itemsContainer.clear()
|
|
94
115
|
}
|
|
95
116
|
|
|
96
|
-
this.
|
|
97
|
-
|
|
117
|
+
this.scrollableContainer = engine.createScrollableContainer(
|
|
118
|
+
scrollableContainer,
|
|
119
|
+
getItemsContainerElement
|
|
120
|
+
)
|
|
98
121
|
|
|
99
|
-
// if (
|
|
100
|
-
// // Renders items which are outside of the screen by this "margin".
|
|
122
|
+
// if (prerenderMargin === undefined) {
|
|
123
|
+
// // Renders items which are outside of the screen by this "prerender margin".
|
|
101
124
|
// // Is the screen height by default: seems to be the optimal value
|
|
102
125
|
// // for "Page Up" / "Page Down" navigation and optimized mouse wheel scrolling.
|
|
103
|
-
//
|
|
126
|
+
// prerenderMargin = this.scrollableContainer ? this.scrollableContainer.getHeight() : 0
|
|
104
127
|
// }
|
|
105
128
|
|
|
106
129
|
// Work around `<tbody/>` not being able to have `padding`.
|
|
107
130
|
// https://gitlab.com/catamphetamine/virtual-scroller/-/issues/1
|
|
108
131
|
if (tbody) {
|
|
109
|
-
if (
|
|
110
|
-
throw new Error('`tbody` option is only supported for DOM rendering engine')
|
|
132
|
+
if (engine !== DOMEngine) {
|
|
133
|
+
throw new Error('[virtual-scroller] `tbody` option is only supported for DOM rendering engine')
|
|
111
134
|
}
|
|
112
135
|
log('~ <tbody/> detected ~')
|
|
113
136
|
this.tbody = true
|
|
@@ -150,7 +173,7 @@ export default class VirtualScroller {
|
|
|
150
173
|
}
|
|
151
174
|
|
|
152
175
|
this.initialItems = items
|
|
153
|
-
// this.
|
|
176
|
+
// this.prerenderMargin = prerenderMargin
|
|
154
177
|
|
|
155
178
|
this.onStateChange = onStateChange
|
|
156
179
|
|
|
@@ -186,20 +209,33 @@ export default class VirtualScroller {
|
|
|
186
209
|
log('Estimated item height', estimatedItemHeight)
|
|
187
210
|
}
|
|
188
211
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
212
|
+
// There're three main places where state is updated:
|
|
213
|
+
//
|
|
214
|
+
// * On scroll.
|
|
215
|
+
// * On window resize.
|
|
216
|
+
// * On set new items.
|
|
217
|
+
//
|
|
218
|
+
// State updates may be "asynchronous" (like in React), in which case the
|
|
219
|
+
// corresponding operation is "pending" until the state update is applied.
|
|
220
|
+
//
|
|
221
|
+
// If there's a "pending" window resize or a "pending" update of the set of items,
|
|
222
|
+
// then "on scroll" updates aren't dispatched.
|
|
223
|
+
//
|
|
224
|
+
// If there's a "pending" on scroll update and the window is resize or a new set
|
|
225
|
+
// of items is set, then that "pending" on scroll update gets overwritten.
|
|
226
|
+
//
|
|
227
|
+
// If there's a "pending" update of the set of items, then window resize handler
|
|
228
|
+
// sees that "pending" update and dispatches its own state update so that the
|
|
229
|
+
// "pending" state update originating from `setItems()` is not lost.
|
|
230
|
+
//
|
|
231
|
+
// If there's a "pending" window resize, and a new set of items is set,
|
|
232
|
+
// then the state update of the window resize handler gets overwritten.
|
|
233
|
+
|
|
234
|
+
// Create default `getState()`/`setState()` functions.
|
|
235
|
+
if (!getState) {
|
|
236
|
+
getState = () => this.state
|
|
237
|
+
setState = (stateUpdate, { willUpdateState, didUpdateState }) => {
|
|
238
|
+
const prevState = getState()
|
|
203
239
|
// Because this variant of `.setState()` is "synchronous" (immediate),
|
|
204
240
|
// it can be written like `...prevState`, and no state updates would be lost.
|
|
205
241
|
// But if it was "asynchronous" (not immediate), then `...prevState`
|
|
@@ -208,40 +244,82 @@ export default class VirtualScroller {
|
|
|
208
244
|
// the state actually updates, making `prevState` stale.
|
|
209
245
|
const newState = {
|
|
210
246
|
...prevState,
|
|
211
|
-
...
|
|
247
|
+
...stateUpdate
|
|
212
248
|
}
|
|
213
|
-
|
|
249
|
+
willUpdateState(newState, prevState)
|
|
214
250
|
this.state = newState
|
|
215
|
-
|
|
251
|
+
// // Is only used in tests.
|
|
252
|
+
// if (this._onStateUpdate) {
|
|
253
|
+
// this._onStateUpdate(stateUpdate)
|
|
254
|
+
// }
|
|
255
|
+
didUpdateState(prevState)
|
|
216
256
|
}
|
|
217
257
|
}
|
|
218
258
|
|
|
259
|
+
this.getState = getState
|
|
260
|
+
this.setState = (stateUpdate) => {
|
|
261
|
+
if (isDebug()) {
|
|
262
|
+
log('Set state', getStateSnapshot(stateUpdate))
|
|
263
|
+
}
|
|
264
|
+
setState(stateUpdate, {
|
|
265
|
+
willUpdateState: this.willUpdateState,
|
|
266
|
+
didUpdateState: this.didUpdateState
|
|
267
|
+
})
|
|
268
|
+
}
|
|
269
|
+
|
|
219
270
|
if (state) {
|
|
220
|
-
|
|
271
|
+
if (isDebug()) {
|
|
272
|
+
log('Initial state (passed)', getStateSnapshot(state))
|
|
273
|
+
}
|
|
221
274
|
}
|
|
222
275
|
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
this.
|
|
276
|
+
// Check if the current `columnsCount` matches the one from state.
|
|
277
|
+
// For example, a developer might snapshot `VirtualScroller` state
|
|
278
|
+
// when the user navigates from the page containing the list
|
|
279
|
+
// in order to later restore the list's state when the user goes "Back".
|
|
280
|
+
// But, the user might have also resized the window while being on that
|
|
281
|
+
// "other" page, and when they come "Back", their snapshotted state
|
|
282
|
+
// no longer qualifies. Well, it does qualify, but only partially.
|
|
283
|
+
// For example, `itemStates` are still valid, but first and last shown
|
|
284
|
+
// item indexes aren't.
|
|
285
|
+
if (state) {
|
|
286
|
+
let shouldResetLayout
|
|
287
|
+
const columnsCountForState = this.getActualColumnsCountForState()
|
|
288
|
+
if (columnsCountForState !== state.columnsCount) {
|
|
289
|
+
warn('~ Columns Count changed from', state.columnsCount || 1, 'to', columnsCountForState || 1, '~')
|
|
290
|
+
shouldResetLayout = true
|
|
291
|
+
}
|
|
292
|
+
const columnsCount = this.getActualColumnsCount()
|
|
293
|
+
const firstShownItemIndex = Math.floor(state.firstShownItemIndex / columnsCount) * columnsCount
|
|
294
|
+
if (firstShownItemIndex !== state.firstShownItemIndex) {
|
|
295
|
+
warn('~ First Shown Item Index', state.firstShownItemIndex, 'is not divisible by Columns Count', columnsCount, '~')
|
|
296
|
+
shouldResetLayout = true
|
|
297
|
+
}
|
|
298
|
+
if (shouldResetLayout) {
|
|
299
|
+
warn('Reset Layout')
|
|
300
|
+
state = {
|
|
301
|
+
...state,
|
|
302
|
+
...this.getInitialLayoutState(state.items)
|
|
303
|
+
}
|
|
304
|
+
}
|
|
240
305
|
}
|
|
241
306
|
|
|
307
|
+
// Reset `verticalSpacing` so that it re-measures it after the list
|
|
308
|
+
// has been rendered initially. The rationale is that the `state`
|
|
309
|
+
// can't be "trusted" in a sense that the user might have resized
|
|
310
|
+
// their window after the `state` has been snapshotted, and changing
|
|
311
|
+
// window width might have activated different CSS `@media()` "queries"
|
|
312
|
+
// resulting in a potentially different vertical spacing.
|
|
313
|
+
if (state) {
|
|
314
|
+
state = {
|
|
315
|
+
...state,
|
|
316
|
+
verticalSpacing: undefined
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Create `ItemHeights` instance.
|
|
242
321
|
this.itemHeights = new ItemHeights(
|
|
243
|
-
this.
|
|
244
|
-
this.getContainerElement,
|
|
322
|
+
this.itemsContainer,
|
|
245
323
|
(i) => this.getState().itemHeights[i],
|
|
246
324
|
(i, height) => this.getState().itemHeights[i] = height
|
|
247
325
|
)
|
|
@@ -255,62 +333,113 @@ export default class VirtualScroller {
|
|
|
255
333
|
bypass,
|
|
256
334
|
estimatedItemHeight,
|
|
257
335
|
measureItemsBatchSize: measureItemsBatchSize === undefined ? 50 : measureItemsBatchSize,
|
|
336
|
+
getPrerenderMargin: () => this.getPrerenderMargin(),
|
|
258
337
|
getVerticalSpacing: () => this.getVerticalSpacing(),
|
|
338
|
+
getVerticalSpacingBeforeResize: () => this.getVerticalSpacingBeforeResize(),
|
|
259
339
|
getColumnsCount: () => this.getColumnsCount(),
|
|
340
|
+
getColumnsCountBeforeResize: () => this.getState().beforeResize && this.getState().beforeResize.columnsCount,
|
|
260
341
|
getItemHeight: (i) => this.getState().itemHeights[i],
|
|
261
|
-
|
|
342
|
+
getItemHeightBeforeResize: (i) => this.getState().beforeResize && this.getState().beforeResize.itemHeights[i],
|
|
343
|
+
getBeforeResizeItemsCount: () => this.getState().beforeResize ? this.getState().beforeResize.itemHeights.length : 0,
|
|
344
|
+
getAverageItemHeight: () => this.itemHeights.getAverage(),
|
|
345
|
+
getMaxVisibleAreaHeight: () => this.scrollableContainer && this.scrollableContainer.getHeight(),
|
|
346
|
+
//
|
|
347
|
+
// The "previously calculated layout" feature is not currently used.
|
|
348
|
+
//
|
|
349
|
+
// The current layout snapshot could be stored as a "previously calculated layout" variable
|
|
350
|
+
// so that it could theoretically be used when calculating new layout incrementally
|
|
351
|
+
// rather than from scratch, which would be an optimization.
|
|
352
|
+
//
|
|
353
|
+
getPreviouslyCalculatedLayout: () => this.previouslyCalculatedLayout
|
|
262
354
|
})
|
|
263
355
|
|
|
264
356
|
this.resize = new Resize({
|
|
265
357
|
bypass,
|
|
266
358
|
scrollableContainer: this.scrollableContainer,
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
//
|
|
277
|
-
this.
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
359
|
+
onStart: () => {
|
|
360
|
+
log('~ Scrollable container resize started ~')
|
|
361
|
+
this.isResizing = true
|
|
362
|
+
},
|
|
363
|
+
onStop: () => {
|
|
364
|
+
log('~ Scrollable container resize finished ~')
|
|
365
|
+
this.isResizing = undefined
|
|
366
|
+
},
|
|
367
|
+
onNoChange: () => {
|
|
368
|
+
// There might have been some missed `this.onUpdateShownItemIndexes()` calls
|
|
369
|
+
// due to setting `this.isResizing` flag to `true` during the resize.
|
|
370
|
+
// So, update shown item indexes just in case.
|
|
371
|
+
this.onUpdateShownItemIndexes({
|
|
372
|
+
reason: LAYOUT_REASON.VIEWPORT_SIZE_UNCHANGED
|
|
373
|
+
})
|
|
374
|
+
},
|
|
375
|
+
onHeightChange: () => this.onUpdateShownItemIndexes({
|
|
376
|
+
reason: LAYOUT_REASON.VIEWPORT_HEIGHT_CHANGED
|
|
377
|
+
}),
|
|
378
|
+
onWidthChange: (prevWidth, newWidth) => {
|
|
379
|
+
log('~ Scrollable container width changed from', prevWidth, 'to', newWidth, '~')
|
|
380
|
+
this.onResize()
|
|
282
381
|
}
|
|
283
382
|
})
|
|
284
383
|
|
|
285
|
-
if (preserveScrollPositionAtBottomOnMount) {
|
|
286
|
-
warn('`preserveScrollPositionAtBottomOnMount` option/property has been renamed to `preserveScrollPositionOfTheBottomOfTheListOnMount`')
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
this.preserveScrollPositionOfTheBottomOfTheListOnMount = preserveScrollPositionOfTheBottomOfTheListOnMount || preserveScrollPositionAtBottomOnMount
|
|
290
|
-
|
|
291
384
|
this.scroll = new Scroll({
|
|
292
385
|
bypass: this.bypass,
|
|
293
386
|
scrollableContainer: this.scrollableContainer,
|
|
294
|
-
|
|
387
|
+
itemsContainer: this.itemsContainer,
|
|
388
|
+
waitForScrollingToStop: _waitForScrollingToStop,
|
|
389
|
+
onScroll: ({ delayed } = {}) => {
|
|
390
|
+
this.onUpdateShownItemIndexes({
|
|
391
|
+
reason: delayed ? LAYOUT_REASON.STOPPED_SCROLLING : LAYOUT_REASON.SCROLL
|
|
392
|
+
})
|
|
393
|
+
},
|
|
295
394
|
initialScrollPosition,
|
|
296
395
|
onScrollPositionChange,
|
|
297
396
|
isImmediateLayoutScheduled: () => this.layoutTimer,
|
|
298
397
|
hasNonRenderedItemsAtTheTop: () => this.getState().firstShownItemIndex > 0,
|
|
299
398
|
hasNonRenderedItemsAtTheBottom: () => this.getState().lastShownItemIndex < this.getItemsCount() - 1,
|
|
300
|
-
|
|
301
|
-
|
|
399
|
+
getLatestLayoutVisibleArea: () => this.latestLayoutVisibleArea,
|
|
400
|
+
getListTopOffset: this.getListTopOffsetInsideScrollableContainer,
|
|
401
|
+
getPrerenderMargin: () => this.getPrerenderMargin()
|
|
302
402
|
})
|
|
303
403
|
|
|
304
|
-
this.
|
|
305
|
-
|
|
306
|
-
|
|
404
|
+
this.listHeightChangeWatcher = new ListHeightChangeWatcher({
|
|
405
|
+
itemsContainer: this.itemsContainer,
|
|
406
|
+
getListTopOffset: this.getListTopOffsetInsideScrollableContainer
|
|
307
407
|
})
|
|
308
408
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
409
|
+
if (engine.watchListTopOffset) {
|
|
410
|
+
this.listTopOffsetWatcher = engine.watchListTopOffset({
|
|
411
|
+
getListTopOffset: this.getListTopOffsetInsideScrollableContainer,
|
|
412
|
+
onListTopOffsetChange: ({ reason }) => this.onUpdateShownItemIndexes({
|
|
413
|
+
reason: LAYOUT_REASON.TOP_OFFSET_CHANGED
|
|
414
|
+
})
|
|
415
|
+
})
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
this.beforeResize = new BeforeResize({
|
|
419
|
+
getState: this.getState,
|
|
420
|
+
getVerticalSpacing: this.getVerticalSpacing,
|
|
421
|
+
getColumnsCount: this.getColumnsCount
|
|
312
422
|
})
|
|
313
423
|
|
|
424
|
+
// Possibly clean up "before resize" property in state.
|
|
425
|
+
// "Before resize" state property is cleaned up when all "before resize" item heights
|
|
426
|
+
// have been re-measured in an asynchronous `this.setState({ beforeResize: undefined })` call.
|
|
427
|
+
// If `VirtualScroller` state was snapshotted externally before that `this.setState()` call
|
|
428
|
+
// has been applied, then "before resize" property might have not been cleaned up properly.
|
|
429
|
+
this.beforeResize.onInitialState(state)
|
|
430
|
+
|
|
431
|
+
// `this.verticalSpacing` acts as a "true" source for vertical spacing value.
|
|
432
|
+
// Vertical spacing is also stored in `state` but `state` updates could be
|
|
433
|
+
// "asynchronous" (not applied immediately) and `this.onUpdateShownItemIndexes()`
|
|
434
|
+
// requires vertical spacing to be correct at any time, without any delays.
|
|
435
|
+
// So, vertical spacing is also duplicated in `state`, but the "true" source
|
|
436
|
+
// is still `this.verticalSpacing`.
|
|
437
|
+
//
|
|
438
|
+
// `this.verticalSpacing` must be initialized before calling `this.getInitialState()`.
|
|
439
|
+
//
|
|
440
|
+
this.verticalSpacing = state ? state.verticalSpacing : undefined
|
|
441
|
+
|
|
442
|
+
// Set initial `state`.
|
|
314
443
|
this.setState(state || this.getInitialState(customState))
|
|
315
444
|
}
|
|
316
445
|
|
|
@@ -327,20 +456,14 @@ export default class VirtualScroller {
|
|
|
327
456
|
items,
|
|
328
457
|
itemStates: new Array(items.length)
|
|
329
458
|
}
|
|
330
|
-
|
|
459
|
+
if (isDebug()) {
|
|
460
|
+
log('Initial state (autogenerated)', getStateSnapshot(state))
|
|
461
|
+
}
|
|
331
462
|
log('First shown item index', state.firstShownItemIndex)
|
|
332
463
|
log('Last shown item index', state.lastShownItemIndex)
|
|
333
464
|
return state
|
|
334
465
|
}
|
|
335
466
|
|
|
336
|
-
getInitialLayoutValues({ itemsCount, bypass }) {
|
|
337
|
-
return this.layout.getInitialLayoutValues({
|
|
338
|
-
bypass,
|
|
339
|
-
itemsCount,
|
|
340
|
-
visibleAreaHeightIncludingMargins: this.scrollableContainer && (2 * this.getMargin() + this.scrollableContainer.getHeight())
|
|
341
|
-
})
|
|
342
|
-
}
|
|
343
|
-
|
|
344
467
|
getInitialLayoutState(items) {
|
|
345
468
|
const itemsCount = items.length
|
|
346
469
|
const {
|
|
@@ -348,9 +471,9 @@ export default class VirtualScroller {
|
|
|
348
471
|
lastShownItemIndex,
|
|
349
472
|
beforeItemsHeight,
|
|
350
473
|
afterItemsHeight
|
|
351
|
-
} = this.getInitialLayoutValues({
|
|
474
|
+
} = this.layout.getInitialLayoutValues({
|
|
352
475
|
itemsCount,
|
|
353
|
-
|
|
476
|
+
columnsCount: this.getColumnsCount()
|
|
354
477
|
})
|
|
355
478
|
const itemHeights = new Array(itemsCount)
|
|
356
479
|
// Optionally preload items to be rendered.
|
|
@@ -360,15 +483,10 @@ export default class VirtualScroller {
|
|
|
360
483
|
firstShownItemIndex,
|
|
361
484
|
lastShownItemIndex
|
|
362
485
|
)
|
|
363
|
-
// This "initial" state object must include all possible state properties
|
|
364
|
-
// because `this.setState()` gets called with this state on window resize,
|
|
365
|
-
// when `VirtualScroller` gets reset.
|
|
366
|
-
// Item states aren't included here because the state of all items should be
|
|
367
|
-
// preserved on window resize.
|
|
368
486
|
return {
|
|
369
487
|
itemHeights,
|
|
370
|
-
columnsCount: this.
|
|
371
|
-
verticalSpacing:
|
|
488
|
+
columnsCount: this.getActualColumnsCountForState(),
|
|
489
|
+
verticalSpacing: this.verticalSpacing,
|
|
372
490
|
firstShownItemIndex,
|
|
373
491
|
lastShownItemIndex,
|
|
374
492
|
beforeItemsHeight,
|
|
@@ -376,8 +494,29 @@ export default class VirtualScroller {
|
|
|
376
494
|
}
|
|
377
495
|
}
|
|
378
496
|
|
|
379
|
-
|
|
380
|
-
|
|
497
|
+
// Bind to `this` in order to prevent bugs when this function is passed by reference
|
|
498
|
+
// and then called with its `this` being unintentionally `window` resulting in
|
|
499
|
+
// the `if` condition being "falsy".
|
|
500
|
+
getActualColumnsCountForState = () => {
|
|
501
|
+
return this._getColumnsCount ? this._getColumnsCount(this.scrollableContainer) : undefined
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
getActualColumnsCount() {
|
|
505
|
+
return this.getActualColumnsCountForState() || 1
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Bind to `this` in order to prevent bugs when this function is passed by reference
|
|
509
|
+
// and then called with its `this` being unintentionally `window` resulting in
|
|
510
|
+
// the `if` condition being "falsy".
|
|
511
|
+
getVerticalSpacing = () => {
|
|
512
|
+
return this.verticalSpacing || 0
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
getVerticalSpacingBeforeResize() {
|
|
516
|
+
// `beforeResize.verticalSpacing` can be `undefined`.
|
|
517
|
+
// For example, if `this.setState({ verticalSpacing })` call hasn't been applied
|
|
518
|
+
// before the resize happened (in case of an "asynchronous" state update).
|
|
519
|
+
return this.getState().beforeResize && this.getState().beforeResize.verticalSpacing || 0
|
|
381
520
|
}
|
|
382
521
|
|
|
383
522
|
getColumnsCount() {
|
|
@@ -388,14 +527,18 @@ export default class VirtualScroller {
|
|
|
388
527
|
return this.getState().items.length
|
|
389
528
|
}
|
|
390
529
|
|
|
391
|
-
|
|
392
|
-
//
|
|
393
|
-
//
|
|
394
|
-
//
|
|
530
|
+
getPrerenderMargin() {
|
|
531
|
+
// The list component renders not only the items that're currently visible
|
|
532
|
+
// but also the items that lie within some extra vertical margin (called
|
|
533
|
+
// "prerender margin") on top and bottom for future scrolling: this way,
|
|
534
|
+
// there'll be significantly less layout recalculations as the user scrolls,
|
|
535
|
+
// because now it doesn't have to recalculate layout on each scroll event.
|
|
536
|
+
// By default, the "prerender margin" is equal to the screen height:
|
|
395
537
|
// this seems to be the optimal value for "Page Up" / "Page Down" navigation
|
|
396
538
|
// and optimized mouse wheel scrolling (a user is unlikely to continuously
|
|
397
|
-
// scroll past the height
|
|
398
|
-
// the
|
|
539
|
+
// scroll past the screen height, because they'd stop to read through
|
|
540
|
+
// the newly visible items first, and when they do stop scrolling, that's
|
|
541
|
+
// when layout gets recalculated).
|
|
399
542
|
const renderAheadMarginRatio = 1 // in scrollable container heights.
|
|
400
543
|
return this.scrollableContainer.getHeight() * renderAheadMarginRatio
|
|
401
544
|
}
|
|
@@ -442,52 +585,69 @@ export default class VirtualScroller {
|
|
|
442
585
|
if (this.isRendered === false) {
|
|
443
586
|
throw new Error('[virtual-scroller] Can\'t restart a `VirtualScroller` after it has been stopped')
|
|
444
587
|
}
|
|
588
|
+
|
|
445
589
|
log('~ Rendered (initial) ~')
|
|
446
590
|
// `this.isRendered = true` should be the first statement in this function,
|
|
447
591
|
// otherwise `DOMVirtualScroller` would enter an infinite re-render loop.
|
|
448
592
|
this.isRendered = true
|
|
449
|
-
|
|
593
|
+
|
|
594
|
+
const stateUpdate = this.measureItemHeightsAndSpacingAndUpdateTablePadding()
|
|
595
|
+
|
|
450
596
|
this.resize.listen()
|
|
451
597
|
this.scroll.listen()
|
|
598
|
+
|
|
452
599
|
// Work around `<tbody/>` not being able to have `padding`.
|
|
453
600
|
// https://gitlab.com/catamphetamine/virtual-scroller/-/issues/1
|
|
454
601
|
if (this.tbody) {
|
|
455
|
-
addTbodyStyles(this.
|
|
456
|
-
}
|
|
457
|
-
if (this.preserveScrollPositionOfTheBottomOfTheListOnMount) {
|
|
458
|
-
// In this case, all items are shown, so there's no need to call
|
|
459
|
-
// `this.onUpdateShownItemIndexes()` after the initial render.
|
|
460
|
-
} else {
|
|
461
|
-
this.onUpdateShownItemIndexes({ reason: LAYOUT_REASON.MOUNT })
|
|
602
|
+
addTbodyStyles(this.getItemsContainerElement())
|
|
462
603
|
}
|
|
604
|
+
|
|
605
|
+
// Re-calculate layout and re-render the list.
|
|
606
|
+
// Do that even if when an initial `state` parameter, containing layout values,
|
|
607
|
+
// has been passed. The reason is that the `state` parameter can't be "trusted"
|
|
608
|
+
// in a way that it could have been snapshotted for another window width and
|
|
609
|
+
// the user might have resized their window since then.
|
|
610
|
+
this.onUpdateShownItemIndexes({ reason: LAYOUT_REASON.MOUNTED, stateUpdate })
|
|
463
611
|
}
|
|
464
612
|
|
|
465
|
-
|
|
466
|
-
// Update item vertical spacing.
|
|
467
|
-
this.measureVerticalSpacing()
|
|
613
|
+
measureItemHeightsAndSpacingAndUpdateTablePadding() {
|
|
468
614
|
// Measure "newly shown" item heights.
|
|
469
615
|
// Also re-validate already measured items' heights.
|
|
470
616
|
this.itemHeights.measureItemHeights(
|
|
471
617
|
this.getState().firstShownItemIndex,
|
|
472
618
|
this.getState().lastShownItemIndex
|
|
473
619
|
)
|
|
620
|
+
|
|
621
|
+
// Update item vertical spacing.
|
|
622
|
+
const verticalSpacing = this.measureVerticalSpacing()
|
|
623
|
+
|
|
474
624
|
// Update `<tbody/>` `padding`.
|
|
475
625
|
// (`<tbody/>` is different in a way that it can't have `margin`, only `padding`).
|
|
476
626
|
// https://gitlab.com/catamphetamine/virtual-scroller/-/issues/1
|
|
477
627
|
if (this.tbody) {
|
|
478
628
|
setTbodyPadding(
|
|
479
|
-
this.
|
|
629
|
+
this.getItemsContainerElement(),
|
|
480
630
|
this.getState().beforeItemsHeight,
|
|
481
631
|
this.getState().afterItemsHeight
|
|
482
632
|
)
|
|
483
633
|
}
|
|
634
|
+
|
|
635
|
+
// Return a state update.
|
|
636
|
+
if (verticalSpacing !== undefined) {
|
|
637
|
+
return { verticalSpacing }
|
|
638
|
+
}
|
|
484
639
|
}
|
|
485
640
|
|
|
486
|
-
|
|
641
|
+
getVisibleArea() {
|
|
487
642
|
const visibleArea = this.scroll.getVisibleAreaBounds()
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
643
|
+
this.latestLayoutVisibleArea = visibleArea
|
|
644
|
+
|
|
645
|
+
// Subtract the top offset of the list inside the scrollable container.
|
|
646
|
+
const listTopOffsetInsideScrollableContainer = this.getListTopOffsetInsideScrollableContainer()
|
|
647
|
+
return {
|
|
648
|
+
top: visibleArea.top - listTopOffsetInsideScrollableContainer,
|
|
649
|
+
bottom: visibleArea.bottom - listTopOffsetInsideScrollableContainer
|
|
650
|
+
}
|
|
491
651
|
}
|
|
492
652
|
|
|
493
653
|
/**
|
|
@@ -495,11 +655,26 @@ export default class VirtualScroller {
|
|
|
495
655
|
* @return {number}
|
|
496
656
|
*/
|
|
497
657
|
getListTopOffsetInsideScrollableContainer = () => {
|
|
498
|
-
const listTopOffset = this.scrollableContainer.
|
|
499
|
-
this.
|
|
658
|
+
const listTopOffset = this.scrollableContainer.getItemsContainerTopOffset()
|
|
659
|
+
if (this.listTopOffsetWatcher) {
|
|
660
|
+
this.listTopOffsetWatcher.onListTopOffset(listTopOffset)
|
|
661
|
+
}
|
|
500
662
|
return listTopOffset
|
|
501
663
|
}
|
|
502
664
|
|
|
665
|
+
/**
|
|
666
|
+
* Returns the items's top offset relative to the scrollable container's top edge.
|
|
667
|
+
* @param {number} i — Item index
|
|
668
|
+
* @return {[number]} Returns the item's scroll Y position. Returns `undefined` if any of the previous items haven't been rendered yet.
|
|
669
|
+
*/
|
|
670
|
+
getItemScrollPosition(i) {
|
|
671
|
+
const itemTopOffsetInList = this.layout.getItemTopOffset(i)
|
|
672
|
+
if (itemTopOffsetInList === undefined) {
|
|
673
|
+
return
|
|
674
|
+
}
|
|
675
|
+
return this.getListTopOffsetInsideScrollableContainer() + itemTopOffsetInList
|
|
676
|
+
}
|
|
677
|
+
|
|
503
678
|
onUnmount() {
|
|
504
679
|
warn('`.onUnmount()` instance method name is deprecated, use `.stop()` instance method name instead.')
|
|
505
680
|
this.stop()
|
|
@@ -510,17 +685,46 @@ export default class VirtualScroller {
|
|
|
510
685
|
this.stop()
|
|
511
686
|
}
|
|
512
687
|
|
|
513
|
-
stop() {
|
|
688
|
+
stop = () => {
|
|
514
689
|
this.isRendered = false
|
|
515
690
|
this.resize.stop()
|
|
516
691
|
this.scroll.stop()
|
|
517
|
-
this.
|
|
692
|
+
if (this.listTopOffsetWatcher) {
|
|
693
|
+
this.listTopOffsetWatcher.stop()
|
|
694
|
+
}
|
|
695
|
+
this.cancelLayoutTimer({})
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
cancelLayoutTimer({ stateUpdate }) {
|
|
518
699
|
if (this.layoutTimer) {
|
|
519
700
|
clearTimeout(this.layoutTimer)
|
|
520
701
|
this.layoutTimer = undefined
|
|
702
|
+
// Merge state updates.
|
|
703
|
+
if (stateUpdate || this.layoutTimerStateUpdate) {
|
|
704
|
+
stateUpdate = {
|
|
705
|
+
...this.layoutTimerStateUpdate,
|
|
706
|
+
...stateUpdate
|
|
707
|
+
}
|
|
708
|
+
this.layoutTimerStateUpdate = undefined
|
|
709
|
+
return stateUpdate
|
|
710
|
+
}
|
|
711
|
+
} else {
|
|
712
|
+
return stateUpdate
|
|
521
713
|
}
|
|
522
714
|
}
|
|
523
715
|
|
|
716
|
+
scheduleLayoutTimer({ reason, stateUpdate }) {
|
|
717
|
+
this.layoutTimerStateUpdate = stateUpdate
|
|
718
|
+
this.layoutTimer = setTimeout(() => {
|
|
719
|
+
this.layoutTimerStateUpdate = undefined
|
|
720
|
+
this.layoutTimer = undefined
|
|
721
|
+
this.onUpdateShownItemIndexes({
|
|
722
|
+
reason,
|
|
723
|
+
stateUpdate
|
|
724
|
+
})
|
|
725
|
+
}, 0)
|
|
726
|
+
}
|
|
727
|
+
|
|
524
728
|
/**
|
|
525
729
|
* Should be called right before `state` is updated.
|
|
526
730
|
* @param {object} prevState
|
|
@@ -542,73 +746,277 @@ export default class VirtualScroller {
|
|
|
542
746
|
*/
|
|
543
747
|
didUpdateState = (prevState) => {
|
|
544
748
|
const newState = this.getState()
|
|
749
|
+
|
|
545
750
|
if (this.onStateChange) {
|
|
546
751
|
if (!shallowEqual(newState, prevState)) {
|
|
547
752
|
this.onStateChange(newState, prevState)
|
|
548
753
|
}
|
|
549
754
|
}
|
|
755
|
+
|
|
550
756
|
// Ignore setting initial state.
|
|
551
757
|
if (!prevState) {
|
|
552
758
|
return
|
|
553
759
|
}
|
|
760
|
+
|
|
554
761
|
if (!this.isRendered) {
|
|
555
762
|
return
|
|
556
763
|
}
|
|
764
|
+
|
|
557
765
|
log('~ Rendered ~')
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
766
|
+
if (isDebug()) {
|
|
767
|
+
log('State', getStateSnapshot(newState))
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
let layoutUpdateReason
|
|
771
|
+
|
|
772
|
+
if (this.firstNonMeasuredItemIndex !== undefined) {
|
|
773
|
+
layoutUpdateReason = LAYOUT_REASON.ACTUAL_ITEM_HEIGHTS_HAVE_BEEN_MEASURED
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
if (this.resetLayoutAfterResize) {
|
|
777
|
+
layoutUpdateReason = LAYOUT_REASON.VIEWPORT_WIDTH_CHANGED
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// If `this.resetLayoutAfterResize` flag was reset after calling
|
|
781
|
+
// `this.measureItemHeightsAndSpacingAndUpdateTablePadding()`
|
|
782
|
+
// then there would be a bug because
|
|
783
|
+
// `this.measureItemHeightsAndSpacingAndUpdateTablePadding()`
|
|
784
|
+
// calls `this.setState({ verticalSpacing })` which calls
|
|
785
|
+
// `this.didUpdateState()` immediately, so `this.resetLayoutAfterResize`
|
|
786
|
+
// flag wouldn't be reset by that time and would trigger things
|
|
787
|
+
// like `this.itemHeights.reset()` a second time.
|
|
788
|
+
//
|
|
789
|
+
// So, instead read the value of `this.resetLayoutAfterResize` flag
|
|
790
|
+
// and reset it right away to prevent any such potential bugs.
|
|
791
|
+
//
|
|
792
|
+
const resetLayoutAfterResize = this.resetLayoutAfterResize
|
|
793
|
+
|
|
794
|
+
// Reset `this.firstNonMeasuredItemIndex`.
|
|
795
|
+
this.firstNonMeasuredItemIndex = undefined
|
|
796
|
+
|
|
797
|
+
// Reset `this.resetLayoutAfterResize` flag.
|
|
798
|
+
this.resetLayoutAfterResize = undefined
|
|
799
|
+
|
|
800
|
+
// Reset `this.newItemsWillBeRendered` flag.
|
|
801
|
+
this.newItemsWillBeRendered = undefined
|
|
802
|
+
|
|
803
|
+
// Reset `this.itemHeightsThatChangedWhileNewItemsWereBeingRendered`.
|
|
804
|
+
this.itemHeightsThatChangedWhileNewItemsWereBeingRendered = undefined
|
|
805
|
+
|
|
806
|
+
// Reset `this.itemStatesThatChangedWhileNewItemsWereBeingRendered`.
|
|
807
|
+
this.itemStatesThatChangedWhileNewItemsWereBeingRendered = undefined
|
|
808
|
+
|
|
809
|
+
if (resetLayoutAfterResize) {
|
|
810
|
+
// Reset measured item heights on viewport width change.
|
|
811
|
+
this.itemHeights.reset()
|
|
812
|
+
|
|
813
|
+
// Reset `verticalSpacing` (will be re-measured).
|
|
814
|
+
this.verticalSpacing = undefined
|
|
815
|
+
}
|
|
816
|
+
|
|
562
817
|
const { items: previousItems } = prevState
|
|
563
818
|
const { items: newItems } = newState
|
|
819
|
+
// Even if `this.newItemsWillBeRendered` flag is `true`,
|
|
820
|
+
// `newItems` could still be equal to `previousItems`.
|
|
821
|
+
// For example, when `setState()` calls don't update `state` immediately
|
|
822
|
+
// and a developer first calls `setItems(newItems)` and then calls `setItems(oldItems)`:
|
|
823
|
+
// in that case, `this.newItemsWillBeRendered` flag will be `true` but the actual `items`
|
|
824
|
+
// in state wouldn't have changed due to the first `setState()` call being overwritten
|
|
825
|
+
// by the second `setState()` call (that's called "batching state updates" in React).
|
|
564
826
|
if (newItems !== previousItems) {
|
|
565
|
-
let layoutNeedsReCalculating = true
|
|
566
827
|
const itemsDiff = this.getItemsDiff(previousItems, newItems)
|
|
567
|
-
// If it's an "incremental" update.
|
|
568
828
|
if (itemsDiff) {
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
// the call to `.measureItemHeights()`
|
|
576
|
-
// which is called in `.onRendered()`.
|
|
577
|
-
this.itemHeights.onPrepend(prependedItemsCount)
|
|
578
|
-
if (this.restoreScroll.shouldRestoreScrollAfterRender()) {
|
|
579
|
-
layoutNeedsReCalculating = false
|
|
580
|
-
log('~ Restore Scroll Position ~')
|
|
581
|
-
const scrollByY = this.restoreScroll.getScrollDifference()
|
|
582
|
-
if (scrollByY) {
|
|
583
|
-
log('Scroll down by', scrollByY)
|
|
584
|
-
this.scroll.scrollByY(scrollByY)
|
|
585
|
-
} else {
|
|
586
|
-
log('Scroll position hasn\'t changed')
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
}
|
|
829
|
+
// The call to `.onPrepend()` must precede the call to `.measureItemHeights()`
|
|
830
|
+
// which is called in `.onRendered()`.
|
|
831
|
+
// `this.itemHeights.onPrepend()` updates `firstMeasuredItemIndex`
|
|
832
|
+
// and `lastMeasuredItemIndex` of `this.itemHeights`.
|
|
833
|
+
const { prependedItemsCount } = itemsDiff
|
|
834
|
+
this.itemHeights.onPrepend(prependedItemsCount)
|
|
590
835
|
} else {
|
|
591
836
|
this.itemHeights.reset()
|
|
592
|
-
|
|
837
|
+
// `newState.itemHeights` is an array of `undefined`s.
|
|
838
|
+
this.itemHeights.initialize(newState.itemHeights)
|
|
593
839
|
}
|
|
594
|
-
|
|
595
|
-
|
|
840
|
+
|
|
841
|
+
if (!resetLayoutAfterResize) {
|
|
842
|
+
// The call to `this.onNewItemsRendered()` must precede the call to
|
|
843
|
+
// `.measureItemHeights()` which is called in `.onRendered()` because
|
|
844
|
+
// `this.onNewItemsRendered()` updates `firstMeasuredItemIndex` and
|
|
845
|
+
// `lastMeasuredItemIndex` of `this.itemHeights` in case of a prepend.
|
|
846
|
+
//
|
|
847
|
+
// If after prepending items the scroll position
|
|
848
|
+
// should be "restored" so that there's no "jump" of content
|
|
849
|
+
// then it means that all previous items have just been rendered
|
|
850
|
+
// in a single pass, and there's no need to update layout again.
|
|
851
|
+
//
|
|
852
|
+
if (this.onNewItemsRendered(itemsDiff, newState) !== 'SEAMLESS_PREPEND') {
|
|
853
|
+
layoutUpdateReason = LAYOUT_REASON.ITEMS_CHANGED
|
|
854
|
+
}
|
|
596
855
|
}
|
|
597
856
|
}
|
|
598
|
-
|
|
599
|
-
|
|
857
|
+
|
|
858
|
+
let stateUpdate
|
|
859
|
+
|
|
860
|
+
// Re-measure item heights.
|
|
861
|
+
// Also, measure vertical spacing (if not measured) and fix `<table/>` padding.
|
|
862
|
+
//
|
|
863
|
+
// This block should go after `if (newItems !== previousItems) {}`
|
|
864
|
+
// because `this.itemHeights` can get `.reset()` there, which would
|
|
865
|
+
// discard all the measurements done here, and having currently shown
|
|
866
|
+
// item height measurements is required.
|
|
867
|
+
//
|
|
868
|
+
if (
|
|
869
|
+
newState.firstShownItemIndex !== prevState.firstShownItemIndex ||
|
|
600
870
|
newState.lastShownItemIndex !== prevState.lastShownItemIndex ||
|
|
601
|
-
newState.items !== prevState.items
|
|
602
|
-
|
|
871
|
+
newState.items !== prevState.items ||
|
|
872
|
+
resetLayoutAfterResize
|
|
873
|
+
) {
|
|
874
|
+
const verticalSpacingStateUpdate = this.measureItemHeightsAndSpacingAndUpdateTablePadding()
|
|
875
|
+
if (verticalSpacingStateUpdate) {
|
|
876
|
+
stateUpdate = {
|
|
877
|
+
...stateUpdate,
|
|
878
|
+
...verticalSpacingStateUpdate
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// Clean up "before resize" item heights and adjust the scroll position accordingly.
|
|
884
|
+
// Calling `this.beforeResize.cleanUpBeforeResizeItemHeights()` might trigger
|
|
885
|
+
// a `this.setState()` call but that wouldn't matter because `beforeResize`
|
|
886
|
+
// properties have already been modified directly in `state` (a hacky technique)
|
|
887
|
+
const cleanedUpBeforeResize = this.beforeResize.cleanUpBeforeResizeItemHeights(prevState)
|
|
888
|
+
if (cleanedUpBeforeResize !== undefined) {
|
|
889
|
+
const { scrollBy, beforeResize } = cleanedUpBeforeResize
|
|
890
|
+
log('Correct scroll position by', scrollBy)
|
|
891
|
+
this.scroll.scrollByY(scrollBy)
|
|
892
|
+
stateUpdate = {
|
|
893
|
+
...stateUpdate,
|
|
894
|
+
beforeResize
|
|
895
|
+
}
|
|
603
896
|
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
897
|
+
|
|
898
|
+
if (layoutUpdateReason) {
|
|
899
|
+
this.updateStateRightAfterRender({
|
|
900
|
+
stateUpdate,
|
|
901
|
+
reason: layoutUpdateReason
|
|
607
902
|
})
|
|
903
|
+
} else if (stateUpdate) {
|
|
904
|
+
this.setState(stateUpdate)
|
|
608
905
|
}
|
|
609
906
|
}
|
|
610
907
|
|
|
611
|
-
|
|
908
|
+
// After a new set of items has been rendered:
|
|
909
|
+
//
|
|
910
|
+
// * Restores scroll position when using `preserveScrollPositionOnPrependItems`
|
|
911
|
+
// and items have been prepended.
|
|
912
|
+
//
|
|
913
|
+
// * Applies any "pending" `itemHeights` updates — those ones that happened
|
|
914
|
+
// while an asynchronous `setState()` call in `setItems()` was pending.
|
|
915
|
+
//
|
|
916
|
+
// * Either creates or resets the snapshot of the current layout.
|
|
917
|
+
//
|
|
918
|
+
// The current layout snapshot could be stored as a "previously calculated layout" variable
|
|
919
|
+
// so that it could theoretically be used when calculating new layout incrementally
|
|
920
|
+
// rather than from scratch, which would be an optimization.
|
|
921
|
+
//
|
|
922
|
+
// The "previously calculated layout" feature is not currently used.
|
|
923
|
+
//
|
|
924
|
+
onNewItemsRendered(itemsDiff, newLayout) {
|
|
925
|
+
// If it's an "incremental" update.
|
|
926
|
+
if (itemsDiff) {
|
|
927
|
+
const {
|
|
928
|
+
prependedItemsCount,
|
|
929
|
+
appendedItemsCount
|
|
930
|
+
} = itemsDiff
|
|
931
|
+
|
|
932
|
+
const {
|
|
933
|
+
itemHeights,
|
|
934
|
+
itemStates
|
|
935
|
+
} = this.getState()
|
|
936
|
+
|
|
937
|
+
// See if any items' heights changed while new items were being rendered.
|
|
938
|
+
if (this.itemHeightsThatChangedWhileNewItemsWereBeingRendered) {
|
|
939
|
+
for (const i of Object.keys(this.itemHeightsThatChangedWhileNewItemsWereBeingRendered)) {
|
|
940
|
+
itemHeights[prependedItemsCount + parseInt(i)] = this.itemHeightsThatChangedWhileNewItemsWereBeingRendered[i]
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// See if any items' states changed while new items were being rendered.
|
|
945
|
+
if (this.itemStatesThatChangedWhileNewItemsWereBeingRendered) {
|
|
946
|
+
for (const i of Object.keys(this.itemStatesThatChangedWhileNewItemsWereBeingRendered)) {
|
|
947
|
+
itemStates[prependedItemsCount + parseInt(i)] = this.itemStatesThatChangedWhileNewItemsWereBeingRendered[i]
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
if (prependedItemsCount === 0) {
|
|
952
|
+
// Adjust `this.previouslyCalculatedLayout`.
|
|
953
|
+
if (this.previouslyCalculatedLayout) {
|
|
954
|
+
if (
|
|
955
|
+
this.previouslyCalculatedLayout.firstShownItemIndex === newLayout.firstShownItemIndex &&
|
|
956
|
+
this.previouslyCalculatedLayout.lastShownItemIndex === newLayout.lastShownItemIndex
|
|
957
|
+
) {
|
|
958
|
+
// `this.previouslyCalculatedLayout` stays the same.
|
|
959
|
+
// `firstShownItemIndex` / `lastShownItemIndex` didn't get changed in `setItems()`,
|
|
960
|
+
// so `beforeItemsHeight` and `shownItemsHeight` also stayed the same.
|
|
961
|
+
} else {
|
|
962
|
+
warn('Unexpected (non-matching) "firstShownItemIndex" or "lastShownItemIndex" encountered in "didUpdateState()" after appending items')
|
|
963
|
+
warn('Previously calculated layout', this.previouslyCalculatedLayout)
|
|
964
|
+
warn('New layout', newLayout)
|
|
965
|
+
this.previouslyCalculatedLayout = undefined
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
return 'SEAMLESS_APPEND'
|
|
969
|
+
} else {
|
|
970
|
+
if (this.listHeightChangeWatcher.hasSnapshot()) {
|
|
971
|
+
if (newLayout.firstShownItemIndex === 0) {
|
|
972
|
+
// Restore (adjust) scroll position.
|
|
973
|
+
log('~ Restore Scroll Position ~')
|
|
974
|
+
const listBottomOffsetChange = this.listHeightChangeWatcher.getListBottomOffsetChange({
|
|
975
|
+
beforeItemsHeight: newLayout.beforeItemsHeight
|
|
976
|
+
})
|
|
977
|
+
this.listHeightChangeWatcher.reset()
|
|
978
|
+
if (listBottomOffsetChange) {
|
|
979
|
+
log('Scroll down by', listBottomOffsetChange)
|
|
980
|
+
this.scroll.scrollByY(listBottomOffsetChange)
|
|
981
|
+
} else {
|
|
982
|
+
log('Scroll position hasn\'t changed')
|
|
983
|
+
}
|
|
984
|
+
// Create new `this.previouslyCalculatedLayout`.
|
|
985
|
+
if (this.previouslyCalculatedLayout) {
|
|
986
|
+
if (
|
|
987
|
+
this.previouslyCalculatedLayout.firstShownItemIndex === 0 &&
|
|
988
|
+
this.previouslyCalculatedLayout.lastShownItemIndex === newLayout.lastShownItemIndex - prependedItemsCount
|
|
989
|
+
) {
|
|
990
|
+
this.previouslyCalculatedLayout = {
|
|
991
|
+
beforeItemsHeight: 0,
|
|
992
|
+
shownItemsHeight: this.previouslyCalculatedLayout.shownItemsHeight + listBottomOffsetChange,
|
|
993
|
+
firstShownItemIndex: 0,
|
|
994
|
+
lastShownItemIndex: newLayout.lastShownItemIndex
|
|
995
|
+
}
|
|
996
|
+
} else {
|
|
997
|
+
warn('Unexpected (non-matching) "firstShownItemIndex" or "lastShownItemIndex" encountered in "didUpdateState()" after prepending items')
|
|
998
|
+
warn('Previously calculated layout', this.previouslyCalculatedLayout)
|
|
999
|
+
warn('New layout', newLayout)
|
|
1000
|
+
this.previouslyCalculatedLayout = undefined
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
return 'SEAMLESS_PREPEND'
|
|
1004
|
+
} else {
|
|
1005
|
+
warn(`Unexpected "firstShownItemIndex" ${newLayout.firstShownItemIndex} encountered in "didUpdateState()" after prepending items. Expected 0.`)
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// Reset `this.previouslyCalculatedLayout` in any case other than
|
|
1012
|
+
// SEAMLESS_PREPEND or SEAMLESS_APPEND.
|
|
1013
|
+
this.previouslyCalculatedLayout = undefined
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
updateStateRightAfterRender({
|
|
1017
|
+
reason,
|
|
1018
|
+
stateUpdate
|
|
1019
|
+
}) {
|
|
612
1020
|
// In React, `setTimeout()` is used to prevent a React error:
|
|
613
1021
|
// "Maximum update depth exceeded.
|
|
614
1022
|
// This can happen when a component repeatedly calls
|
|
@@ -616,31 +1024,36 @@ export default class VirtualScroller {
|
|
|
616
1024
|
// React limits the number of nested updates to prevent infinite loops."
|
|
617
1025
|
if (this._useTimeoutInRenderLoop) {
|
|
618
1026
|
// Cancel a previously scheduled re-layout.
|
|
619
|
-
|
|
620
|
-
clearTimeout(this.layoutTimer)
|
|
621
|
-
}
|
|
1027
|
+
stateUpdate = this.cancelLayoutTimer({ stateUpdate })
|
|
622
1028
|
// Schedule a new re-layout.
|
|
623
|
-
this.
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
}
|
|
1029
|
+
this.scheduleLayoutTimer({
|
|
1030
|
+
reason,
|
|
1031
|
+
stateUpdate
|
|
1032
|
+
})
|
|
627
1033
|
} else {
|
|
628
|
-
this.onUpdateShownItemIndexes({
|
|
1034
|
+
this.onUpdateShownItemIndexes({
|
|
1035
|
+
reason,
|
|
1036
|
+
stateUpdate
|
|
1037
|
+
})
|
|
629
1038
|
}
|
|
630
1039
|
}
|
|
631
1040
|
|
|
632
1041
|
measureVerticalSpacing() {
|
|
633
|
-
if (this.
|
|
1042
|
+
if (this.verticalSpacing === undefined) {
|
|
1043
|
+
const { firstShownItemIndex, lastShownItemIndex } = this.getState()
|
|
634
1044
|
log('~ Measure item vertical spacing ~')
|
|
635
1045
|
const verticalSpacing = getVerticalSpacing({
|
|
636
|
-
|
|
637
|
-
|
|
1046
|
+
itemsContainer: this.itemsContainer,
|
|
1047
|
+
renderedItemsCount: lastShownItemIndex - firstShownItemIndex + 1
|
|
638
1048
|
})
|
|
639
1049
|
if (verticalSpacing === undefined) {
|
|
640
1050
|
log('Not enough items rendered to measure vertical spacing')
|
|
641
1051
|
} else {
|
|
642
1052
|
log('Item vertical spacing', verticalSpacing)
|
|
643
|
-
this.
|
|
1053
|
+
this.verticalSpacing = verticalSpacing
|
|
1054
|
+
if (verticalSpacing !== 0) {
|
|
1055
|
+
return verticalSpacing
|
|
1056
|
+
}
|
|
644
1057
|
}
|
|
645
1058
|
}
|
|
646
1059
|
}
|
|
@@ -650,27 +1063,42 @@ export default class VirtualScroller {
|
|
|
650
1063
|
return this.itemHeights.remeasureItemHeight(i, firstShownItemIndex)
|
|
651
1064
|
}
|
|
652
1065
|
|
|
653
|
-
onItemStateChange(i,
|
|
1066
|
+
onItemStateChange(i, newItemState) {
|
|
654
1067
|
if (isDebug()) {
|
|
655
1068
|
log('~ Item state changed ~')
|
|
656
1069
|
log('Item', i)
|
|
1070
|
+
// Uses `JSON.stringify()` here instead of just outputting the JSON objects as is
|
|
1071
|
+
// because outputting JSON objects as is would show different results later when
|
|
1072
|
+
// the developer inspects those in the web browser console if those state objects
|
|
1073
|
+
// get modified in between they've been output to the console and the developer
|
|
1074
|
+
// decided to inspect them.
|
|
657
1075
|
log('Previous state' + '\n' + JSON.stringify(this.getState().itemStates[i], null, 2))
|
|
658
|
-
log('New state' + '\n' + JSON.stringify(
|
|
1076
|
+
log('New state' + '\n' + JSON.stringify(newItemState, null, 2))
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
this.getState().itemStates[i] = newItemState
|
|
1080
|
+
|
|
1081
|
+
// Schedule the item state update for after the new items have been rendered.
|
|
1082
|
+
if (this.newItemsWillBeRendered) {
|
|
1083
|
+
if (!this.itemStatesThatChangedWhileNewItemsWereBeingRendered) {
|
|
1084
|
+
this.itemStatesThatChangedWhileNewItemsWereBeingRendered = {}
|
|
1085
|
+
}
|
|
1086
|
+
this.itemStatesThatChangedWhileNewItemsWereBeingRendered[String(i)] = newItemState
|
|
659
1087
|
}
|
|
660
|
-
this.getState().itemStates[i] = itemState
|
|
661
1088
|
}
|
|
662
1089
|
|
|
663
1090
|
onItemHeightChange(i) {
|
|
664
1091
|
log('~ Re-measure item height ~')
|
|
665
1092
|
log('Item', i)
|
|
666
|
-
|
|
667
|
-
const
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
1093
|
+
|
|
1094
|
+
const {
|
|
1095
|
+
itemHeights,
|
|
1096
|
+
firstShownItemIndex,
|
|
1097
|
+
lastShownItemIndex
|
|
1098
|
+
} = this.getState()
|
|
1099
|
+
|
|
672
1100
|
// Check if the item is still rendered.
|
|
673
|
-
if (
|
|
1101
|
+
if (!(i >= firstShownItemIndex && i <= lastShownItemIndex)) {
|
|
674
1102
|
// There could be valid cases when an item is no longer rendered
|
|
675
1103
|
// by the time `.onItemHeightChange(i)` gets called.
|
|
676
1104
|
// For example, suppose there's a list of several items on a page,
|
|
@@ -698,12 +1126,60 @@ export default class VirtualScroller {
|
|
|
698
1126
|
// `.onItemHeightChange(i)` gets called.
|
|
699
1127
|
return warn('The item is no longer rendered. This is not necessarily a bug, and could happen, for example, when there\'re several `onItemHeightChange(i)` calls issued at the same time.')
|
|
700
1128
|
}
|
|
1129
|
+
|
|
1130
|
+
const previousHeight = itemHeights[i]
|
|
1131
|
+
if (previousHeight === undefined) {
|
|
1132
|
+
return reportError(`"onItemHeightChange()" has been called for item ${i}, but that item hasn't been rendered before.`)
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
const newHeight = this.remeasureItemHeight(i)
|
|
1136
|
+
|
|
701
1137
|
log('Previous height', previousHeight)
|
|
702
1138
|
log('New height', newHeight)
|
|
1139
|
+
|
|
703
1140
|
if (previousHeight !== newHeight) {
|
|
704
1141
|
log('~ Item height has changed ~')
|
|
705
|
-
|
|
1142
|
+
|
|
1143
|
+
// Update or reset previously calculated layout.
|
|
1144
|
+
this.updatePreviouslyCalculatedLayoutOnItemHeightChange(i, previousHeight, newHeight)
|
|
1145
|
+
|
|
1146
|
+
// Recalculate layout.
|
|
706
1147
|
this.onUpdateShownItemIndexes({ reason: LAYOUT_REASON.ITEM_HEIGHT_CHANGED })
|
|
1148
|
+
|
|
1149
|
+
// Schedule the item height update for after the new items have been rendered.
|
|
1150
|
+
if (this.newItemsWillBeRendered) {
|
|
1151
|
+
if (!this.itemHeightsThatChangedWhileNewItemsWereBeingRendered) {
|
|
1152
|
+
this.itemHeightsThatChangedWhileNewItemsWereBeingRendered = {}
|
|
1153
|
+
}
|
|
1154
|
+
this.itemHeightsThatChangedWhileNewItemsWereBeingRendered[String(i)] = newHeight
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// Updates the snapshot of the current layout when an item's height changes.
|
|
1160
|
+
//
|
|
1161
|
+
// The "previously calculated layout" feature is not currently used.
|
|
1162
|
+
//
|
|
1163
|
+
// The current layout snapshot could be stored as a "previously calculated layout" variable
|
|
1164
|
+
// so that it could theoretically be used when calculating new layout incrementally
|
|
1165
|
+
// rather than from scratch, which would be an optimization.
|
|
1166
|
+
//
|
|
1167
|
+
updatePreviouslyCalculatedLayoutOnItemHeightChange(i, previousHeight, newHeight) {
|
|
1168
|
+
if (this.previouslyCalculatedLayout) {
|
|
1169
|
+
const heightDifference = newHeight - previousHeight
|
|
1170
|
+
if (i < this.previouslyCalculatedLayout.firstShownItemIndex) {
|
|
1171
|
+
// Patch `this.previouslyCalculatedLayout`'s `.beforeItemsHeight`.
|
|
1172
|
+
this.previouslyCalculatedLayout.beforeItemsHeight += heightDifference
|
|
1173
|
+
} else if (i > this.previouslyCalculatedLayout.lastShownItemIndex) {
|
|
1174
|
+
// Could patch `.afterItemsHeight` of `this.previouslyCalculatedLayout` here,
|
|
1175
|
+
// if `.afterItemsHeight` property existed in `this.previouslyCalculatedLayout`.
|
|
1176
|
+
if (this.previouslyCalculatedLayout.afterItemsHeight !== undefined) {
|
|
1177
|
+
this.previouslyCalculatedLayout.afterItemsHeight += heightDifference
|
|
1178
|
+
}
|
|
1179
|
+
} else {
|
|
1180
|
+
// Patch `this.previouslyCalculatedLayout`'s shown items height.
|
|
1181
|
+
this.previouslyCalculatedLayout.shownItemsHeight += newHeight - previousHeight
|
|
1182
|
+
}
|
|
707
1183
|
}
|
|
708
1184
|
}
|
|
709
1185
|
|
|
@@ -756,8 +1232,13 @@ export default class VirtualScroller {
|
|
|
756
1232
|
const previouslyMeasuredItemHeight = this.getState().itemHeights[i]
|
|
757
1233
|
const actualItemHeight = this.remeasureItemHeight(i)
|
|
758
1234
|
if (actualItemHeight !== previouslyMeasuredItemHeight) {
|
|
1235
|
+
if (isValid) {
|
|
1236
|
+
log('~ Validate will-be-hidden item heights. ~')
|
|
1237
|
+
// Update or reset previously calculated layout.
|
|
1238
|
+
this.updatePreviouslyCalculatedLayoutOnItemHeightChange(i, previouslyMeasuredItemHeight, actualItemHeight)
|
|
1239
|
+
}
|
|
759
1240
|
isValid = false
|
|
760
|
-
warn('Item', i, '
|
|
1241
|
+
warn('Item index', i, 'is no longer visible and will be unmounted. Its height has changed from', previouslyMeasuredItemHeight, 'to', actualItemHeight, 'since it was last measured. This is not necessarily a bug, and could happen, for example, on screen width change, or when there\'re several `onItemHeightChange(i)` calls issued at the same time, and the first one triggers a re-layout before the rest of them have had a chance to be executed.')
|
|
761
1242
|
}
|
|
762
1243
|
}
|
|
763
1244
|
i++
|
|
@@ -765,44 +1246,80 @@ export default class VirtualScroller {
|
|
|
765
1246
|
return isValid
|
|
766
1247
|
}
|
|
767
1248
|
|
|
1249
|
+
getShownItemIndexes() {
|
|
1250
|
+
const itemsCount = this.getItemsCount()
|
|
1251
|
+
|
|
1252
|
+
const {
|
|
1253
|
+
top: visibleAreaTop,
|
|
1254
|
+
bottom: visibleAreaBottom
|
|
1255
|
+
} = this.getVisibleArea()
|
|
1256
|
+
|
|
1257
|
+
if (this.bypass) {
|
|
1258
|
+
return {
|
|
1259
|
+
firstShownItemIndex: 0,
|
|
1260
|
+
lastShownItemIndex: itemsCount - 1,
|
|
1261
|
+
// shownItemsHeight: this.getState().itemHeights.reduce((sum, itemHeight) => sum + itemHeight, 0)
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
// Find the indexes of the items that are currently visible
|
|
1266
|
+
// (or close to being visible) in the scrollable container.
|
|
1267
|
+
// For scrollable containers other than the main screen, it could also
|
|
1268
|
+
// check the visibility of such scrollable container itself, because it
|
|
1269
|
+
// might be not visible.
|
|
1270
|
+
// If such kind of an optimization would hypothetically be implemented,
|
|
1271
|
+
// then it would also require listening for "scroll" events on the screen.
|
|
1272
|
+
// Overall, I suppose that such "actual visibility" feature would be
|
|
1273
|
+
// a very minor optimization and not something I'd deal with.
|
|
1274
|
+
const isVisible = visibleAreaTop < this.itemsContainer.getHeight() && visibleAreaBottom > 0
|
|
1275
|
+
if (!isVisible) {
|
|
1276
|
+
log('The entire list is off-screen. No items are visible.')
|
|
1277
|
+
return this.layout.getNonVisibleListShownItemIndexes()
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// Get shown item indexes.
|
|
1281
|
+
return this.layout.getShownItemIndexes({
|
|
1282
|
+
itemsCount: this.getItemsCount(),
|
|
1283
|
+
visibleAreaTop,
|
|
1284
|
+
visibleAreaBottom
|
|
1285
|
+
})
|
|
1286
|
+
}
|
|
1287
|
+
|
|
768
1288
|
/**
|
|
769
1289
|
* Updates the "from" and "to" shown item indexes.
|
|
770
1290
|
* If the list is visible and some of the items being shown are new
|
|
771
1291
|
* and are required to be measured first, then
|
|
772
|
-
* `
|
|
1292
|
+
* `firstNonMeasuredItemIndex` is defined.
|
|
773
1293
|
* If the list is visible and all items being shown have been encountered
|
|
774
|
-
* (and measured) before, then `
|
|
1294
|
+
* (and measured) before, then `firstNonMeasuredItemIndex` is `undefined`.
|
|
1295
|
+
*
|
|
1296
|
+
* The `stateUpdate` parameter is just an optional "additional" state update.
|
|
775
1297
|
*/
|
|
776
|
-
updateShownItemIndexes = () => {
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
this.latestLayoutVisibleAreaIncludingMargins = visibleAreaIncludingMargins
|
|
780
|
-
const listTopOffsetInsideScrollableContainer = this.getListTopOffsetInsideScrollableContainer()
|
|
1298
|
+
updateShownItemIndexes = ({ stateUpdate }) => {
|
|
1299
|
+
const startedAt = Date.now()
|
|
1300
|
+
|
|
781
1301
|
// Get shown item indexes.
|
|
782
1302
|
let {
|
|
783
1303
|
firstShownItemIndex,
|
|
784
1304
|
lastShownItemIndex,
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
visibleAreaIncludingMargins,
|
|
790
|
-
listTopOffsetInsideScrollableContainer
|
|
791
|
-
})
|
|
1305
|
+
shownItemsHeight,
|
|
1306
|
+
firstNonMeasuredItemIndex
|
|
1307
|
+
} = this.getShownItemIndexes()
|
|
1308
|
+
|
|
792
1309
|
// If scroll position is scheduled to be restored after render,
|
|
793
1310
|
// then the "anchor" item must be rendered, and all of the prepended
|
|
794
1311
|
// items before it, all in a single pass. This way, all of the
|
|
795
1312
|
// prepended items' heights could be measured right after the render
|
|
796
1313
|
// has finished, and the scroll position can then be immediately restored.
|
|
797
|
-
if (this.
|
|
798
|
-
if (lastShownItemIndex < this.
|
|
799
|
-
lastShownItemIndex = this.
|
|
1314
|
+
if (this.listHeightChangeWatcher.hasSnapshot()) {
|
|
1315
|
+
if (lastShownItemIndex < this.listHeightChangeWatcher.getAnchorItemIndex()) {
|
|
1316
|
+
lastShownItemIndex = this.listHeightChangeWatcher.getAnchorItemIndex()
|
|
800
1317
|
}
|
|
801
1318
|
// `firstShownItemIndex` is always `0` when prepending items.
|
|
802
1319
|
// And `lastShownItemIndex` always covers all prepended items in this case.
|
|
803
1320
|
// None of the prepended items have been rendered before,
|
|
804
1321
|
// so their heights are unknown. The code at the start of this function
|
|
805
|
-
// did therefore set `
|
|
1322
|
+
// did therefore set `firstNonMeasuredItemIndex` to non-`undefined`
|
|
806
1323
|
// in order to render just the first prepended item in order to
|
|
807
1324
|
// measure it, and only then make a decision on how many other
|
|
808
1325
|
// prepended items to render. But since we've instructed the code
|
|
@@ -812,8 +1329,9 @@ export default class VirtualScroller {
|
|
|
812
1329
|
// "jitter" due to the scroll position not being restored because it'd
|
|
813
1330
|
// wait for the second layout to finish instead of being restored
|
|
814
1331
|
// right after the first one.
|
|
815
|
-
|
|
1332
|
+
firstNonMeasuredItemIndex = undefined
|
|
816
1333
|
}
|
|
1334
|
+
|
|
817
1335
|
// Validate the heights of items to be hidden on next render.
|
|
818
1336
|
// For example, a user could click a "Show more" button,
|
|
819
1337
|
// or an "Expand YouTube video" button, which would result
|
|
@@ -821,22 +1339,31 @@ export default class VirtualScroller {
|
|
|
821
1339
|
// from what has been initially measured in `this.itemHeights[i]`,
|
|
822
1340
|
// if the developer didn't call `.onItemStateChange()` and `.onItemHeightChange(i)`.
|
|
823
1341
|
if (!this.validateWillBeHiddenItemHeightsAreAccurate(firstShownItemIndex, lastShownItemIndex)) {
|
|
1342
|
+
log('~ Because some of the will-be-hidden item heights (listed above) have changed since they\'ve last been measured, redo layout. ~')
|
|
824
1343
|
// Redo layout, now with the correct item heights.
|
|
825
|
-
|
|
826
|
-
return this.updateShownItemIndexes();
|
|
1344
|
+
return this.updateShownItemIndexes({ stateUpdate });
|
|
827
1345
|
}
|
|
1346
|
+
|
|
828
1347
|
// Measure "before" items height.
|
|
829
1348
|
const beforeItemsHeight = this.layout.getBeforeItemsHeight(
|
|
830
|
-
firstShownItemIndex
|
|
831
|
-
lastShownItemIndex
|
|
1349
|
+
firstShownItemIndex
|
|
832
1350
|
)
|
|
1351
|
+
|
|
833
1352
|
// Measure "after" items height.
|
|
834
1353
|
const afterItemsHeight = this.layout.getAfterItemsHeight(
|
|
835
|
-
firstShownItemIndex,
|
|
836
1354
|
lastShownItemIndex,
|
|
837
1355
|
this.getItemsCount()
|
|
838
1356
|
)
|
|
1357
|
+
|
|
1358
|
+
const layoutDuration = Date.now() - startedAt
|
|
1359
|
+
|
|
839
1360
|
// Debugging.
|
|
1361
|
+
log('~ Layout values ' + (this.bypass ? '(bypass) ' : '') + '~')
|
|
1362
|
+
if (layoutDuration < SLOW_LAYOUT_DURATION) {
|
|
1363
|
+
// log('Calculated in', layoutDuration, 'ms')
|
|
1364
|
+
} else {
|
|
1365
|
+
warn('Layout calculated in', layoutDuration, 'ms')
|
|
1366
|
+
}
|
|
840
1367
|
if (this._getColumnsCount) {
|
|
841
1368
|
log('Columns count', this.getColumnsCount())
|
|
842
1369
|
}
|
|
@@ -844,16 +1371,12 @@ export default class VirtualScroller {
|
|
|
844
1371
|
log('Last shown item index', lastShownItemIndex)
|
|
845
1372
|
log('Before items height', beforeItemsHeight)
|
|
846
1373
|
log('After items height (actual or estimated)', afterItemsHeight)
|
|
847
|
-
log('Average item height (
|
|
1374
|
+
log('Average item height (used for estimated after items height calculation)', this.itemHeights.getAverage())
|
|
848
1375
|
if (isDebug()) {
|
|
849
1376
|
log('Item heights', this.getState().itemHeights.slice())
|
|
850
1377
|
log('Item states', this.getState().itemStates.slice())
|
|
851
1378
|
}
|
|
852
|
-
|
|
853
|
-
// `this.redoLayoutReason` will be detected in `didUpdateState()`.
|
|
854
|
-
// `didUpdateState()` is triggered by `this.setState()` below.
|
|
855
|
-
this.redoLayoutReason = LAYOUT_REASON.ITEM_HEIGHT_NOT_MEASURED
|
|
856
|
-
}
|
|
1379
|
+
|
|
857
1380
|
// Optionally preload items to be rendered.
|
|
858
1381
|
this.onBeforeShowItems(
|
|
859
1382
|
this.getState().items,
|
|
@@ -861,34 +1384,109 @@ export default class VirtualScroller {
|
|
|
861
1384
|
firstShownItemIndex,
|
|
862
1385
|
lastShownItemIndex
|
|
863
1386
|
)
|
|
864
|
-
|
|
1387
|
+
|
|
1388
|
+
// Set `this.firstNonMeasuredItemIndex`.
|
|
1389
|
+
this.firstNonMeasuredItemIndex = firstNonMeasuredItemIndex
|
|
1390
|
+
|
|
1391
|
+
// Set "previously calculated layout".
|
|
1392
|
+
//
|
|
1393
|
+
// The "previously calculated layout" feature is not currently used.
|
|
1394
|
+
//
|
|
1395
|
+
// The current layout snapshot could be stored as a "previously calculated layout" variable
|
|
1396
|
+
// so that it could theoretically be used when calculating new layout incrementally
|
|
1397
|
+
// rather than from scratch, which would be an optimization.
|
|
1398
|
+
//
|
|
1399
|
+
// Currently, this feature is not used, and `shownItemsHeight` property
|
|
1400
|
+
// is not returned at all, so don't set any "previously calculated layout".
|
|
1401
|
+
//
|
|
1402
|
+
if (shownItemsHeight === undefined) {
|
|
1403
|
+
this.previouslyCalculatedLayout = undefined
|
|
1404
|
+
} else {
|
|
1405
|
+
// If "previously calculated layout" feature would be implmeneted,
|
|
1406
|
+
// then this code would set "previously calculate layout" instance variable.
|
|
1407
|
+
//
|
|
1408
|
+
// What for would this instance variable be used?
|
|
1409
|
+
//
|
|
1410
|
+
// Instead of using a `this.previouslyCalculatedLayout` instance variable,
|
|
1411
|
+
// this code could use `this.getState()` because it reflects what's currently on screen,
|
|
1412
|
+
// but there's a single edge case when it could go out of sync —
|
|
1413
|
+
// updating item heights externally via `.onItemHeightChange(i)`.
|
|
1414
|
+
//
|
|
1415
|
+
// If, for example, an item height was updated externally via `.onItemHeightChange(i)`
|
|
1416
|
+
// then `this.getState().itemHeights` would get updated immediately but
|
|
1417
|
+
// `this.getState().beforeItemsHeight` or `this.getState().afterItemsHeight`
|
|
1418
|
+
// would still correspond to the previous item height, so those would be "stale".
|
|
1419
|
+
// On the other hand, same values in `this.previouslyCalculatedLayout` instance variable
|
|
1420
|
+
// can also be updated immediately, so they won't go out of sync with the updated item height.
|
|
1421
|
+
// That seems the only edge case when using a separate `this.previouslyCalculatedLayout`
|
|
1422
|
+
// instance variable instead of using `this.getState()` would theoretically be justified.
|
|
1423
|
+
//
|
|
1424
|
+
this.previouslyCalculatedLayout = {
|
|
1425
|
+
firstShownItemIndex,
|
|
1426
|
+
lastShownItemIndex,
|
|
1427
|
+
beforeItemsHeight,
|
|
1428
|
+
shownItemsHeight
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
// Update `VirtualScroller` state.
|
|
1433
|
+
// `VirtualScroller` automatically re-renders on state updates.
|
|
1434
|
+
//
|
|
1435
|
+
// All `state` properties updated here should be overwritten in
|
|
1436
|
+
// the implementation of `setItems()` and `onResize()` methods
|
|
1437
|
+
// so that the `state` is not left in an inconsistent state
|
|
1438
|
+
// whenever there're concurrent `setState()` updates that could
|
|
1439
|
+
// possibly conflict with one another — instead, those state updates
|
|
1440
|
+
// should overwrite each other in terms of priority.
|
|
1441
|
+
// These "on scroll" updates have the lowest priority compared to
|
|
1442
|
+
// the state updates originating from `setItems()` and `onResize()` methods.
|
|
1443
|
+
//
|
|
865
1444
|
this.setState({
|
|
866
1445
|
firstShownItemIndex,
|
|
867
1446
|
lastShownItemIndex,
|
|
868
1447
|
beforeItemsHeight,
|
|
869
1448
|
afterItemsHeight,
|
|
870
|
-
|
|
871
|
-
// // the initial state and "anything has been measured already" state.
|
|
872
|
-
// averageItemHeight: this.itemHeights.getAverage()
|
|
1449
|
+
...stateUpdate
|
|
873
1450
|
})
|
|
874
1451
|
}
|
|
875
1452
|
|
|
876
|
-
onUpdateShownItemIndexes = ({ reason }) => {
|
|
1453
|
+
onUpdateShownItemIndexes = ({ reason, stateUpdate }) => {
|
|
1454
|
+
// In case of "don't do anything".
|
|
1455
|
+
const skip = () => {
|
|
1456
|
+
if (stateUpdate) {
|
|
1457
|
+
this.setState(stateUpdate)
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
// If new `items` have been set and are waiting to be applied,
|
|
1462
|
+
// or if the viewport width has changed requiring a re-layout,
|
|
1463
|
+
// then temporarily stop all other updates like "on scroll" updates.
|
|
1464
|
+
// This prevents `state` being inconsistent, because, for example,
|
|
1465
|
+
// both `setItems()` and this function could update `VirtualScroller` state
|
|
1466
|
+
// and having them operate in parallel could result in incorrectly calculated
|
|
1467
|
+
// `beforeItemsHeight` / `afterItemsHeight` / `firstShownItemIndex` /
|
|
1468
|
+
// `lastShownItemIndex`, because, when operating in parallel, this function
|
|
1469
|
+
// would have different `items` than the `setItems()` function, so their
|
|
1470
|
+
// results could diverge.
|
|
1471
|
+
if (this.newItemsWillBeRendered || this.resetLayoutAfterResize || this.isResizing) {
|
|
1472
|
+
return skip()
|
|
1473
|
+
}
|
|
1474
|
+
|
|
877
1475
|
// If there're no items then there's no need to re-layout anything.
|
|
878
1476
|
if (this.getItemsCount() === 0) {
|
|
879
|
-
return
|
|
1477
|
+
return skip()
|
|
880
1478
|
}
|
|
1479
|
+
|
|
881
1480
|
// Cancel a "re-layout when user stops scrolling" timer.
|
|
882
|
-
this.scroll.
|
|
1481
|
+
this.scroll.cancelScheduledLayout()
|
|
1482
|
+
|
|
883
1483
|
// Cancel a re-layout that is scheduled to run at the next "frame",
|
|
884
1484
|
// because a re-layout will be performed right now.
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
this.layoutTimer = undefined
|
|
888
|
-
}
|
|
1485
|
+
stateUpdate = this.cancelLayoutTimer({ stateUpdate })
|
|
1486
|
+
|
|
889
1487
|
// Perform a re-layout.
|
|
890
|
-
log(`~
|
|
891
|
-
this.updateShownItemIndexes()
|
|
1488
|
+
log(`~ Update Layout (on ${reason}) ~`)
|
|
1489
|
+
this.updateShownItemIndexes({ stateUpdate })
|
|
892
1490
|
}
|
|
893
1491
|
|
|
894
1492
|
updateLayout = () => this.onUpdateShownItemIndexes({ reason: LAYOUT_REASON.MANUAL })
|
|
@@ -914,38 +1512,103 @@ export default class VirtualScroller {
|
|
|
914
1512
|
const {
|
|
915
1513
|
items: previousItems
|
|
916
1514
|
} = this.getState()
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
1515
|
+
|
|
1516
|
+
// Even if `newItems` are equal to `this.state.items`,
|
|
1517
|
+
// still perform a `setState()` call, because, if `setState()` calls
|
|
1518
|
+
// were "asynchronous", there could be a situation when a developer
|
|
1519
|
+
// first calls `setItems(newItems)` and then `setItems(oldItems)`:
|
|
1520
|
+
// if this function did `return` `if (newItems === this.state.items)`
|
|
1521
|
+
// then `setState({ items: newItems })` would be scheduled as part of
|
|
1522
|
+
// `setItems(newItems)` call, but the subsequent `setItems(oldItems)` call
|
|
1523
|
+
// wouldn't do anything resulting in `newItems` being set as a result,
|
|
1524
|
+
// and that wouldn't be what the developer intended.
|
|
1525
|
+
|
|
1526
|
+
let { itemStates } = this.getState()
|
|
1527
|
+
let { itemHeights } = this.resetLayoutAfterResize
|
|
1528
|
+
? this.resetLayoutAfterResize.stateUpdate
|
|
1529
|
+
: this.getState()
|
|
1530
|
+
|
|
921
1531
|
log('~ Update items ~')
|
|
922
|
-
|
|
1532
|
+
|
|
1533
|
+
let layoutUpdate
|
|
1534
|
+
let itemsUpdateInfo
|
|
1535
|
+
|
|
1536
|
+
// Compare the new items to the current items.
|
|
923
1537
|
const itemsDiff = this.getItemsDiff(previousItems, newItems)
|
|
924
|
-
|
|
925
|
-
if
|
|
1538
|
+
|
|
1539
|
+
// See if it's an "incremental" items update.
|
|
1540
|
+
if (itemsDiff) {
|
|
926
1541
|
const {
|
|
927
1542
|
firstShownItemIndex,
|
|
928
1543
|
lastShownItemIndex,
|
|
929
1544
|
beforeItemsHeight,
|
|
930
1545
|
afterItemsHeight
|
|
931
|
-
} = this.
|
|
932
|
-
|
|
1546
|
+
} = this.resetLayoutAfterResize
|
|
1547
|
+
? this.resetLayoutAfterResize.stateUpdate
|
|
1548
|
+
: this.getState()
|
|
1549
|
+
|
|
1550
|
+
const shouldRestoreScrollPosition = firstShownItemIndex === 0 &&
|
|
1551
|
+
// `preserveScrollPosition` option name is deprecated,
|
|
1552
|
+
// use `preserveScrollPositionOnPrependItems` instead.
|
|
1553
|
+
(options.preserveScrollPositionOnPrependItems || options.preserveScrollPosition)
|
|
1554
|
+
|
|
1555
|
+
const {
|
|
1556
|
+
prependedItemsCount,
|
|
1557
|
+
appendedItemsCount
|
|
1558
|
+
} = itemsDiff
|
|
1559
|
+
|
|
1560
|
+
layoutUpdate = this.layout.getLayoutUpdateForItemsDiff({
|
|
933
1561
|
firstShownItemIndex,
|
|
934
1562
|
lastShownItemIndex,
|
|
935
1563
|
beforeItemsHeight,
|
|
936
1564
|
afterItemsHeight
|
|
937
|
-
}
|
|
938
|
-
const {
|
|
1565
|
+
}, {
|
|
939
1566
|
prependedItemsCount,
|
|
940
1567
|
appendedItemsCount
|
|
941
|
-
}
|
|
1568
|
+
}, {
|
|
1569
|
+
itemsCount: newItems.length,
|
|
1570
|
+
columnsCount: this.getActualColumnsCount(),
|
|
1571
|
+
shouldRestoreScrollPosition
|
|
1572
|
+
})
|
|
1573
|
+
|
|
942
1574
|
if (prependedItemsCount > 0) {
|
|
943
1575
|
log('Prepend', prependedItemsCount, 'items')
|
|
1576
|
+
|
|
944
1577
|
itemHeights = new Array(prependedItemsCount).concat(itemHeights)
|
|
1578
|
+
|
|
945
1579
|
if (itemStates) {
|
|
946
1580
|
itemStates = new Array(prependedItemsCount).concat(itemStates)
|
|
947
1581
|
}
|
|
1582
|
+
|
|
1583
|
+
// Restore scroll position after prepending items (if requested).
|
|
1584
|
+
if (shouldRestoreScrollPosition) {
|
|
1585
|
+
log('Will restore scroll position')
|
|
1586
|
+
this.listHeightChangeWatcher.snapshot({
|
|
1587
|
+
previousItems,
|
|
1588
|
+
newItems,
|
|
1589
|
+
prependedItemsCount
|
|
1590
|
+
})
|
|
1591
|
+
// "Seamless prepend" scenario doesn't result in a re-layout,
|
|
1592
|
+
// so if any "non measured item" is currently pending,
|
|
1593
|
+
// it doesn't get reset and will be handled after `state` is updated.
|
|
1594
|
+
if (this.firstNonMeasuredItemIndex !== undefined) {
|
|
1595
|
+
this.firstNonMeasuredItemIndex += prependedItemsCount
|
|
1596
|
+
}
|
|
1597
|
+
} else {
|
|
1598
|
+
log('Reset layout')
|
|
1599
|
+
// Reset layout because none of the prepended items have been measured.
|
|
1600
|
+
layoutUpdate = this.layout.getInitialLayoutValues({
|
|
1601
|
+
itemsCount: newItems.length,
|
|
1602
|
+
columnsCount: this.getActualColumnsCount()
|
|
1603
|
+
})
|
|
1604
|
+
// Unschedule a potentially scheduled layout update
|
|
1605
|
+
// after measuring a previously non-measured item
|
|
1606
|
+
// because the list will be re-layout anyway
|
|
1607
|
+
// due to the new items being set.
|
|
1608
|
+
this.firstNonMeasuredItemIndex = undefined
|
|
1609
|
+
}
|
|
948
1610
|
}
|
|
1611
|
+
|
|
949
1612
|
if (appendedItemsCount > 0) {
|
|
950
1613
|
log('Append', appendedItemsCount, 'items')
|
|
951
1614
|
itemHeights = itemHeights.concat(new Array(appendedItemsCount))
|
|
@@ -953,58 +1616,349 @@ export default class VirtualScroller {
|
|
|
953
1616
|
itemStates = itemStates.concat(new Array(appendedItemsCount))
|
|
954
1617
|
}
|
|
955
1618
|
}
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
// `preserveScrollPosition` option name is deprecated,
|
|
961
|
-
// use `preserveScrollPositionOnPrependItems` instead.
|
|
962
|
-
if (options.preserveScrollPositionOnPrependItems || options.preserveScrollPosition) {
|
|
963
|
-
if (this.getState().firstShownItemIndex === 0) {
|
|
964
|
-
this.restoreScroll.captureScroll({
|
|
965
|
-
previousItems,
|
|
966
|
-
newItems,
|
|
967
|
-
prependedItemsCount
|
|
968
|
-
})
|
|
969
|
-
this.layout.showItemsFromTheStart(layout)
|
|
970
|
-
}
|
|
971
|
-
}
|
|
1619
|
+
|
|
1620
|
+
itemsUpdateInfo = {
|
|
1621
|
+
prepend: prependedItemsCount > 0,
|
|
1622
|
+
append: appendedItemsCount > 0
|
|
972
1623
|
}
|
|
973
1624
|
} else {
|
|
974
1625
|
log('Items have changed, and', (itemsDiff ? 'a re-layout from scratch has been requested.' : 'it\'s not a simple append and/or prepend.'), 'Rerender the entire list from scratch.')
|
|
975
1626
|
log('Previous items', previousItems)
|
|
976
1627
|
log('New items', newItems)
|
|
1628
|
+
|
|
1629
|
+
// Reset item heights and item states.
|
|
977
1630
|
itemHeights = new Array(newItems.length)
|
|
978
1631
|
itemStates = new Array(newItems.length)
|
|
979
|
-
|
|
980
|
-
|
|
1632
|
+
|
|
1633
|
+
layoutUpdate = this.layout.getInitialLayoutValues({
|
|
1634
|
+
itemsCount: newItems.length,
|
|
1635
|
+
columnsCount: this.getActualColumnsCount()
|
|
981
1636
|
})
|
|
1637
|
+
|
|
1638
|
+
// Unschedule a potentially scheduled layout update
|
|
1639
|
+
// after measuring a previously non-measured item
|
|
1640
|
+
// because the list will be re-layout from scratch
|
|
1641
|
+
// due to the new items being set.
|
|
1642
|
+
this.firstNonMeasuredItemIndex = undefined
|
|
1643
|
+
|
|
1644
|
+
// Also reset any potential pending scroll position restoration.
|
|
1645
|
+
// For example, imagine a developer first called `.setItems(incrementalItemsUpdate)`
|
|
1646
|
+
// and then called `.setItems(differentItems)` and there was no state update
|
|
1647
|
+
// in between those two calls. This could happen because state updates aren't
|
|
1648
|
+
// required to be "synchronous". On other words, calling `this.setState()`
|
|
1649
|
+
// doesn't necessarily mean that the state is applied immediately.
|
|
1650
|
+
// Imagine also that such "delayed" state updates could be batched,
|
|
1651
|
+
// like they do in React inside event handlers (though that doesn't apply to this case):
|
|
1652
|
+
// https://github.com/facebook/react/issues/10231#issuecomment-316644950
|
|
1653
|
+
// If `this.listHeightChangeWatcher` wasn't reset on `.setItems(differentItems)`
|
|
1654
|
+
// and if the second `this.setState()` call overwrites the first one
|
|
1655
|
+
// then it would attempt to restore scroll position in a situation when
|
|
1656
|
+
// it should no longer do that. Hence the reset here.
|
|
1657
|
+
this.listHeightChangeWatcher.reset()
|
|
1658
|
+
|
|
1659
|
+
itemsUpdateInfo = {
|
|
1660
|
+
replace: true
|
|
1661
|
+
}
|
|
982
1662
|
}
|
|
1663
|
+
|
|
983
1664
|
log('~ Update state ~')
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1665
|
+
|
|
1666
|
+
// const layoutValuesAfterUpdate = {
|
|
1667
|
+
// ...this.getState(),
|
|
1668
|
+
// ...layoutUpdate
|
|
1669
|
+
// }
|
|
1670
|
+
|
|
1671
|
+
// `layoutUpdate` is equivalent to `layoutValuesAfterUpdate` because
|
|
1672
|
+
// `layoutUpdate` contains all the relevant properties.
|
|
1673
|
+
log('First shown item index', layoutUpdate.firstShownItemIndex)
|
|
1674
|
+
log('Last shown item index', layoutUpdate.lastShownItemIndex)
|
|
1675
|
+
log('Before items height', layoutUpdate.beforeItemsHeight)
|
|
1676
|
+
log('After items height (actual or estimated)', layoutUpdate.afterItemsHeight)
|
|
1677
|
+
|
|
988
1678
|
// Optionally preload items to be rendered.
|
|
1679
|
+
//
|
|
1680
|
+
// `layoutUpdate` is equivalent to `layoutValuesAfterUpdate` because
|
|
1681
|
+
// `layoutUpdate` contains all the relevant properties.
|
|
1682
|
+
//
|
|
989
1683
|
this.onBeforeShowItems(
|
|
990
1684
|
newItems,
|
|
991
1685
|
itemHeights,
|
|
992
|
-
|
|
993
|
-
|
|
1686
|
+
layoutUpdate.firstShownItemIndex,
|
|
1687
|
+
layoutUpdate.lastShownItemIndex
|
|
994
1688
|
)
|
|
995
|
-
|
|
996
|
-
this.
|
|
997
|
-
//
|
|
998
|
-
|
|
1689
|
+
|
|
1690
|
+
// `this.newItemsWillBeRendered` signals that new `items` are being rendered,
|
|
1691
|
+
// and that `VirtualScroller` should temporarily stop all other updates.
|
|
1692
|
+
//
|
|
1693
|
+
// `this.newItemsWillBeRendered` is cleared in `didUpdateState()`.
|
|
1694
|
+
//
|
|
1695
|
+
// The values in `this.newItemsWillBeRendered` are used, for example,
|
|
1696
|
+
// in `.onResize()` handler in order to not break state consistency when
|
|
1697
|
+
// state updates are "asynchronous" (delayed) and there's a window resize event
|
|
1698
|
+
// in between calling `setState()` below and that call actually being applied.
|
|
1699
|
+
//
|
|
1700
|
+
this.newItemsWillBeRendered = {
|
|
1701
|
+
...itemsUpdateInfo,
|
|
1702
|
+
count: newItems.length,
|
|
1703
|
+
// `layoutUpdate` now contains all layout-related properties, even if those that
|
|
1704
|
+
// didn't change. So `firstShownItemIndex` is always in `this.newItemsWillBeRendered`.
|
|
1705
|
+
layout: layoutUpdate
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
// `layoutUpdate` now contains all layout-related properties, even if those that
|
|
1709
|
+
// didn't change. So this part is no longer relevant.
|
|
1710
|
+
//
|
|
1711
|
+
// // If `firstShownItemIndex` is gonna be modified as a result of setting new items
|
|
1712
|
+
// // then keep that "new" `firstShownItemIndex` in order for it to be used by
|
|
1713
|
+
// // `onResize()` handler when it calculates "new" `firstShownItemIndex`
|
|
1714
|
+
// // based on the new columns count (corresponding to the new window width).
|
|
1715
|
+
// if (layoutUpdate.firstShownItemIndex !== undefined) {
|
|
1716
|
+
// this.newItemsWillBeRendered = {
|
|
1717
|
+
// ...this.newItemsWillBeRendered,
|
|
1718
|
+
// firstShownItemIndex: layoutUpdate.firstShownItemIndex
|
|
1719
|
+
// }
|
|
1720
|
+
// }
|
|
1721
|
+
|
|
1722
|
+
// Update `VirtualScroller` state.
|
|
1723
|
+
//
|
|
1724
|
+
// This state update should overwrite all the `state` properties
|
|
1725
|
+
// that are also updated in the "on scroll" handler (`getShownItemIndexes()`):
|
|
1726
|
+
//
|
|
1727
|
+
// * `firstShownItemIndex`
|
|
1728
|
+
// * `lastShownItemIndex`
|
|
1729
|
+
// * `beforeItemsHeight`
|
|
1730
|
+
// * `afterItemsHeight`
|
|
1731
|
+
//
|
|
1732
|
+
// That's because this `setState()` update has a higher priority
|
|
1733
|
+
// than that of the "on scroll" handler, so it should overwrite
|
|
1734
|
+
// any potential state changes dispatched by the "on scroll" handler.
|
|
1735
|
+
//
|
|
1736
|
+
const newState = {
|
|
999
1737
|
// ...customState,
|
|
1000
|
-
...
|
|
1738
|
+
...layoutUpdate,
|
|
1001
1739
|
items: newItems,
|
|
1002
1740
|
itemStates,
|
|
1003
1741
|
itemHeights
|
|
1004
|
-
}
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
// Introduced `shouldIncludeBeforeResizeValuesInState()` getter just to prevent
|
|
1745
|
+
// cluttering `state` with `beforeResize: undefined` property if `beforeResize`
|
|
1746
|
+
// hasn't ever been set in `state` previously.
|
|
1747
|
+
if (this.beforeResize.shouldIncludeBeforeResizeValuesInState()) {
|
|
1748
|
+
if (this.shouldDiscardBeforeResizeItemHeights()) {
|
|
1749
|
+
// Reset "before resize" item heights because now there're new items prepended
|
|
1750
|
+
// with unknown heights, or completely new items with unknown heights, so
|
|
1751
|
+
// `beforeItemsHeight` value won't be preserved anyway.
|
|
1752
|
+
newState.beforeResize = undefined
|
|
1753
|
+
}
|
|
1754
|
+
else {
|
|
1755
|
+
// Overwrite `beforeResize` property in `state` even if it wasn't modified
|
|
1756
|
+
// because state updates could be "asynchronous" and in that case there could be
|
|
1757
|
+
// some previous `setState()` call from some previous `setItems()` call that
|
|
1758
|
+
// hasn't yet been applied, and that previous call might have scheduled setting
|
|
1759
|
+
// `state.beforeResize` property to `undefined` in order to reset it, but this
|
|
1760
|
+
// next `setState()` call might not require resetting `state.beforeResize` property
|
|
1761
|
+
// so it should undo resetting it by simply overwriting it with its normal value.
|
|
1762
|
+
newState.beforeResize = this.resetLayoutAfterResize
|
|
1763
|
+
? this.resetLayoutAfterResize.stateUpdate.beforeResize
|
|
1764
|
+
: this.getState().beforeResize
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
// `newState` should also overwrite all `state` properties that're updated in `onResize()`
|
|
1769
|
+
// because `setItems()`'s state updates always overwrite `onResize()`'s state updates.
|
|
1770
|
+
// (The least-priority ones are `onScroll()` state updates, but those're simply skipped
|
|
1771
|
+
// if there's a pending `setItems()` or `onResize()` update).
|
|
1772
|
+
//
|
|
1773
|
+
// `state` property exceptions:
|
|
1774
|
+
//
|
|
1775
|
+
// `verticalSpacing` property is not updated here because it's fine setting it to
|
|
1776
|
+
// `undefined` in `onResize()` — it will simply be re-measured after the component re-renders.
|
|
1777
|
+
//
|
|
1778
|
+
// `columnsCount` property is also not updated here because by definition it's only
|
|
1779
|
+
// updated in `onResize()`.
|
|
1780
|
+
|
|
1781
|
+
// Render.
|
|
1782
|
+
this.setState(newState)
|
|
1005
1783
|
}
|
|
1006
1784
|
|
|
1007
1785
|
getItemsDiff(previousItems, newItems) {
|
|
1008
1786
|
return getItemsDiff(previousItems, newItems, this.isItemEqual)
|
|
1009
1787
|
}
|
|
1010
|
-
|
|
1788
|
+
|
|
1789
|
+
// Returns whether "before resize" item heights should be discarded
|
|
1790
|
+
// as a result of calling `setItems()` with a new set of items
|
|
1791
|
+
// when an asynchronous `setState()` call inside that function
|
|
1792
|
+
// hasn't been applied yet.
|
|
1793
|
+
//
|
|
1794
|
+
// If `setItems()` update was an "incremental" one and no items
|
|
1795
|
+
// have been prepended, then `firstShownItemIndex` is preserved,
|
|
1796
|
+
// and all items' heights before it should be kept in order to
|
|
1797
|
+
// preserve the top offset of the first shown item so that there's
|
|
1798
|
+
// no "content jumping".
|
|
1799
|
+
//
|
|
1800
|
+
// If `setItems()` update was an "incremental" one but there're
|
|
1801
|
+
// some prepended items, then it means that now there're new items
|
|
1802
|
+
// with unknown heights at the top, so the top offset of the first
|
|
1803
|
+
// shown item won't be preserved because there're no "before resize"
|
|
1804
|
+
// heights of those items.
|
|
1805
|
+
//
|
|
1806
|
+
// If `setItems()` update was not an "incremental" one, then don't
|
|
1807
|
+
// attempt to restore previous item heights after a potential window
|
|
1808
|
+
// width change because all item heights have been reset.
|
|
1809
|
+
//
|
|
1810
|
+
shouldDiscardBeforeResizeItemHeights() {
|
|
1811
|
+
if (this.newItemsWillBeRendered) {
|
|
1812
|
+
const { prepend, replace } = this.newItemsWillBeRendered
|
|
1813
|
+
return prepend || replace
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
onResize() {
|
|
1818
|
+
// Reset "previously calculated layout".
|
|
1819
|
+
//
|
|
1820
|
+
// The "previously calculated layout" feature is not currently used.
|
|
1821
|
+
//
|
|
1822
|
+
// The current layout snapshot could be stored as a "previously calculated layout" variable
|
|
1823
|
+
// so that it could theoretically be used when calculating new layout incrementally
|
|
1824
|
+
// rather than from scratch, which would be an optimization.
|
|
1825
|
+
//
|
|
1826
|
+
this.previouslyCalculatedLayout = undefined
|
|
1827
|
+
|
|
1828
|
+
// Cancel any potential scheduled scroll position restoration.
|
|
1829
|
+
this.listHeightChangeWatcher.reset()
|
|
1830
|
+
|
|
1831
|
+
// Get the most recent items count.
|
|
1832
|
+
// If there're a "pending" `setItems()` call then use the items count from that call
|
|
1833
|
+
// instead of using the count of currently shown `items` from `state`.
|
|
1834
|
+
// A `setItems()` call is "pending" when `setState()` operation is "asynchronous", that is
|
|
1835
|
+
// when `setState()` calls aren't applied immediately, like in React.
|
|
1836
|
+
const itemsCount = this.newItemsWillBeRendered
|
|
1837
|
+
? this.newItemsWillBeRendered.count
|
|
1838
|
+
: this.getState().itemHeights.length
|
|
1839
|
+
|
|
1840
|
+
// If layout values have been calculated as a result of a "pending" `setItems()` call,
|
|
1841
|
+
// then don't discard those new layout values and use them instead of the ones from `state`.
|
|
1842
|
+
//
|
|
1843
|
+
// A `setItems()` call is "pending" when `setState()` operation is "asynchronous", that is
|
|
1844
|
+
// when `setState()` calls aren't applied immediately, like in React.
|
|
1845
|
+
//
|
|
1846
|
+
const layout = this.newItemsWillBeRendered
|
|
1847
|
+
? this.newItemsWillBeRendered.layout
|
|
1848
|
+
: this.getState()
|
|
1849
|
+
|
|
1850
|
+
// Update `VirtualScroller` state.
|
|
1851
|
+
const newState = {
|
|
1852
|
+
// This state update should also overwrite all the `state` properties
|
|
1853
|
+
// that are also updated in the "on scroll" handler (`getShownItemIndexes()`):
|
|
1854
|
+
//
|
|
1855
|
+
// * `firstShownItemIndex`
|
|
1856
|
+
// * `lastShownItemIndex`
|
|
1857
|
+
// * `beforeItemsHeight`
|
|
1858
|
+
// * `afterItemsHeight`
|
|
1859
|
+
//
|
|
1860
|
+
// That's because this `setState()` update has a higher priority
|
|
1861
|
+
// than that of the "on scroll" handler, so it should overwrite
|
|
1862
|
+
// any potential state changes dispatched by the "on scroll" handler.
|
|
1863
|
+
//
|
|
1864
|
+
// All these properties might have changed, but they're not
|
|
1865
|
+
// recalculated here becase they'll be recalculated after
|
|
1866
|
+
// this new state is applied (rendered).
|
|
1867
|
+
//
|
|
1868
|
+
firstShownItemIndex: layout.firstShownItemIndex,
|
|
1869
|
+
lastShownItemIndex: layout.lastShownItemIndex,
|
|
1870
|
+
beforeItemsHeight: layout.beforeItemsHeight,
|
|
1871
|
+
afterItemsHeight: layout.afterItemsHeight,
|
|
1872
|
+
|
|
1873
|
+
// Reset item heights, because if scrollable container's width (or height)
|
|
1874
|
+
// has changed, then the list width (or height) most likely also has changed,
|
|
1875
|
+
// and also some CSS `@media()` rules might have been added or removed.
|
|
1876
|
+
// So re-render the list entirely.
|
|
1877
|
+
itemHeights: new Array(itemsCount),
|
|
1878
|
+
|
|
1879
|
+
columnsCount: this.getActualColumnsCountForState(),
|
|
1880
|
+
|
|
1881
|
+
// Re-measure vertical spacing after render because new CSS styles
|
|
1882
|
+
// might be applied for the new window width.
|
|
1883
|
+
verticalSpacing: undefined
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
const { firstShownItemIndex, lastShownItemIndex } = layout
|
|
1887
|
+
|
|
1888
|
+
// Get the `columnsCount` for the new window width.
|
|
1889
|
+
const newColumnsCount = this.getActualColumnsCount()
|
|
1890
|
+
|
|
1891
|
+
// Re-calculate `firstShownItemIndex` and `lastShownItemIndex`
|
|
1892
|
+
// based on the new `columnsCount` so that the whole row is visible.
|
|
1893
|
+
const newFirstShownItemIndex = Math.floor(firstShownItemIndex / newColumnsCount) * newColumnsCount
|
|
1894
|
+
const newLastShownItemIndex = Math.ceil((lastShownItemIndex + 1) / newColumnsCount) * newColumnsCount - 1
|
|
1895
|
+
|
|
1896
|
+
// Potentially update `firstShownItemIndex` if it needs to be adjusted in order to
|
|
1897
|
+
// correspond to the new `columnsCount`.
|
|
1898
|
+
if (newFirstShownItemIndex !== firstShownItemIndex) {
|
|
1899
|
+
log('Columns Count changed from', this.getState().columnsCount || 1, 'to', newColumnsCount)
|
|
1900
|
+
log('First Shown Item Index needs to change from', firstShownItemIndex, 'to', newFirstShownItemIndex)
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
// Always rewrite `firstShownItemIndex` and `lastShownItemIndex`
|
|
1904
|
+
// as part of the `state` update, even if it hasn't been modified.
|
|
1905
|
+
//
|
|
1906
|
+
// The reason is that there could be two subsequent `onResize()` calls:
|
|
1907
|
+
// the first one could be user resizing the window to half of its width,
|
|
1908
|
+
// resulting in an "asynchronous" `setState()` call, and then, before that
|
|
1909
|
+
// `setState()` call is applied, a second resize event happens when the user
|
|
1910
|
+
// has resized the window back to its original width, meaning that the
|
|
1911
|
+
// `columnsCount` is back to its original value.
|
|
1912
|
+
// In that case, the final `newFirstShownItemIndex` will be equal to the
|
|
1913
|
+
// original `firstShownItemIndex` that was in `state` before the user
|
|
1914
|
+
// has started resizing the window, so, in the end, `state.firstShownItemIndex`
|
|
1915
|
+
// property wouldn't have changed, but it still has to be part of the final
|
|
1916
|
+
// state update in order to overwrite the previous update of `firstShownItemIndex`
|
|
1917
|
+
// property that has been scheduled to be applied in state after the first resize
|
|
1918
|
+
// happened.
|
|
1919
|
+
//
|
|
1920
|
+
newState.firstShownItemIndex = newFirstShownItemIndex
|
|
1921
|
+
newState.lastShownItemIndex = newLastShownItemIndex
|
|
1922
|
+
|
|
1923
|
+
const verticalSpacing = this.getVerticalSpacing()
|
|
1924
|
+
const columnsCount = this.getColumnsCount()
|
|
1925
|
+
|
|
1926
|
+
// `beforeResize` is always overwritten in `state` here.
|
|
1927
|
+
// (once it has started being tracked in `state`)
|
|
1928
|
+
if (this.shouldDiscardBeforeResizeItemHeights() || newFirstShownItemIndex === 0) {
|
|
1929
|
+
if (this.beforeResize.shouldIncludeBeforeResizeValuesInState()) {
|
|
1930
|
+
newState.beforeResize = undefined
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
// Snapshot "before resize" values in order to preserve the currently
|
|
1934
|
+
// shown items' vertical position on screen so that there's no "content jumping".
|
|
1935
|
+
else {
|
|
1936
|
+
// Keep "before resize" values in order to preserve the currently
|
|
1937
|
+
// shown items' vertical position on screen so that there's no
|
|
1938
|
+
// "content jumping". These "before resize" values will be discarded
|
|
1939
|
+
// when (if) the user scrolls back to the top of the list.
|
|
1940
|
+
newState.beforeResize = {
|
|
1941
|
+
verticalSpacing,
|
|
1942
|
+
columnsCount,
|
|
1943
|
+
itemHeights: this.beforeResize.snapshotBeforeResizeItemHeights({
|
|
1944
|
+
firstShownItemIndex,
|
|
1945
|
+
newFirstShownItemIndex,
|
|
1946
|
+
newColumnsCount
|
|
1947
|
+
})
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
// `this.resetLayoutAfterResize` tells `VirtualScroller` that it should
|
|
1952
|
+
// temporarily stop other updates (like "on scroll" updates) and wait
|
|
1953
|
+
// for the new `state` to be applied, after which the `didUpdateState()`
|
|
1954
|
+
// function will clear this flag and perform a re-layout.
|
|
1955
|
+
this.resetLayoutAfterResize = {
|
|
1956
|
+
stateUpdate: newState
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
// Rerender.
|
|
1960
|
+
this.setState(newState)
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
const SLOW_LAYOUT_DURATION = 15 // in milliseconds.
|