virtual-scroller 1.7.6 → 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 +29 -1
- package/README.md +139 -33
- package/babel.config.js +25 -0
- package/babel.js +5 -0
- package/bundle/index-bypass.html +1 -1
- package/bundle/index-dom.html +1 -1
- package/bundle/index-grid.html +1 -2
- package/bundle/index-scrollableContainer.html +1 -1
- package/bundle/index-tbody-scrollableContainer.html +2 -0
- package/bundle/index-tbody.html +2 -0
- package/bundle/virtual-scroller-dom.js +1 -1
- package/bundle/virtual-scroller-dom.js.map +1 -1
- package/bundle/virtual-scroller-react.js +1 -1
- package/bundle/virtual-scroller-react.js.map +1 -1
- package/bundle/virtual-scroller.js +1 -1
- package/bundle/virtual-scroller.js.map +1 -1
- package/commonjs/BeforeResize.js +319 -0
- package/commonjs/BeforeResize.js.map +1 -0
- package/commonjs/DOM/Engine.js +46 -0
- package/commonjs/DOM/Engine.js.map +1 -0
- package/commonjs/DOM/ItemsContainer.js +78 -0
- package/commonjs/DOM/ItemsContainer.js.map +1 -0
- package/commonjs/DOM/{WaitForStylesToLoad.js → ListTopOffsetWatcher.js} +56 -35
- package/commonjs/DOM/ListTopOffsetWatcher.js.map +1 -0
- package/commonjs/DOM/ScrollableContainer.js +56 -81
- package/commonjs/DOM/ScrollableContainer.js.map +1 -1
- package/commonjs/DOM/VirtualScroller.js +20 -15
- package/commonjs/DOM/VirtualScroller.js.map +1 -1
- package/commonjs/DOM/tbody.js +2 -2
- package/commonjs/ItemHeights.js +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 +55 -80
- package/modules/DOM/ScrollableContainer.js.map +1 -1
- package/modules/DOM/VirtualScroller.js +15 -14
- package/modules/DOM/VirtualScroller.js.map +1 -1
- package/modules/ItemHeights.js +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 +39 -56
- 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
package/source/Layout.js
CHANGED
|
@@ -1,45 +1,59 @@
|
|
|
1
|
-
import log from './utility/debug'
|
|
1
|
+
import log, { warn } from './utility/debug'
|
|
2
2
|
|
|
3
3
|
export default class Layout {
|
|
4
4
|
constructor({
|
|
5
5
|
bypass,
|
|
6
6
|
estimatedItemHeight,
|
|
7
7
|
measureItemsBatchSize,
|
|
8
|
+
getPrerenderMargin,
|
|
8
9
|
getVerticalSpacing,
|
|
10
|
+
getVerticalSpacingBeforeResize,
|
|
9
11
|
getColumnsCount,
|
|
12
|
+
getColumnsCountBeforeResize,
|
|
10
13
|
getItemHeight,
|
|
11
|
-
|
|
14
|
+
getItemHeightBeforeResize,
|
|
15
|
+
getBeforeResizeItemsCount,
|
|
16
|
+
getAverageItemHeight,
|
|
17
|
+
getMaxVisibleAreaHeight,
|
|
18
|
+
getPreviouslyCalculatedLayout
|
|
12
19
|
}) {
|
|
13
20
|
this.bypass = bypass
|
|
14
21
|
this.estimatedItemHeight = estimatedItemHeight
|
|
15
22
|
this.measureItemsBatchSize = measureItemsBatchSize
|
|
23
|
+
this.getPrerenderMargin = getPrerenderMargin
|
|
16
24
|
this.getVerticalSpacing = getVerticalSpacing
|
|
25
|
+
this.getVerticalSpacingBeforeResize = getVerticalSpacingBeforeResize
|
|
17
26
|
this.getColumnsCount = getColumnsCount
|
|
27
|
+
this.getColumnsCountBeforeResize = getColumnsCountBeforeResize
|
|
18
28
|
this.getItemHeight = getItemHeight
|
|
29
|
+
this.getItemHeightBeforeResize = getItemHeightBeforeResize
|
|
30
|
+
this.getBeforeResizeItemsCount = getBeforeResizeItemsCount
|
|
19
31
|
this.getAverageItemHeight = getAverageItemHeight
|
|
32
|
+
this.getMaxVisibleAreaHeight = getMaxVisibleAreaHeight
|
|
33
|
+
//
|
|
34
|
+
// The "previously calculated layout" feature is not currently used.
|
|
35
|
+
//
|
|
36
|
+
// The current layout snapshot could be stored as a "previously calculated layout" variable
|
|
37
|
+
// so that it could theoretically be used when calculating new layout incrementally
|
|
38
|
+
// rather than from scratch, which would be an optimization.
|
|
39
|
+
//
|
|
40
|
+
this.getPreviouslyCalculatedLayout = getPreviouslyCalculatedLayout
|
|
20
41
|
}
|
|
21
42
|
|
|
22
43
|
getInitialLayoutValues({
|
|
23
|
-
bypass,
|
|
24
44
|
itemsCount,
|
|
25
|
-
|
|
45
|
+
columnsCount
|
|
26
46
|
}) {
|
|
27
|
-
// On server side, at initialization time, there's no "visible area height",
|
|
28
|
-
// so default to `1` estimated rows count.
|
|
29
|
-
const estimatedRowsCount = visibleAreaHeightIncludingMargins
|
|
30
|
-
? this.getEstimatedRowsCountForHeight(visibleAreaHeightIncludingMargins)
|
|
31
|
-
: 1
|
|
32
47
|
let firstShownItemIndex
|
|
33
48
|
let lastShownItemIndex
|
|
34
49
|
// If there're no items then `firstShownItemIndex` stays `undefined`.
|
|
35
50
|
if (itemsCount > 0) {
|
|
36
51
|
firstShownItemIndex = 0
|
|
37
|
-
lastShownItemIndex = this.
|
|
38
|
-
firstShownItemIndex,
|
|
52
|
+
lastShownItemIndex = this.getInitialLastShownItemIndex({
|
|
39
53
|
itemsCount,
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
)
|
|
54
|
+
columnsCount,
|
|
55
|
+
firstShownItemIndex
|
|
56
|
+
})
|
|
43
57
|
}
|
|
44
58
|
return {
|
|
45
59
|
beforeItemsHeight: 0,
|
|
@@ -49,25 +63,32 @@ export default class Layout {
|
|
|
49
63
|
}
|
|
50
64
|
}
|
|
51
65
|
|
|
52
|
-
|
|
53
|
-
firstShownItemIndex,
|
|
66
|
+
getInitialLastShownItemIndex({
|
|
54
67
|
itemsCount,
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
) {
|
|
58
|
-
if (this.bypass
|
|
68
|
+
columnsCount,
|
|
69
|
+
firstShownItemIndex
|
|
70
|
+
}) {
|
|
71
|
+
if (this.bypass) {
|
|
59
72
|
return itemsCount - 1
|
|
60
73
|
}
|
|
74
|
+
// On server side, at initialization time,
|
|
75
|
+
// `scrollableContainer` is `undefined`,
|
|
76
|
+
// so default to `1` estimated rows count.
|
|
77
|
+
let estimatedRowsCount = 1
|
|
78
|
+
if (this.getMaxVisibleAreaHeight()) {
|
|
79
|
+
estimatedRowsCount = this.getEstimatedRowsCountForHeight(this.getMaxVisibleAreaHeight() + this.getPrerenderMargin())
|
|
80
|
+
}
|
|
61
81
|
return Math.min(
|
|
62
|
-
firstShownItemIndex + (estimatedRowsCount *
|
|
82
|
+
firstShownItemIndex + (estimatedRowsCount * columnsCount - 1),
|
|
63
83
|
itemsCount - 1
|
|
64
84
|
)
|
|
65
85
|
}
|
|
66
86
|
|
|
67
87
|
getEstimatedRowsCountForHeight(height) {
|
|
68
88
|
const estimatedItemHeight = this.getEstimatedItemHeight()
|
|
89
|
+
const verticalSpacing = this.getVerticalSpacing()
|
|
69
90
|
if (estimatedItemHeight) {
|
|
70
|
-
return Math.ceil((height +
|
|
91
|
+
return Math.ceil((height + verticalSpacing) / (estimatedItemHeight + verticalSpacing))
|
|
71
92
|
} else {
|
|
72
93
|
// If no items have been rendered yet, and no `estimatedItemHeight` option
|
|
73
94
|
// has been passed, then default to `1` estimated rows count in any `height`.
|
|
@@ -84,315 +105,666 @@ export default class Layout {
|
|
|
84
105
|
return this.getAverageItemHeight() || this.estimatedItemHeight || 0
|
|
85
106
|
}
|
|
86
107
|
|
|
87
|
-
|
|
108
|
+
getLayoutUpdateForItemsDiff({
|
|
109
|
+
firstShownItemIndex,
|
|
110
|
+
lastShownItemIndex,
|
|
111
|
+
beforeItemsHeight,
|
|
112
|
+
afterItemsHeight
|
|
113
|
+
}, {
|
|
88
114
|
prependedItemsCount,
|
|
89
115
|
appendedItemsCount
|
|
90
116
|
}, {
|
|
91
|
-
itemsCount
|
|
117
|
+
itemsCount,
|
|
118
|
+
columnsCount,
|
|
119
|
+
shouldRestoreScrollPosition
|
|
92
120
|
}) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
121
|
+
// const layoutUpdate = {}
|
|
122
|
+
|
|
123
|
+
// If the layout stays the same, then simply increase
|
|
124
|
+
// the top and bottom margins proportionally to the amount
|
|
125
|
+
// of the items added.
|
|
126
|
+
const averageItemHeight = this.getAverageItemHeight()
|
|
127
|
+
const verticalSpacing = this.getVerticalSpacing()
|
|
128
|
+
|
|
129
|
+
if (appendedItemsCount > 0) {
|
|
101
130
|
const appendedRowsCount = Math.ceil(appendedItemsCount / columnsCount)
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
131
|
+
const addedHeightAfter = appendedRowsCount * (verticalSpacing + averageItemHeight)
|
|
132
|
+
|
|
133
|
+
afterItemsHeight += addedHeightAfter
|
|
134
|
+
|
|
135
|
+
// layoutUpdate = {
|
|
136
|
+
// ...layoutUpdate,
|
|
137
|
+
// afterItemsHeight
|
|
138
|
+
// }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (prependedItemsCount > 0) {
|
|
142
|
+
const prependedRowsCount = Math.ceil(prependedItemsCount / columnsCount)
|
|
143
|
+
const addedHeightBefore = prependedRowsCount * (averageItemHeight + verticalSpacing)
|
|
144
|
+
|
|
145
|
+
firstShownItemIndex += prependedItemsCount
|
|
146
|
+
lastShownItemIndex += prependedItemsCount
|
|
147
|
+
beforeItemsHeight += addedHeightBefore
|
|
148
|
+
|
|
149
|
+
// If the currently shown items position on screen should be preserved
|
|
150
|
+
// when prepending new items, then it means that:
|
|
151
|
+
// * The current scroll position should be snapshotted.
|
|
152
|
+
// * The current list height should be snapshotted.
|
|
153
|
+
// * All prepended items should be shown so that their height could be
|
|
154
|
+
// measured after they're rendered. Based on the prepended items' height,
|
|
155
|
+
// the scroll position will be restored so that there's no "jump of content".
|
|
156
|
+
if (shouldRestoreScrollPosition) {
|
|
157
|
+
firstShownItemIndex = 0
|
|
158
|
+
beforeItemsHeight = 0
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (prependedItemsCount % columnsCount > 0) {
|
|
162
|
+
// Rows will be rebalanced as a result of prepending new items,
|
|
163
|
+
// and row heights can change as a result, so re-layout items
|
|
164
|
+
// after they've been measured (after the upcoming re-render).
|
|
165
|
+
//
|
|
166
|
+
// For example, consider a web page where item rows are `display: flex`.
|
|
167
|
+
// Suppose there're 3 columns and it shows items from 4 to 6.
|
|
168
|
+
//
|
|
169
|
+
// ------------------------------------------
|
|
170
|
+
// | Apples are | Bananas | Cranberries |
|
|
171
|
+
// | green | | |
|
|
172
|
+
// ------------------------------------------
|
|
173
|
+
// | Dates | Elderberry | Figs are |
|
|
174
|
+
// | | | tasty |
|
|
175
|
+
// ------------------------------------------
|
|
176
|
+
//
|
|
177
|
+
// Now, 1 item gets prepended. As a result, all existing rows will have
|
|
178
|
+
// a different set of items, which means that the row heights will change.
|
|
179
|
+
//
|
|
180
|
+
// ------------------------------------------
|
|
181
|
+
// | Zucchini | Apples are | Bananas |
|
|
182
|
+
// | | green | |
|
|
183
|
+
// ------------------------------------------
|
|
184
|
+
// | Cranberries | Dates | Elderberry |
|
|
185
|
+
// ------------------------------------------
|
|
186
|
+
// | Figs |
|
|
187
|
+
// | are tasty |
|
|
188
|
+
// ---------------
|
|
189
|
+
//
|
|
190
|
+
// As it can be seen above, the second row's height has changed from 2 to 1.
|
|
191
|
+
// Not only that, but `itemHeights` have changed as well, so if you thought
|
|
192
|
+
// that the library could easily recalculate row heights using `Math.max()` —
|
|
193
|
+
// turns out it's not always the case.
|
|
194
|
+
//
|
|
195
|
+
// There could be an explicit opt-in option for automatically recalculating
|
|
196
|
+
// row heights, but I don't want to write code for such an extremely rare
|
|
197
|
+
// use case. Instead, use the `getColumnsCount()` parameter function when
|
|
198
|
+
// fetching previous items.
|
|
199
|
+
|
|
200
|
+
warn('~ Prepended items count', prependedItemsCount, 'is not divisible by Columns Count', columnsCount, '~')
|
|
201
|
+
warn('Reset Layout')
|
|
202
|
+
|
|
203
|
+
const shownItemsCountBeforeItemsUpdate = lastShownItemIndex - firstShownItemIndex + 1
|
|
204
|
+
|
|
205
|
+
firstShownItemIndex = 0
|
|
206
|
+
beforeItemsHeight = 0
|
|
207
|
+
|
|
208
|
+
if (!shouldRestoreScrollPosition) {
|
|
209
|
+
// Limit shown items count if too many items have been prepended.
|
|
210
|
+
if (prependedItemsCount > shownItemsCountBeforeItemsUpdate) {
|
|
211
|
+
lastShownItemIndex = this.getInitialLastShownItemIndex({
|
|
212
|
+
itemsCount,
|
|
213
|
+
columnsCount,
|
|
214
|
+
firstShownItemIndex
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
// Approximate `afterItemsHeight` calculation.
|
|
218
|
+
const afterItemsCount = itemsCount - (lastShownItemIndex + 1)
|
|
219
|
+
afterItemsHeight = Math.ceil(afterItemsCount / columnsCount) * (verticalSpacing + averageItemHeight)
|
|
220
|
+
|
|
221
|
+
// layoutUpdate = {
|
|
222
|
+
// ...layoutUpdate,
|
|
223
|
+
// afterItemsHeight
|
|
224
|
+
// }
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// layoutUpdate = {
|
|
230
|
+
// ...layoutUpdate,
|
|
231
|
+
// beforeItemsHeight,
|
|
232
|
+
// firstShownItemIndex,
|
|
233
|
+
// lastShownItemIndex
|
|
234
|
+
// }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// return layoutUpdate
|
|
238
|
+
|
|
239
|
+
// Overwrite all four props in all scenarios.
|
|
240
|
+
// The reason is that only this way subsequent `setItems()` calls
|
|
241
|
+
// will be truly "stateless" when a chain of `setItems()` calls
|
|
242
|
+
// could be replaced with just the last one in a scenario when
|
|
243
|
+
// `setState()` calls are "asynchronous" (delayed execution).
|
|
244
|
+
//
|
|
245
|
+
// So, for example, the user calls `setItems()` with one set of items.
|
|
246
|
+
// A `setState()` call has been dispatched but the `state` hasn't been updated yet.
|
|
247
|
+
// Then the user calls `setItems()` with another set of items.
|
|
248
|
+
// If this function only returned a minimal set of properties that actually change,
|
|
249
|
+
// the other layout properties of the second `setItems()` call wouldn't overwrite the ones
|
|
250
|
+
// scheduled for update during the first `setItems()` call, resulting in an inconsistent `state`.
|
|
251
|
+
//
|
|
252
|
+
// For example, the first `setItems()` call does a `setState()` call where it updates
|
|
253
|
+
// `afterItemsHeight`, and then the second `setItems()` call only updates `beforeItemsHeight`
|
|
254
|
+
// and `firstShownItemIndex` and `lastShownItemIndex`. If the second `setItems()` call was to
|
|
255
|
+
// overwrite any effects of the pending-but-not-yet-applied first `setItems()` call, it would
|
|
256
|
+
// have to call `setState()` with an `afterItemsHeight` property too, even though it hasn't change.
|
|
257
|
+
// That would be just to revert the change to `afterItemsHeight` state property already scheduled
|
|
258
|
+
// by the first `setItems()` call.
|
|
259
|
+
//
|
|
260
|
+
return {
|
|
261
|
+
beforeItemsHeight,
|
|
262
|
+
afterItemsHeight,
|
|
263
|
+
firstShownItemIndex,
|
|
264
|
+
lastShownItemIndex
|
|
121
265
|
}
|
|
122
266
|
}
|
|
123
267
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
268
|
+
// If an item that hasn't been shown (and measured) yet is encountered
|
|
269
|
+
// then show such item and then retry after it has been measured.
|
|
270
|
+
getItemNotMeasuredIndexes(i, {
|
|
271
|
+
itemsCount,
|
|
272
|
+
firstShownItemIndex,
|
|
273
|
+
nonMeasuredAreaHeight,
|
|
274
|
+
indexOfTheFirstItemInTheRow
|
|
275
|
+
}) {
|
|
276
|
+
log('Item index', i, 'height is required for calculations but hasn\'t been measured yet. Mark the item as "shown", rerender the list, measure the item\'s height and redo the layout.')
|
|
277
|
+
|
|
130
278
|
const columnsCount = this.getColumnsCount()
|
|
279
|
+
|
|
280
|
+
const itemsCountToRenderForMeasurement = Math.min(
|
|
281
|
+
this.getEstimatedRowsCountForHeight(nonMeasuredAreaHeight) * columnsCount,
|
|
282
|
+
this.measureItemsBatchSize || Infinity,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
if (firstShownItemIndex === undefined) {
|
|
286
|
+
firstShownItemIndex = indexOfTheFirstItemInTheRow
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const lastShownItemIndex = Math.min(
|
|
290
|
+
indexOfTheFirstItemInTheRow + itemsCountToRenderForMeasurement - 1,
|
|
291
|
+
// Guard against index overflow.
|
|
292
|
+
itemsCount - 1
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
firstNonMeasuredItemIndex: i,
|
|
297
|
+
firstShownItemIndex,
|
|
298
|
+
lastShownItemIndex
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Finds the indexes of the currently visible items.
|
|
304
|
+
* @return {object} `{ firstShownItemIndex: number, lastShownItemIndex: number, firstNonMeasuredItemIndex: number? }`
|
|
305
|
+
*/
|
|
306
|
+
getShownItemIndexes({
|
|
307
|
+
itemsCount,
|
|
308
|
+
visibleAreaTop,
|
|
309
|
+
visibleAreaBottom
|
|
310
|
+
}) {
|
|
311
|
+
let indexes = this._getShownItemIndex({
|
|
312
|
+
itemsCount,
|
|
313
|
+
fromIndex: 0,
|
|
314
|
+
visibleAreaTop,
|
|
315
|
+
visibleAreaBottom,
|
|
316
|
+
findFirstShownItemIndex: true
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
if (indexes === null) {
|
|
320
|
+
return this.getNonVisibleListShownItemIndexes()
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (indexes.firstNonMeasuredItemIndex !== undefined) {
|
|
324
|
+
return indexes
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const { firstShownItemIndex, beforeItemsHeight } = indexes
|
|
328
|
+
|
|
329
|
+
indexes = this._getShownItemIndex({
|
|
330
|
+
itemsCount,
|
|
331
|
+
fromIndex: firstShownItemIndex,
|
|
332
|
+
beforeItemsHeight,
|
|
333
|
+
visibleAreaTop,
|
|
334
|
+
visibleAreaBottom,
|
|
335
|
+
findLastShownItemIndex: true
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
if (indexes === null) {
|
|
339
|
+
return this.getNonVisibleListShownItemIndexes()
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (indexes.firstNonMeasuredItemIndex !== undefined) {
|
|
343
|
+
return indexes
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const { lastShownItemIndex } = indexes
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
firstShownItemIndex,
|
|
350
|
+
lastShownItemIndex
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
_getShownItemIndex(parameters) {
|
|
355
|
+
const {
|
|
356
|
+
beforeResize,
|
|
357
|
+
itemsCount,
|
|
358
|
+
visibleAreaTop,
|
|
359
|
+
visibleAreaBottom,
|
|
360
|
+
findFirstShownItemIndex,
|
|
361
|
+
findLastShownItemIndex,
|
|
362
|
+
// backwards
|
|
363
|
+
} = parameters
|
|
364
|
+
|
|
365
|
+
let {
|
|
366
|
+
fromIndex,
|
|
367
|
+
beforeItemsHeight
|
|
368
|
+
} = parameters
|
|
369
|
+
|
|
370
|
+
// This function could potentially also use `this.getPreviouslyCalculatedLayout()`
|
|
371
|
+
// in order to skip calculating visible item indexes from scratch
|
|
372
|
+
// and instead just calculate the difference from a "previously calculated layout".
|
|
373
|
+
//
|
|
374
|
+
// I did a simple test in a web browser and found out that running the following
|
|
375
|
+
// piece of code is less than 10 milliseconds:
|
|
376
|
+
//
|
|
377
|
+
// var startedAt = Date.now()
|
|
378
|
+
// var i = 0
|
|
379
|
+
// while (i < 1000000) {
|
|
380
|
+
// i++
|
|
381
|
+
// }
|
|
382
|
+
// console.log(Date.now() - startedAt)
|
|
383
|
+
//
|
|
384
|
+
// Which becomes negligible in my project's use case (a couple thousands items max).
|
|
385
|
+
//
|
|
386
|
+
// If someone would attempt to use a "previously calculated layout" here
|
|
387
|
+
// then `shownItemsHeight` would also have to be returned from this function:
|
|
388
|
+
// the total height of all shown items including vertical spacing between them.
|
|
389
|
+
//
|
|
390
|
+
// If "previously calculated layout" would be used then it would first find
|
|
391
|
+
// `firstShownItemIndex` and then find `lastShownItemIndex` as part of two
|
|
392
|
+
// separate calls of this function, each with or without `backwards` flag,
|
|
393
|
+
// depending on whether `visibleAreaTop` and `visibleAreBottom` have shifted up or down.
|
|
394
|
+
|
|
131
395
|
let firstShownItemIndex
|
|
132
396
|
let lastShownItemIndex
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
397
|
+
|
|
398
|
+
// It's not always required to pass `beforeItemsHeight` parameter:
|
|
399
|
+
// when `fromIndex` is `0`, it's also assumed to be `0`.
|
|
400
|
+
if (fromIndex === 0) {
|
|
401
|
+
beforeItemsHeight = 0
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (beforeItemsHeight === undefined) {
|
|
405
|
+
throw new Error('[virtual-scroller] `beforeItemsHeight` not passed to `Layout.getShownItemIndexes()` when starting from index ' + fromIndex);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// const backwards = false
|
|
409
|
+
// while (backwards ? i >= 0 : i < itemsCount) {}
|
|
410
|
+
|
|
411
|
+
if (!beforeResize) {
|
|
412
|
+
const beforeResizeItemsCount = this.getBeforeResizeItemsCount()
|
|
413
|
+
if (beforeResizeItemsCount > fromIndex) {
|
|
414
|
+
// First search for the item in "before resize" items.
|
|
415
|
+
const {
|
|
416
|
+
notFound,
|
|
417
|
+
beforeItemsHeight: beforeResizeItemsHeight,
|
|
418
|
+
firstShownItemIndex,
|
|
419
|
+
lastShownItemIndex
|
|
420
|
+
} = this._getShownItemIndex({
|
|
421
|
+
...parameters,
|
|
422
|
+
beforeResize: true,
|
|
423
|
+
itemsCount: beforeResizeItemsCount
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
// If the item was not found in "before resize" items
|
|
427
|
+
// then search in regular items skipping "before resize" ones.
|
|
428
|
+
if (notFound) {
|
|
429
|
+
beforeItemsHeight = beforeResizeItemsHeight
|
|
430
|
+
fromIndex += beforeResizeItemsCount
|
|
431
|
+
} else {
|
|
432
|
+
// If the item was found in "before resize" items
|
|
433
|
+
// then return the result.
|
|
434
|
+
// Rebalance first / last shown item indexes based on
|
|
435
|
+
// the current columns count, if required.
|
|
436
|
+
const columnsCount = this.getColumnsCount()
|
|
437
|
+
return {
|
|
438
|
+
firstShownItemIndex: firstShownItemIndex === undefined
|
|
439
|
+
? undefined
|
|
440
|
+
: Math.floor(firstShownItemIndex / columnsCount) * columnsCount,
|
|
441
|
+
lastShownItemIndex: lastShownItemIndex === undefined
|
|
442
|
+
? undefined
|
|
443
|
+
: Math.floor(lastShownItemIndex / columnsCount) * columnsCount,
|
|
444
|
+
beforeItemsHeight: beforeResizeItemsHeight
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const columnsCount = beforeResize ? this.getColumnsCountBeforeResize() : this.getColumnsCount()
|
|
451
|
+
const verticalSpacing = beforeResize ? this.getVerticalSpacingBeforeResize() : this.getVerticalSpacing()
|
|
452
|
+
|
|
453
|
+
let i = fromIndex
|
|
454
|
+
while (i < itemsCount) {
|
|
455
|
+
const currentRowFirstItemIndex = i
|
|
456
|
+
|
|
457
|
+
const hasMoreRows = itemsCount > currentRowFirstItemIndex + columnsCount
|
|
458
|
+
const verticalSpacingAfterCurrentRow = hasMoreRows ? verticalSpacing : 0
|
|
459
|
+
|
|
139
460
|
let currentRowHeight = 0
|
|
461
|
+
|
|
462
|
+
// Calculate current row height.
|
|
140
463
|
let columnIndex = 0
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
//
|
|
146
|
-
//
|
|
464
|
+
while (columnIndex < columnsCount && i < itemsCount) {
|
|
465
|
+
const itemHeight = beforeResize ? this.getItemHeightBeforeResize(i) : this.getItemHeight(i)
|
|
466
|
+
|
|
467
|
+
// If this item hasn't been measured yet (or re-measured after a resize)
|
|
468
|
+
// then mark it as the first non-measured one.
|
|
469
|
+
//
|
|
470
|
+
// Can't happen by definition when `beforeResize` parameter is `true`.
|
|
471
|
+
//
|
|
147
472
|
if (itemHeight === undefined) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
firstShownItemIndex
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
(rowIndex + this.getEstimatedRowsCountForHeight(heightLeft)) * columnsCount - 1,
|
|
155
|
-
// Guard against index overflow.
|
|
156
|
-
itemsCount - 1
|
|
157
|
-
)
|
|
158
|
-
return {
|
|
159
|
-
firstNonMeasuredItemIndex: i,
|
|
160
|
-
firstShownItemIndex,
|
|
161
|
-
lastShownItemIndex
|
|
162
|
-
}
|
|
473
|
+
return this.getItemNotMeasuredIndexes(i, {
|
|
474
|
+
itemsCount,
|
|
475
|
+
firstShownItemIndex: findLastShownItemIndex ? fromIndex : undefined,
|
|
476
|
+
indexOfTheFirstItemInTheRow: currentRowFirstItemIndex,
|
|
477
|
+
nonMeasuredAreaHeight: (visibleAreaBottom + this.getPrerenderMargin()) - beforeItemsHeight
|
|
478
|
+
})
|
|
163
479
|
}
|
|
480
|
+
|
|
164
481
|
currentRowHeight = Math.max(currentRowHeight, itemHeight)
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
482
|
+
|
|
483
|
+
columnIndex++
|
|
484
|
+
i++
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const itemsHeightFromFirstRowToThisRow = beforeItemsHeight + currentRowHeight
|
|
488
|
+
|
|
489
|
+
const rowStepsIntoVisibleAreaTop = itemsHeightFromFirstRowToThisRow > visibleAreaTop - this.getPrerenderMargin()
|
|
490
|
+
const rowStepsOutOfVisibleAreaBottomOrIsAtTheBorder = itemsHeightFromFirstRowToThisRow + verticalSpacingAfterCurrentRow >= visibleAreaBottom + this.getPrerenderMargin()
|
|
491
|
+
|
|
492
|
+
// if (backwards) {
|
|
493
|
+
// if (findFirstShownItemIndex) {
|
|
494
|
+
// if (rowStepsOutOfVisibleAreaTop) {
|
|
495
|
+
// return {
|
|
496
|
+
// firstShownItemIndex: currentRowFirstItemIndex + columnsCount
|
|
497
|
+
// }
|
|
498
|
+
// }
|
|
499
|
+
// } else if (findLastShownItemIndex) {
|
|
500
|
+
// if (rowStepsIntoVisibleAreaBottom) {
|
|
501
|
+
// return {
|
|
502
|
+
// lastShownItemIndex: currentRowFirstItemIndex + columnsCount - 1
|
|
503
|
+
// }
|
|
504
|
+
// }
|
|
505
|
+
// }
|
|
506
|
+
// }
|
|
507
|
+
|
|
508
|
+
if (findFirstShownItemIndex) {
|
|
509
|
+
if (rowStepsIntoVisibleAreaTop) {
|
|
510
|
+
// If item is the first one visible in the viewport
|
|
511
|
+
// then start showing items from this row.
|
|
512
|
+
return {
|
|
513
|
+
firstShownItemIndex: currentRowFirstItemIndex,
|
|
514
|
+
beforeItemsHeight
|
|
171
515
|
}
|
|
172
516
|
}
|
|
173
|
-
|
|
174
|
-
if (
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
// so it's possible that even when the list DOM element happens
|
|
178
|
-
// to be in the viewport in reality the list isn't visible
|
|
179
|
-
// in which case `firstShownItemIndex` will be `undefined`.
|
|
180
|
-
if (firstShownItemIndex !== undefined) {
|
|
181
|
-
lastShownItemIndex = Math.min(
|
|
517
|
+
} else if (findLastShownItemIndex) {
|
|
518
|
+
if (rowStepsOutOfVisibleAreaBottomOrIsAtTheBorder) {
|
|
519
|
+
return {
|
|
520
|
+
lastShownItemIndex: Math.min(
|
|
182
521
|
// The index of the last item in the current row.
|
|
183
|
-
|
|
522
|
+
currentRowFirstItemIndex + columnsCount - 1,
|
|
184
523
|
// Guards against index overflow.
|
|
185
524
|
itemsCount - 1
|
|
186
525
|
)
|
|
187
526
|
}
|
|
188
|
-
return {
|
|
189
|
-
firstShownItemIndex,
|
|
190
|
-
lastShownItemIndex
|
|
191
|
-
}
|
|
192
527
|
}
|
|
193
|
-
columnIndex++
|
|
194
528
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
firstShownItemIndex,
|
|
207
|
-
lastShownItemIndex
|
|
529
|
+
|
|
530
|
+
beforeItemsHeight += currentRowHeight + verticalSpacingAfterCurrentRow
|
|
531
|
+
|
|
532
|
+
// if (backwards) {
|
|
533
|
+
// // Set `i` to be the first item of the current row.
|
|
534
|
+
// i -= columnsCount
|
|
535
|
+
// const prevoiusRowIsBeforeResize = i - 1 < this.getBeforeResizeItemsCount()
|
|
536
|
+
// const previousRowColumnsCount = prevoiusRowIsBeforeResize ? this.getColumnsCountBeforeResize() : this.getColumnsCount()
|
|
537
|
+
// // Set `i` to be the first item of the previous row.
|
|
538
|
+
// i -= previousRowColumnsCount
|
|
539
|
+
// }
|
|
208
540
|
}
|
|
209
|
-
}
|
|
210
541
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
)
|
|
228
|
-
const redoLayoutAfterMeasuringItemHeights = firstNonMeasuredItemIndex !== undefined
|
|
229
|
-
// If some items will be rendered in order to measure their height,
|
|
230
|
-
// and it's not a `preserveScrollPositionOnPrependItems` case,
|
|
231
|
-
// then limit the amount of such items being measured in a single pass.
|
|
232
|
-
if (redoLayoutAfterMeasuringItemHeights && this.measureItemsBatchSize) {
|
|
233
|
-
const maxAllowedLastShownItemIndex = firstNonMeasuredItemIndex + this.measureItemsBatchSize - 1
|
|
234
|
-
const columnsCount = this.getColumnsCount()
|
|
235
|
-
lastShownItemIndex = Math.min(
|
|
236
|
-
// Also guards against index overflow.
|
|
237
|
-
lastShownItemIndex,
|
|
238
|
-
// The index of the last item in the row.
|
|
239
|
-
Math.ceil(maxAllowedLastShownItemIndex / columnsCount) * columnsCount - 1
|
|
240
|
-
)
|
|
542
|
+
// if (backwards) {
|
|
543
|
+
// if (findFirstShownItemIndex) {
|
|
544
|
+
// warn('The list is supposed to be visible but no visible item has been found (while traversing backwards)')
|
|
545
|
+
// return null
|
|
546
|
+
// } else if (findLastShownItemIndex) {
|
|
547
|
+
// return {
|
|
548
|
+
// firstShownItemIndex: 0
|
|
549
|
+
// }
|
|
550
|
+
// }
|
|
551
|
+
// }
|
|
552
|
+
|
|
553
|
+
if (beforeResize) {
|
|
554
|
+
return {
|
|
555
|
+
notFound: true,
|
|
556
|
+
beforeItemsHeight
|
|
557
|
+
}
|
|
241
558
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
559
|
+
|
|
560
|
+
// This case isn't supposed to happen but it could hypothetically happen
|
|
561
|
+
// because the list height is measured from the user's screen and
|
|
562
|
+
// not necessarily can be trusted.
|
|
563
|
+
if (findFirstShownItemIndex) {
|
|
564
|
+
warn('The list is supposed to be visible but no visible item has been found')
|
|
565
|
+
return null
|
|
566
|
+
} else if (findLastShownItemIndex) {
|
|
567
|
+
return {
|
|
568
|
+
lastShownItemIndex: itemsCount - 1
|
|
569
|
+
}
|
|
246
570
|
}
|
|
247
571
|
}
|
|
248
572
|
|
|
249
573
|
getNonVisibleListShownItemIndexes() {
|
|
250
|
-
|
|
574
|
+
const layout = {
|
|
251
575
|
firstShownItemIndex: 0,
|
|
252
|
-
lastShownItemIndex: 0
|
|
253
|
-
redoLayoutAfterMeasuringItemHeights: this.getItemHeight(0) === undefined
|
|
576
|
+
lastShownItemIndex: 0
|
|
254
577
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
getItemIndexes(
|
|
258
|
-
visibleAreaTop,
|
|
259
|
-
visibleAreaBottom,
|
|
260
|
-
listTopOffset,
|
|
261
|
-
listHeight,
|
|
262
|
-
itemsCount
|
|
263
|
-
) {
|
|
264
|
-
const isVisible = listTopOffset + listHeight > visibleAreaTop && listTopOffset < visibleAreaBottom
|
|
265
|
-
if (!isVisible) {
|
|
266
|
-
log('The entire list is off-screen. No items are visible.')
|
|
267
|
-
return
|
|
268
|
-
}
|
|
269
|
-
// Find the items which are displayed in the viewport.
|
|
270
|
-
const indexes = this.getVisibleItemIndexes(
|
|
271
|
-
visibleAreaTop,
|
|
272
|
-
visibleAreaBottom,
|
|
273
|
-
listTopOffset,
|
|
274
|
-
itemsCount
|
|
275
|
-
)
|
|
276
|
-
// The list height is estimated until all items have been seen,
|
|
277
|
-
// so it's possible that even when the list DOM element happens
|
|
278
|
-
// to be in the viewport, in reality the list isn't visible
|
|
279
|
-
// in which case `firstShownItemIndex` will be `undefined`.
|
|
280
|
-
if (indexes.firstShownItemIndex === undefined) {
|
|
281
|
-
log('The entire list is off-screen. No items are visible.')
|
|
282
|
-
return
|
|
578
|
+
if (this.getItemHeight(0) === undefined) {
|
|
579
|
+
layout.firstNonMeasuredItemIndex = 0
|
|
283
580
|
}
|
|
284
|
-
return
|
|
581
|
+
return layout
|
|
285
582
|
}
|
|
286
583
|
|
|
287
584
|
/**
|
|
288
585
|
* Measures "before" items height.
|
|
289
|
-
* @param {number}
|
|
290
|
-
* @param {number} lastShownItemIndex — New last shown item index.
|
|
586
|
+
* @param {number} beforeItemsCount — Basically, first shown item index.
|
|
291
587
|
* @return {number}
|
|
292
588
|
*/
|
|
293
589
|
getBeforeItemsHeight(
|
|
294
|
-
|
|
295
|
-
|
|
590
|
+
beforeItemsCount,
|
|
591
|
+
{ beforeResize } = {}
|
|
296
592
|
) {
|
|
297
|
-
|
|
298
|
-
|
|
593
|
+
// This function could potentially also use `this.getPreviouslyCalculatedLayout()`
|
|
594
|
+
// in order to skip calculating visible item indexes from scratch
|
|
595
|
+
// and instead just calculate the difference from a "previously calculated layout".
|
|
596
|
+
//
|
|
597
|
+
// I did a simple test in a web browser and found out that running the following
|
|
598
|
+
// piece of code is less than 10 milliseconds:
|
|
599
|
+
//
|
|
600
|
+
// var startedAt = Date.now()
|
|
601
|
+
// var i = 0
|
|
602
|
+
// while (i < 1000000) {
|
|
603
|
+
// i++
|
|
604
|
+
// }
|
|
605
|
+
// console.log(Date.now() - startedAt)
|
|
606
|
+
//
|
|
607
|
+
// Which becomes negligible in my project's use case (a couple thousands items max).
|
|
608
|
+
|
|
299
609
|
let beforeItemsHeight = 0
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
610
|
+
let i = 0
|
|
611
|
+
|
|
612
|
+
if (!beforeResize) {
|
|
613
|
+
const beforeResizeItemsCount = this.getBeforeResizeItemsCount()
|
|
614
|
+
|
|
615
|
+
if (beforeResizeItemsCount > 0) {
|
|
616
|
+
// First add all "before resize" item heights.
|
|
617
|
+
beforeItemsHeight = this.getBeforeItemsHeight(
|
|
618
|
+
// `firstShownItemIndex` (called `beforeItemsCount`) could be greater than
|
|
619
|
+
// `beforeResizeItemsCount` when the user scrolls down.
|
|
620
|
+
// `firstShownItemIndex` (called `beforeItemsCount`) could be less than
|
|
621
|
+
// `beforeResizeItemsCount` when the user scrolls up.
|
|
622
|
+
Math.min(beforeItemsCount, beforeResizeItemsCount),
|
|
623
|
+
{ beforeResize: true }
|
|
624
|
+
)
|
|
625
|
+
i = beforeResizeItemsCount
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const columnsCount = beforeResize ? this.getColumnsCountBeforeResize() : this.getColumnsCount()
|
|
630
|
+
const verticalSpacing = beforeResize ? this.getVerticalSpacingBeforeResize() : this.getVerticalSpacing()
|
|
631
|
+
|
|
632
|
+
while (i < beforeItemsCount) {
|
|
633
|
+
const currentRowFirstItemIndex = i
|
|
634
|
+
|
|
303
635
|
let rowHeight = 0
|
|
304
636
|
let columnIndex = 0
|
|
637
|
+
// Not checking for `itemsCount` overflow here because `i = beforeItemsCount`
|
|
638
|
+
// can only start at the start of a row, meaning that when calculating
|
|
639
|
+
// "before items height" it's not supposed to add item heights from the
|
|
640
|
+
// last row of items because in that case it would have to iterate from
|
|
641
|
+
// `i === beforeItemsCount` and that condition is already checked above.
|
|
642
|
+
// while (i < itemsCount) {
|
|
305
643
|
while (columnIndex < columnsCount) {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
644
|
+
let itemHeight = beforeResize ? this.getItemHeightBeforeResize(i) : this.getItemHeight(i)
|
|
645
|
+
if (itemHeight === undefined) {
|
|
646
|
+
// `itemHeight` can only be `undefined` when not `beforeResize`.
|
|
647
|
+
// Use the current "average item height" as a substitute.
|
|
648
|
+
itemHeight = this.getAverageItemHeight()
|
|
649
|
+
}
|
|
650
|
+
rowHeight = Math.max(rowHeight, itemHeight)
|
|
651
|
+
i++
|
|
311
652
|
columnIndex++
|
|
312
653
|
}
|
|
654
|
+
|
|
313
655
|
beforeItemsHeight += rowHeight
|
|
314
|
-
beforeItemsHeight +=
|
|
315
|
-
rowIndex++
|
|
656
|
+
beforeItemsHeight += verticalSpacing
|
|
316
657
|
}
|
|
658
|
+
|
|
317
659
|
return beforeItemsHeight
|
|
318
660
|
}
|
|
319
661
|
|
|
320
662
|
/**
|
|
321
663
|
* Measures "after" items height.
|
|
322
|
-
* @param {number}
|
|
323
|
-
* @param {number} lastShownItemIndex — New last shown item index.
|
|
324
|
-
* @param {number} averageItemHeight — Average item height.
|
|
325
|
-
* @param {number} verticalSpacing — Item vertical spacing.
|
|
664
|
+
* @param {number} lastShownItemIndex — Last shown item index.
|
|
326
665
|
* @param {number} itemsCount — Items count.
|
|
327
666
|
* @return {number}
|
|
328
667
|
*/
|
|
329
668
|
getAfterItemsHeight(
|
|
330
|
-
firstShownItemIndex,
|
|
331
669
|
lastShownItemIndex,
|
|
332
670
|
itemsCount
|
|
333
671
|
) {
|
|
672
|
+
// This function could potentially also use `this.getPreviouslyCalculatedLayout()`
|
|
673
|
+
// in order to skip calculating visible item indexes from scratch
|
|
674
|
+
// and instead just calculate the difference from a "previously calculated layout".
|
|
675
|
+
//
|
|
676
|
+
// I did a simple test in a web browser and found out that running the following
|
|
677
|
+
// piece of code is less than 10 milliseconds:
|
|
678
|
+
//
|
|
679
|
+
// var startedAt = Date.now()
|
|
680
|
+
// var i = 0
|
|
681
|
+
// while (i < 1000000) {
|
|
682
|
+
// i++
|
|
683
|
+
// }
|
|
684
|
+
// console.log(Date.now() - startedAt)
|
|
685
|
+
//
|
|
686
|
+
// Which becomes negligible in my project's use case (a couple thousands items max).
|
|
687
|
+
|
|
334
688
|
const columnsCount = this.getColumnsCount()
|
|
335
|
-
const rowsCount = Math.ceil(itemsCount / columnsCount)
|
|
336
689
|
const lastShownRowIndex = Math.floor(lastShownItemIndex / columnsCount)
|
|
690
|
+
|
|
337
691
|
let afterItemsHeight = 0
|
|
338
|
-
|
|
339
|
-
|
|
692
|
+
|
|
693
|
+
let i = lastShownItemIndex + 1
|
|
694
|
+
while (i < itemsCount) {
|
|
340
695
|
let rowHeight = 0
|
|
341
696
|
let columnIndex = 0
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
697
|
+
while (columnIndex < columnsCount && i < itemsCount) {
|
|
698
|
+
let itemHeight = this.getItemHeight(i)
|
|
699
|
+
if (itemHeight === undefined) {
|
|
700
|
+
itemHeight = this.getAverageItemHeight()
|
|
701
|
+
}
|
|
702
|
+
rowHeight = Math.max(rowHeight, itemHeight)
|
|
703
|
+
i++
|
|
349
704
|
columnIndex++
|
|
350
705
|
}
|
|
706
|
+
|
|
351
707
|
// Add all "after" items height.
|
|
352
708
|
afterItemsHeight += this.getVerticalSpacing()
|
|
353
709
|
afterItemsHeight += rowHeight
|
|
354
|
-
rowIndex++
|
|
355
710
|
}
|
|
711
|
+
|
|
356
712
|
return afterItemsHeight
|
|
357
713
|
}
|
|
358
714
|
|
|
359
715
|
/**
|
|
360
|
-
*
|
|
361
|
-
* @
|
|
716
|
+
* Returns the items's top offset relative to the top edge of the first item.
|
|
717
|
+
* @param {number} i — Item index
|
|
718
|
+
* @return {[number]} Returns `undefined` if any of the previous items haven't been rendered yet.
|
|
362
719
|
*/
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
720
|
+
getItemTopOffset(i) {
|
|
721
|
+
let topOffsetInsideScrollableContainer = 0
|
|
722
|
+
|
|
723
|
+
const beforeResizeItemsCount = this.getBeforeResizeItemsCount()
|
|
724
|
+
const beforeResizeRowsCount = beforeResizeItemsCount === 0
|
|
725
|
+
? 0
|
|
726
|
+
: Math.ceil(beforeResizeItemsCount / this.getColumnsCountBeforeResize())
|
|
727
|
+
|
|
728
|
+
const maxBeforeResizeRowsCount = i < beforeResizeItemsCount
|
|
729
|
+
? Math.floor(i / this.getColumnsCountBeforeResize())
|
|
730
|
+
: beforeResizeRowsCount
|
|
731
|
+
|
|
732
|
+
let beforeResizeRowIndex = 0
|
|
733
|
+
while (beforeResizeRowIndex < maxBeforeResizeRowsCount) {
|
|
734
|
+
const rowHeight = this.getItemHeightBeforeResize(
|
|
735
|
+
beforeResizeRowIndex * this.getColumnsCountBeforeResize()
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
topOffsetInsideScrollableContainer += rowHeight
|
|
739
|
+
topOffsetInsideScrollableContainer += this.getVerticalSpacingBeforeResize()
|
|
740
|
+
|
|
741
|
+
beforeResizeRowIndex++
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const itemRowIndex = Math.floor((i - beforeResizeItemsCount) / this.getColumnsCount())
|
|
745
|
+
|
|
746
|
+
let rowIndex = 0
|
|
747
|
+
while (rowIndex < itemRowIndex) {
|
|
748
|
+
let rowHeight = 0
|
|
749
|
+
let columnIndex = 0
|
|
750
|
+
while (columnIndex < this.getColumnsCount()) {
|
|
751
|
+
const itemHeight = this.getItemHeight(
|
|
752
|
+
beforeResizeItemsCount + rowIndex * this.getColumnsCount() + columnIndex
|
|
753
|
+
)
|
|
754
|
+
if (itemHeight === undefined) {
|
|
755
|
+
return
|
|
756
|
+
}
|
|
757
|
+
rowHeight = Math.max(rowHeight, itemHeight)
|
|
758
|
+
columnIndex++
|
|
373
759
|
}
|
|
760
|
+
|
|
761
|
+
topOffsetInsideScrollableContainer += rowHeight
|
|
762
|
+
topOffsetInsideScrollableContainer += this.getVerticalSpacing()
|
|
763
|
+
|
|
764
|
+
rowIndex++
|
|
374
765
|
}
|
|
375
|
-
// Finds the indexes of the items that are currently visible
|
|
376
|
-
// (or close to being visible) in the scrollable container.
|
|
377
|
-
// For scrollable containers other than the main screen, it could also
|
|
378
|
-
// check the visibility of such scrollable container itself, because it
|
|
379
|
-
// might be not visible.
|
|
380
|
-
// If such kind of an optimization would hypothetically be implemented,
|
|
381
|
-
// then it would also require listening for "scroll" events on the screen.
|
|
382
|
-
// Overall, I suppose that such "actual visibility" feature would be
|
|
383
|
-
// a very minor optimization and not something I'd deal with.
|
|
384
|
-
return this.getItemIndexes(
|
|
385
|
-
visibleAreaIncludingMargins.top,
|
|
386
|
-
visibleAreaIncludingMargins.bottom,
|
|
387
|
-
listTopOffsetInsideScrollableContainer,
|
|
388
|
-
listHeight,
|
|
389
|
-
itemsCount
|
|
390
|
-
) || this.getNonVisibleListShownItemIndexes()
|
|
391
|
-
}
|
|
392
766
|
|
|
393
|
-
|
|
394
|
-
layout.firstShownItemIndex = 0
|
|
395
|
-
layout.beforeItemsHeight = 0
|
|
767
|
+
return topOffsetInsideScrollableContainer
|
|
396
768
|
}
|
|
397
769
|
}
|
|
398
770
|
|
|
@@ -400,9 +772,11 @@ export const LAYOUT_REASON = {
|
|
|
400
772
|
SCROLL: 'scroll',
|
|
401
773
|
STOPPED_SCROLLING: 'stopped scrolling',
|
|
402
774
|
MANUAL: 'manual',
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
775
|
+
MOUNTED: 'mounted',
|
|
776
|
+
ACTUAL_ITEM_HEIGHTS_HAVE_BEEN_MEASURED: 'actual item heights have been measured',
|
|
777
|
+
VIEWPORT_WIDTH_CHANGED: 'viewport width changed',
|
|
778
|
+
VIEWPORT_HEIGHT_CHANGED: 'viewport height changed',
|
|
779
|
+
VIEWPORT_SIZE_UNCHANGED: 'viewport size unchanged',
|
|
406
780
|
ITEM_HEIGHT_CHANGED: 'item height changed',
|
|
407
781
|
ITEMS_CHANGED: 'items changed',
|
|
408
782
|
TOP_OFFSET_CHANGED: 'list top offset changed'
|