virtual-scroller 1.14.0 → 1.15.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 +23 -0
- package/README.md +309 -250
- package/bundle/index-dom-bypass.html +198 -0
- package/bundle/index-dom-grid.html +204 -0
- package/bundle/index-dom-scrollableContainer.html +215 -0
- package/bundle/index-dom-tbody-scrollableContainer.html +81 -0
- package/bundle/index-dom-tbody.html +65 -0
- package/bundle/index-dom.html +115 -84
- package/bundle/{index-bypass.html → index-react-bypass.html} +83 -78
- package/bundle/{index-grid.html → index-react-grid.html} +78 -91
- package/bundle/{index-scrollableContainer.html → index-react-scrollableContainer.html} +96 -91
- package/bundle/index-react-strictMode.html +199 -0
- package/bundle/index-react-tbody-scrollableContainer.html +96 -0
- package/bundle/index-react-tbody.html +80 -0
- package/bundle/{messages.js → items.js} +1 -1
- package/bundle/lib/babel.min.js +25 -0
- package/bundle/lib/prop-types.min.js +1 -0
- package/bundle/lib/react-dom.development.js +29924 -0
- package/bundle/lib/react-dom.production.min.js +267 -0
- package/bundle/lib/react.development.js +3343 -0
- package/bundle/lib/react.production.min.js +31 -0
- package/bundle/style.base.css +33 -0
- package/{website/style.css → bundle/style.list.css} +10 -43
- package/bundle/style.list.grid.css +23 -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 +1 -2
- package/commonjs/BeforeResize.js.map +1 -1
- package/commonjs/DOM/VirtualScroller.js +7 -13
- package/commonjs/DOM/VirtualScroller.js.map +1 -1
- package/commonjs/DOM/tbody.js +6 -6
- package/commonjs/DOM/tbody.js.map +1 -1
- package/commonjs/ItemHeights.js +10 -13
- package/commonjs/ItemHeights.js.map +1 -1
- package/commonjs/Layout.defaults.js +21 -0
- package/commonjs/Layout.defaults.js.map +1 -0
- package/commonjs/Layout.js +75 -64
- package/commonjs/Layout.js.map +1 -1
- package/commonjs/Layout.test.js +8 -4
- package/commonjs/Layout.test.js.map +1 -1
- package/commonjs/VirtualScroller.constructor.js +38 -4
- package/commonjs/VirtualScroller.constructor.js.map +1 -1
- package/commonjs/VirtualScroller.items.js +50 -4
- package/commonjs/VirtualScroller.items.js.map +1 -1
- package/commonjs/VirtualScroller.js +23 -14
- package/commonjs/VirtualScroller.js.map +1 -1
- package/commonjs/VirtualScroller.layout.js +40 -29
- package/commonjs/VirtualScroller.layout.js.map +1 -1
- package/commonjs/VirtualScroller.onContainerResize.js +1 -2
- package/commonjs/VirtualScroller.onContainerResize.js.map +1 -1
- package/commonjs/VirtualScroller.state.js +10 -9
- package/commonjs/VirtualScroller.state.js.map +1 -1
- package/commonjs/VirtualScroller.verticalSpacing.js +39 -6
- package/commonjs/VirtualScroller.verticalSpacing.js.map +1 -1
- package/commonjs/react/VirtualScroller.js +85 -34
- package/commonjs/react/VirtualScroller.js.map +1 -1
- package/commonjs/react/useClassName.js +2 -2
- package/commonjs/react/useClassName.js.map +1 -1
- package/commonjs/react/useForwardedRef.js +50 -0
- package/commonjs/react/useForwardedRef.js.map +1 -0
- package/commonjs/react/useInstanceMethods.js +4 -4
- package/commonjs/react/useInstanceMethods.js.map +1 -1
- package/commonjs/react/useItemKeys.js +28 -5
- package/commonjs/react/useItemKeys.js.map +1 -1
- package/commonjs/react/useOnItemHeightDidChange.js +28 -12
- package/commonjs/react/useOnItemHeightDidChange.js.map +1 -1
- package/commonjs/react/useSetItemState.js +31 -12
- package/commonjs/react/useSetItemState.js.map +1 -1
- package/commonjs/react/useState.js +9 -9
- package/commonjs/react/useState.js.map +1 -1
- package/commonjs/react/{useStateNoStaleBug.js → useStateWithRepeatableRead.js} +3 -3
- package/commonjs/react/useStateWithRepeatableRead.js.map +1 -0
- package/commonjs/react/useStyle.js +10 -4
- package/commonjs/react/useStyle.js.map +1 -1
- package/commonjs/react/useValidateTableBodyItemsContainer.js +24 -0
- package/commonjs/react/useValidateTableBodyItemsContainer.js.map +1 -0
- package/commonjs/react/useVirtualScroller.js +4 -3
- package/commonjs/react/useVirtualScroller.js.map +1 -1
- package/commonjs/test/ItemsContainer.js +10 -10
- package/commonjs/test/ItemsContainer.js.map +1 -1
- package/commonjs/test/VirtualScroller.js +25 -10
- package/commonjs/test/VirtualScroller.js.map +1 -1
- package/dom/index.d.ts +6 -5
- package/index.d.ts +19 -8
- package/modules/BeforeResize.js +1 -2
- package/modules/BeforeResize.js.map +1 -1
- package/modules/DOM/VirtualScroller.js +7 -13
- package/modules/DOM/VirtualScroller.js.map +1 -1
- package/modules/DOM/tbody.js +4 -4
- package/modules/DOM/tbody.js.map +1 -1
- package/modules/ItemHeights.js +11 -14
- package/modules/ItemHeights.js.map +1 -1
- package/modules/Layout.defaults.js +11 -0
- package/modules/Layout.defaults.js.map +1 -0
- package/modules/Layout.js +74 -64
- package/modules/Layout.js.map +1 -1
- package/modules/Layout.test.js +8 -4
- package/modules/Layout.test.js.map +1 -1
- package/modules/VirtualScroller.constructor.js +37 -4
- package/modules/VirtualScroller.constructor.js.map +1 -1
- package/modules/VirtualScroller.items.js +51 -5
- package/modules/VirtualScroller.items.js.map +1 -1
- package/modules/VirtualScroller.js +23 -14
- package/modules/VirtualScroller.js.map +1 -1
- package/modules/VirtualScroller.layout.js +40 -29
- package/modules/VirtualScroller.layout.js.map +1 -1
- package/modules/VirtualScroller.onContainerResize.js +1 -2
- package/modules/VirtualScroller.onContainerResize.js.map +1 -1
- package/modules/VirtualScroller.state.js +10 -9
- package/modules/VirtualScroller.state.js.map +1 -1
- package/modules/VirtualScroller.verticalSpacing.js +38 -6
- package/modules/VirtualScroller.verticalSpacing.js.map +1 -1
- package/modules/react/VirtualScroller.js +84 -35
- package/modules/react/VirtualScroller.js.map +1 -1
- package/modules/react/useClassName.js +3 -3
- package/modules/react/useClassName.js.map +1 -1
- package/modules/react/useForwardedRef.js +42 -0
- package/modules/react/useForwardedRef.js.map +1 -0
- package/modules/react/useInstanceMethods.js +4 -4
- package/modules/react/useInstanceMethods.js.map +1 -1
- package/modules/react/useItemKeys.js +28 -5
- package/modules/react/useItemKeys.js.map +1 -1
- package/modules/react/useOnItemHeightDidChange.js +28 -12
- package/modules/react/useOnItemHeightDidChange.js.map +1 -1
- package/modules/react/useSetItemState.js +31 -12
- package/modules/react/useSetItemState.js.map +1 -1
- package/modules/react/useState.js +9 -9
- package/modules/react/useState.js.map +1 -1
- package/modules/react/{useStateNoStaleBug.js → useStateWithRepeatableRead.js} +2 -2
- package/modules/react/useStateWithRepeatableRead.js.map +1 -0
- package/modules/react/useStyle.js +10 -4
- package/modules/react/useStyle.js.map +1 -1
- package/modules/react/useValidateTableBodyItemsContainer.js +16 -0
- package/modules/react/useValidateTableBodyItemsContainer.js.map +1 -0
- package/modules/react/useVirtualScroller.js +4 -3
- package/modules/react/useVirtualScroller.js.map +1 -1
- package/modules/test/ItemsContainer.js +10 -10
- package/modules/test/ItemsContainer.js.map +1 -1
- package/modules/test/VirtualScroller.js +25 -10
- package/modules/test/VirtualScroller.js.map +1 -1
- package/package.json +1 -1
- package/react/as.d.ts +42 -0
- package/react/index.d.ts +204 -63
- package/source/BeforeResize.js +1 -2
- package/source/DOM/VirtualScroller.js +7 -13
- package/source/DOM/tbody.js +5 -5
- package/source/ItemHeights.js +11 -12
- package/source/Layout.defaults.js +8 -0
- package/source/Layout.js +66 -53
- package/source/Layout.test.js +7 -2
- package/source/VirtualScroller.constructor.js +27 -4
- package/source/VirtualScroller.items.js +47 -2
- package/source/VirtualScroller.js +23 -14
- package/source/VirtualScroller.layout.js +41 -28
- package/source/VirtualScroller.onContainerResize.js +1 -2
- package/source/VirtualScroller.state.js +10 -11
- package/source/VirtualScroller.verticalSpacing.js +32 -6
- package/source/react/VirtualScroller.js +96 -33
- package/source/react/useClassName.js +3 -3
- package/source/react/useForwardedRef.js +39 -0
- package/source/react/useInstanceMethods.js +12 -4
- package/source/react/useItemKeys.js +22 -4
- package/source/react/useOnItemHeightDidChange.js +29 -10
- package/source/react/useSetItemState.js +32 -10
- package/source/react/useState.js +6 -6
- package/source/react/{useStateNoStaleBug.js → useStateWithRepeatableRead.js} +1 -1
- package/source/react/useStyle.js +3 -2
- package/source/react/useValidateTableBodyItemsContainer.js +16 -0
- package/source/react/useVirtualScroller.js +4 -3
- package/source/test/ItemsContainer.js +10 -10
- package/source/test/VirtualScroller.js +16 -10
- package/website/index-dom-bypass.html +198 -0
- package/website/index-dom-grid.html +204 -0
- package/website/index-dom-scrollableContainer.html +215 -0
- package/website/index-dom-tbody-scrollableContainer.html +81 -0
- package/website/index-dom-tbody.html +65 -0
- package/website/index-dom.html +117 -84
- package/website/index-react-bypass.html +200 -0
- package/website/{index-grid.html → index-react-grid.html} +79 -92
- package/website/index-react-scrollableContainer.html +213 -0
- package/website/index-react-strictMode.html +199 -0
- package/website/index-react-tbody-scrollableContainer.html +96 -0
- package/website/index-react-tbody.html +80 -0
- package/website/{index-bypass.html → index-react.html} +84 -70
- package/website/index.html +84 -69
- package/website/{messages.js → items.js} +1 -1
- package/website/lib/babel.min.js +25 -0
- package/website/lib/prop-types.min.js +1 -0
- package/website/lib/react-dom.development.js +29924 -0
- package/website/lib/react-dom.production.min.js +267 -0
- package/website/lib/react.development.js +3343 -0
- package/website/lib/react.production.min.js +31 -0
- package/website/style.base.css +33 -0
- package/website/style.list.css +92 -0
- package/website/style.list.grid.css +23 -0
- package/bundle/index-tbody-scrollableContainer.html +0 -70
- package/bundle/index-tbody.html +0 -57
- package/bundle/on-scroll-to-dom.js +0 -2
- package/bundle/on-scroll-to-dom.js.map +0 -1
- package/bundle/on-scroll-to-react.js +0 -2
- package/bundle/on-scroll-to-react.js.map +0 -1
- package/bundle/on-scroll-to.js +0 -2
- package/bundle/on-scroll-to.js.map +0 -1
- package/commonjs/react/useStateNoStaleBug.js.map +0 -1
- package/modules/react/useStateNoStaleBug.js.map +0 -1
- package/website/index-scrollableContainer.html +0 -208
- package/website/index-tbody-scrollableContainer.html +0 -70
- package/website/index-tbody.html +0 -57
- package/website/lib/on-scroll-to-dom.js +0 -2
- package/website/lib/on-scroll-to-dom.js.map +0 -1
- package/website/lib/on-scroll-to-react.js +0 -2
- package/website/lib/on-scroll-to-react.js.map +0 -1
package/source/Layout.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import log, { warn } from './utility/debug.js'
|
|
2
2
|
import ScrollableContainerNotReadyError from './ScrollableContainerNotReadyError.js'
|
|
3
|
+
import { DEFAULT_VISIBLE_ITEM_ROWS_COUNT } from './Layout.defaults.js'
|
|
3
4
|
|
|
4
5
|
export default class Layout {
|
|
5
6
|
constructor({
|
|
6
7
|
isInBypassMode,
|
|
7
|
-
|
|
8
|
-
getInitialEstimatedVisibleItemRowsCount,
|
|
8
|
+
getEstimatedVisibleItemRowsCountForInitialRender,
|
|
9
9
|
measureItemsBatchSize,
|
|
10
10
|
getPrerenderMargin,
|
|
11
|
+
getPrerenderMarginRatio,
|
|
11
12
|
getVerticalSpacing,
|
|
12
13
|
getVerticalSpacingBeforeResize,
|
|
13
14
|
getColumnsCount,
|
|
@@ -20,10 +21,10 @@ export default class Layout {
|
|
|
20
21
|
getPreviouslyCalculatedLayout
|
|
21
22
|
}) {
|
|
22
23
|
this.isInBypassMode = isInBypassMode
|
|
23
|
-
this.
|
|
24
|
-
this.getInitialEstimatedVisibleItemRowsCount = getInitialEstimatedVisibleItemRowsCount
|
|
24
|
+
this.getEstimatedVisibleItemRowsCountForInitialRender = getEstimatedVisibleItemRowsCountForInitialRender
|
|
25
25
|
this.measureItemsBatchSize = measureItemsBatchSize
|
|
26
26
|
this.getPrerenderMargin = getPrerenderMargin
|
|
27
|
+
this.getPrerenderMarginRatio = getPrerenderMarginRatio
|
|
27
28
|
this.getVerticalSpacing = getVerticalSpacing
|
|
28
29
|
this.getVerticalSpacingBeforeResize = getVerticalSpacingBeforeResize
|
|
29
30
|
this.getColumnsCount = getColumnsCount
|
|
@@ -69,6 +70,10 @@ export default class Layout {
|
|
|
69
70
|
}) {
|
|
70
71
|
let firstShownItemIndex
|
|
71
72
|
let lastShownItemIndex
|
|
73
|
+
|
|
74
|
+
let beforeItemsHeight = 0
|
|
75
|
+
let afterItemsHeight = 0
|
|
76
|
+
|
|
72
77
|
// If there're no items then `firstShownItemIndex` stays `undefined`.
|
|
73
78
|
if (itemsCount > 0) {
|
|
74
79
|
const getLastShownItemIndex = () => {
|
|
@@ -78,18 +83,30 @@ export default class Layout {
|
|
|
78
83
|
firstShownItemIndex
|
|
79
84
|
})
|
|
80
85
|
}
|
|
86
|
+
|
|
81
87
|
firstShownItemIndex = 0
|
|
88
|
+
|
|
82
89
|
lastShownItemIndex = beforeStart
|
|
83
90
|
? this.getInitialLayoutValueWithFallback(
|
|
84
91
|
'lastShownItemIndex',
|
|
85
92
|
getLastShownItemIndex,
|
|
86
|
-
|
|
93
|
+
firstShownItemIndex
|
|
87
94
|
)
|
|
88
95
|
: getLastShownItemIndex()
|
|
96
|
+
|
|
97
|
+
const averageItemHeight = this.getAverageItemHeight()
|
|
98
|
+
const verticalSpacing = this.getVerticalSpacing()
|
|
99
|
+
|
|
100
|
+
const beforeItemsCount = firstShownItemIndex
|
|
101
|
+
const afterItemsCount = itemsCount - (lastShownItemIndex + 1)
|
|
102
|
+
|
|
103
|
+
beforeItemsHeight = Math.ceil(beforeItemsCount / columnsCount) * (verticalSpacing + averageItemHeight)
|
|
104
|
+
afterItemsHeight = Math.ceil(afterItemsCount / columnsCount) * (verticalSpacing + averageItemHeight)
|
|
89
105
|
}
|
|
106
|
+
|
|
90
107
|
return {
|
|
91
|
-
beforeItemsHeight
|
|
92
|
-
afterItemsHeight
|
|
108
|
+
beforeItemsHeight,
|
|
109
|
+
afterItemsHeight,
|
|
93
110
|
firstShownItemIndex,
|
|
94
111
|
lastShownItemIndex
|
|
95
112
|
}
|
|
@@ -103,54 +120,47 @@ export default class Layout {
|
|
|
103
120
|
if (this.isInBypassMode()) {
|
|
104
121
|
return itemsCount - 1
|
|
105
122
|
}
|
|
106
|
-
// On server side, at initialization time,
|
|
107
|
-
// `scrollableContainer` is `undefined`,
|
|
108
|
-
// so default to `1` estimated rows count.
|
|
109
|
-
let estimatedRowsCount = 1
|
|
110
|
-
if (this.getMaxVisibleAreaHeight()) {
|
|
111
|
-
estimatedRowsCount = this.getEstimatedRowsCountForHeight(this.getMaxVisibleAreaHeight() + this.getPrerenderMargin())
|
|
112
|
-
} else if (this.getInitialEstimatedVisibleItemRowsCount) {
|
|
113
|
-
estimatedRowsCount = this.getInitialEstimatedVisibleItemRowsCount()
|
|
114
|
-
if (isNaN(estimatedRowsCount)) {
|
|
115
|
-
throw new Error('[virtual-scroller] `getEstimatedVisibleItemRowsCount()` must return a number')
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
123
|
return Math.min(
|
|
119
|
-
firstShownItemIndex + (
|
|
124
|
+
firstShownItemIndex + (this.getInitialRenderedRowsCount() * columnsCount - 1),
|
|
120
125
|
itemsCount - 1
|
|
121
126
|
)
|
|
122
127
|
}
|
|
123
128
|
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
return Math.ceil((height + verticalSpacing) / (estimatedItemHeight + verticalSpacing))
|
|
129
|
-
} else {
|
|
130
|
-
// If no items have been rendered yet, and no `estimatedItemHeight` option
|
|
131
|
-
// has been passed, then default to `1` estimated rows count in any `height`.
|
|
132
|
-
return 1
|
|
129
|
+
getInitialRenderedRowsCount() {
|
|
130
|
+
const estimatedVisibleItemRowsCount = this.getEstimatedVisibleItemRowsCount()
|
|
131
|
+
if (typeof estimatedVisibleItemRowsCount === 'number') {
|
|
132
|
+
return Math.ceil(estimatedVisibleItemRowsCount * (1 + this.getPrerenderMarginRatio()))
|
|
133
133
|
}
|
|
134
|
+
// `DEFAULT_VISIBLE_ITEM_ROWS_COUNT` will be used in server-side render
|
|
135
|
+
// unless `getEstimatedVisibleItemRowsCount()` parameter is specified.
|
|
136
|
+
return DEFAULT_VISIBLE_ITEM_ROWS_COUNT
|
|
134
137
|
}
|
|
135
138
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (averageItemHeight) {
|
|
144
|
-
return averageItemHeight
|
|
139
|
+
getEstimatedVisibleItemRowsCount() {
|
|
140
|
+
const maxVisibleAreaHeight = this.getMaxVisibleAreaHeight()
|
|
141
|
+
if (typeof maxVisibleAreaHeight === 'number') {
|
|
142
|
+
const estimatedRowsCount = this.getEstimatedRowsCountForHeight(maxVisibleAreaHeight)
|
|
143
|
+
if (typeof estimatedRowsCount === 'number') {
|
|
144
|
+
return estimatedRowsCount
|
|
145
|
+
}
|
|
145
146
|
}
|
|
146
|
-
if (this.
|
|
147
|
-
const
|
|
148
|
-
if (
|
|
149
|
-
|
|
147
|
+
if (this.getEstimatedVisibleItemRowsCountForInitialRender) {
|
|
148
|
+
const estimatedRowsCount = this.getEstimatedVisibleItemRowsCountForInitialRender()
|
|
149
|
+
if (typeof estimatedRowsCount === 'number') {
|
|
150
|
+
return estimatedRowsCount
|
|
150
151
|
}
|
|
151
|
-
return
|
|
152
|
+
throw new Error('[virtual-scroller] `getEstimatedVisibleItemRowsCount()` must return a number')
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
getEstimatedRowsCountForHeight(height) {
|
|
157
|
+
const averageItemHeight = this.getAverageItemHeight()
|
|
158
|
+
const verticalSpacing = this.getVerticalSpacing()
|
|
159
|
+
// `estimatedItemHeight` will most likely be `0` if it hasn't been specified explicitly.
|
|
160
|
+
// In that case, it can't divide by `0`.
|
|
161
|
+
if (averageItemHeight + verticalSpacing > 0) {
|
|
162
|
+
return Math.ceil((height + verticalSpacing) / (averageItemHeight + verticalSpacing))
|
|
152
163
|
}
|
|
153
|
-
return 0
|
|
154
164
|
}
|
|
155
165
|
|
|
156
166
|
getLayoutUpdateForItemsDiff({
|
|
@@ -328,8 +338,17 @@ export default class Layout {
|
|
|
328
338
|
|
|
329
339
|
const columnsCount = this.getColumnsCount()
|
|
330
340
|
|
|
341
|
+
const getNonMeasuredItemRowsCount = () => {
|
|
342
|
+
const estimatedRowsCount = this.getEstimatedRowsCountForHeight(nonMeasuredAreaHeight)
|
|
343
|
+
if (typeof estimatedRowsCount === 'number') {
|
|
344
|
+
return estimatedRowsCount
|
|
345
|
+
}
|
|
346
|
+
// Render all available item rows as a sensible fallback behavior.
|
|
347
|
+
return Math.ceil(itemsCount / columnsCount)
|
|
348
|
+
}
|
|
349
|
+
|
|
331
350
|
const itemsCountToRenderForMeasurement = Math.min(
|
|
332
|
-
|
|
351
|
+
getNonMeasuredItemRowsCount() * columnsCount,
|
|
333
352
|
this.measureItemsBatchSize || Infinity,
|
|
334
353
|
)
|
|
335
354
|
|
|
@@ -434,15 +453,12 @@ export default class Layout {
|
|
|
434
453
|
// then `shownItemsHeight` would also have to be returned from this function:
|
|
435
454
|
// the total height of all shown items including vertical spacing between them.
|
|
436
455
|
//
|
|
437
|
-
// If "previously calculated layout" would be used then it would first
|
|
438
|
-
// `firstShownItemIndex` and then
|
|
456
|
+
// If "previously calculated layout" would be used then it would first calculate
|
|
457
|
+
// `firstShownItemIndex` and then calculate `lastShownItemIndex` as part of two
|
|
439
458
|
// separate calls of this function, each with or without `backwards` flag,
|
|
440
459
|
// depending on whether `visibleAreaInsideTheList.top` and `visibleAreaInsideTheList.top`
|
|
441
460
|
// have shifted up or down.
|
|
442
461
|
|
|
443
|
-
let firstShownItemIndex
|
|
444
|
-
let lastShownItemIndex
|
|
445
|
-
|
|
446
462
|
// It's not always required to pass `beforeItemsHeight` parameter:
|
|
447
463
|
// when `fromIndex` is `0`, it's also assumed to be `0`.
|
|
448
464
|
if (fromIndex === 0) {
|
|
@@ -678,8 +694,6 @@ export default class Layout {
|
|
|
678
694
|
const verticalSpacing = beforeResize ? this.getVerticalSpacingBeforeResize() : this.getVerticalSpacing()
|
|
679
695
|
|
|
680
696
|
while (i < beforeItemsCount) {
|
|
681
|
-
const currentRowFirstItemIndex = i
|
|
682
|
-
|
|
683
697
|
let rowHeight = 0
|
|
684
698
|
let columnIndex = 0
|
|
685
699
|
// Not checking for `itemsCount` overflow here because `i = beforeItemsCount`
|
|
@@ -734,7 +748,6 @@ export default class Layout {
|
|
|
734
748
|
// Which becomes negligible in my project's use case (a couple thousands items max).
|
|
735
749
|
|
|
736
750
|
const columnsCount = this.getColumnsCount()
|
|
737
|
-
const lastShownRowIndex = Math.floor(lastShownItemIndex / columnsCount)
|
|
738
751
|
|
|
739
752
|
let afterItemsHeight = 0
|
|
740
753
|
|
package/source/Layout.test.js
CHANGED
|
@@ -158,9 +158,11 @@ describe('Layout', function() {
|
|
|
158
158
|
|
|
159
159
|
let shouldResetGridLayout
|
|
160
160
|
|
|
161
|
+
// Don't `throw` `VirtualScroller` errors but rather collect them in an array.
|
|
161
162
|
const errors = []
|
|
162
|
-
|
|
163
|
-
|
|
163
|
+
global.VirtualScrollerCatchError = (error) => {
|
|
164
|
+
errors.push(error)
|
|
165
|
+
}
|
|
164
166
|
|
|
165
167
|
layout.getLayoutUpdateForItemsDiff(
|
|
166
168
|
{
|
|
@@ -185,7 +187,10 @@ describe('Layout', function() {
|
|
|
185
187
|
afterItemsHeight: 5 * (ITEM_HEIGHT + VERTICAL_SPACING)
|
|
186
188
|
})
|
|
187
189
|
|
|
190
|
+
// Stop collecting `VirtualScroller` errors in the `errors` array.
|
|
191
|
+
// Use the default behavior of just `throw`-ing such errors.
|
|
188
192
|
global.VirtualScrollerCatchError = undefined
|
|
193
|
+
// Verify the errors that have been `throw`-n.
|
|
189
194
|
errors.length.should.equal(2)
|
|
190
195
|
errors[0].message.should.equal('[virtual-scroller] ~ Prepended items count 5 is not divisible by Columns Count 4 ~')
|
|
191
196
|
errors[1].message.should.equal('[virtual-scroller] Layout reset required')
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import DOMEngine from './DOM/Engine.js'
|
|
2
2
|
|
|
3
3
|
import Layout, { LAYOUT_REASON } from './Layout.js'
|
|
4
|
+
import { DEFAULT_ITEM_HEIGHT } from './Layout.defaults.js'
|
|
4
5
|
import ScrollableContainerResizeHandler from './ScrollableContainerResizeHandler.js'
|
|
5
6
|
import BeforeResize from './BeforeResize.js'
|
|
6
7
|
import Scroll from './Scroll.js'
|
|
@@ -45,6 +46,7 @@ export default function VirtualScrollerConstructor(
|
|
|
45
46
|
// `estimatedItemHeight` is deprecated, use `getEstimatedItemHeight()` instead.
|
|
46
47
|
estimatedItemHeight,
|
|
47
48
|
getEstimatedVisibleItemRowsCount,
|
|
49
|
+
getEstimatedInterItemVerticalSpacing,
|
|
48
50
|
onItemInitialRender,
|
|
49
51
|
// `onItemFirstRender(i)` is deprecated, use `onItemInitialRender(item)` instead.
|
|
50
52
|
onItemFirstRender,
|
|
@@ -167,7 +169,7 @@ export default function VirtualScrollerConstructor(
|
|
|
167
169
|
|
|
168
170
|
createStateHelpers.call(this, { state, getInitialItemState, onStateChange, render, items })
|
|
169
171
|
|
|
170
|
-
createVerticalSpacingHelpers.call(this)
|
|
172
|
+
createVerticalSpacingHelpers.call(this, { getEstimatedInterItemVerticalSpacing })
|
|
171
173
|
createColumnsHelpers.call(this, { getColumnsCount })
|
|
172
174
|
|
|
173
175
|
createLayoutHelpers.call(this)
|
|
@@ -236,12 +238,33 @@ function createHelpers({
|
|
|
236
238
|
setItemHeight: (i, height) => this.getState().itemHeights[i] = height
|
|
237
239
|
})
|
|
238
240
|
|
|
241
|
+
this.getAverageItemHeight = () => {
|
|
242
|
+
const averageItemHeight = this.itemHeights.getAverageItemHeight()
|
|
243
|
+
if (typeof averageItemHeight === 'number') {
|
|
244
|
+
return averageItemHeight
|
|
245
|
+
}
|
|
246
|
+
return this.getEstimatedItemHeight()
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
this.getEstimatedItemHeight = () => {
|
|
250
|
+
if (getEstimatedItemHeight) {
|
|
251
|
+
const estimatedItemHeight = getEstimatedItemHeight()
|
|
252
|
+
if (typeof estimatedItemHeight === 'number') {
|
|
253
|
+
return estimatedItemHeight
|
|
254
|
+
}
|
|
255
|
+
throw new Error('[virtual-scroller] `getEstimatedItemHeight()` must return a number')
|
|
256
|
+
}
|
|
257
|
+
// `DEFAULT_ITEM_HEIGHT` will be used in server-side render
|
|
258
|
+
// unless `getEstimatedItemHeight()` parameter is specified.
|
|
259
|
+
return DEFAULT_ITEM_HEIGHT
|
|
260
|
+
}
|
|
261
|
+
|
|
239
262
|
this.layout = new Layout({
|
|
240
263
|
isInBypassMode: this.isInBypassMode,
|
|
241
|
-
|
|
242
|
-
getInitialEstimatedVisibleItemRowsCount: getEstimatedVisibleItemRowsCount,
|
|
264
|
+
getEstimatedVisibleItemRowsCountForInitialRender: getEstimatedVisibleItemRowsCount,
|
|
243
265
|
measureItemsBatchSize,
|
|
244
266
|
getPrerenderMargin: () => this.getPrerenderMargin(),
|
|
267
|
+
getPrerenderMarginRatio: () => this.getPrerenderMarginRatio(),
|
|
245
268
|
getVerticalSpacing: () => this.getVerticalSpacing(),
|
|
246
269
|
getVerticalSpacingBeforeResize: () => this.getVerticalSpacingBeforeResize(),
|
|
247
270
|
getColumnsCount: () => this.getColumnsCount(),
|
|
@@ -249,7 +272,7 @@ function createHelpers({
|
|
|
249
272
|
getItemHeight: (i) => this.getState().itemHeights[i],
|
|
250
273
|
getItemHeightBeforeResize: (i) => this.getState().beforeResize && this.getState().beforeResize.itemHeights[i],
|
|
251
274
|
getBeforeResizeItemsCount: () => this.getState().beforeResize ? this.getState().beforeResize.itemHeights.length : 0,
|
|
252
|
-
getAverageItemHeight: () => this.
|
|
275
|
+
getAverageItemHeight: () => this.getAverageItemHeight(),
|
|
253
276
|
// `this.scrollableContainer` is gonna be `undefined` during server-side rendering.
|
|
254
277
|
// https://gitlab.com/catamphetamine/virtual-scroller/-/issues/30
|
|
255
278
|
getMaxVisibleAreaHeight: () => this.scrollableContainer && this.scrollableContainer.getHeight(),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import log, {
|
|
1
|
+
import log, { reportError } from './utility/debug.js'
|
|
2
2
|
import getItemsDiff from './getItemsDiff.js'
|
|
3
3
|
import fillArray from './utility/fillArray.js'
|
|
4
4
|
|
|
@@ -7,6 +7,49 @@ export default function() {
|
|
|
7
7
|
return this.getState().items.length
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
this._getItemIndexByItemOrIndex = (itemOrIndex) => {
|
|
11
|
+
const item = itemOrIndex
|
|
12
|
+
const { items } = this.getState()
|
|
13
|
+
// Find the `item`'s index in the `items` array.
|
|
14
|
+
//
|
|
15
|
+
// Performance notes:
|
|
16
|
+
// Doing an `.indexOf()` operation on a very large array could be inefficient
|
|
17
|
+
// because it would have to check every element of the array.
|
|
18
|
+
// In case of any hypothetical performance-related issues,
|
|
19
|
+
// the code could use a `new Map()` as an "index" for finding individual items.
|
|
20
|
+
const i = items.indexOf(item)
|
|
21
|
+
// validate that the `item` exists in the `items` array.
|
|
22
|
+
if (i >= 0) {
|
|
23
|
+
return i
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Legacy behavior: some functions used to accept an `i: number` item index as an argument.
|
|
27
|
+
// Later the argument was changed to `item: Item`.
|
|
28
|
+
//
|
|
29
|
+
// The old way of passing an `i: number` argument would only result in weird application behavior
|
|
30
|
+
// when all of the below circumstances are true, which is assumed extremely unlikely:
|
|
31
|
+
//
|
|
32
|
+
// BOTH use a previous version of `virtual-scroller` (ones before December 2025, with downloads count that is not very high)
|
|
33
|
+
// AND to be used with an `items` array of type `number[]`
|
|
34
|
+
// AND to use any of the "advanced" functions:
|
|
35
|
+
// * `setItemState(i, newState)`
|
|
36
|
+
// * `onItemHeightDidChange(i)`
|
|
37
|
+
// * `getItemScrollPosition(i)`
|
|
38
|
+
//
|
|
39
|
+
// The code below handles the legacy compatibility aspect of the old type of argument
|
|
40
|
+
// except maybe for those "extremely unlikely" cases in which the app is still unlikely to crash.
|
|
41
|
+
//
|
|
42
|
+
if (typeof itemOrIndex === 'number') {
|
|
43
|
+
const i = itemOrIndex
|
|
44
|
+
// Validate the item index argument.
|
|
45
|
+
if (i >= 0 && i < items.length) {
|
|
46
|
+
return i
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
reportError(`Item not found: ${JSON.stringify(item)}`)
|
|
51
|
+
}
|
|
52
|
+
|
|
10
53
|
/**
|
|
11
54
|
* Updates `items`. For example, can prepend or append new items to the list.
|
|
12
55
|
* @param {any[]} newItems
|
|
@@ -81,7 +124,9 @@ export default function() {
|
|
|
81
124
|
if (prependedItemsCount > 0) {
|
|
82
125
|
log('Prepend', prependedItemsCount, 'items')
|
|
83
126
|
|
|
84
|
-
itemHeights = new Array(prependedItemsCount)
|
|
127
|
+
itemHeights = new Array(prependedItemsCount)
|
|
128
|
+
.concat(itemHeights)
|
|
129
|
+
|
|
85
130
|
itemStates = fillArray(
|
|
86
131
|
new Array(prependedItemsCount),
|
|
87
132
|
(i) => this.getInitialItemState(newItems[i])
|
|
@@ -206,10 +206,19 @@ export default class VirtualScroller {
|
|
|
206
206
|
|
|
207
207
|
/**
|
|
208
208
|
* Returns the items's top offset relative to the scrollable container's top edge.
|
|
209
|
-
* @param {
|
|
209
|
+
* @param {any} item — Item
|
|
210
210
|
* @return {[number]} Returns the item's scroll Y position. Returns `undefined` if any of the previous items haven't been rendered yet.
|
|
211
211
|
*/
|
|
212
|
-
getItemScrollPosition(
|
|
212
|
+
getItemScrollPosition(itemOrIndex) {
|
|
213
|
+
// Item index.
|
|
214
|
+
const i = this._getItemIndexByItemOrIndex(itemOrIndex)
|
|
215
|
+
|
|
216
|
+
// If the item wasn't found, the error was already reported,
|
|
217
|
+
// so just return some "sensible" default value.
|
|
218
|
+
if (i === undefined) {
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
|
|
213
222
|
const itemTopOffsetInList = this.layout.getItemTopOffset(i)
|
|
214
223
|
if (itemTopOffsetInList === undefined) {
|
|
215
224
|
return
|
|
@@ -221,28 +230,28 @@ export default class VirtualScroller {
|
|
|
221
230
|
* @deprecated
|
|
222
231
|
* `.onItemHeightChange()` has been renamed to `.onItemHeightDidChange()`.
|
|
223
232
|
*/
|
|
224
|
-
onItemHeightChange(
|
|
225
|
-
warn('`.onItemHeightChange(
|
|
226
|
-
this.onItemHeightDidChange(
|
|
233
|
+
onItemHeightChange(item) {
|
|
234
|
+
warn('`.onItemHeightChange(item)` method was renamed to `.onItemHeightDidChange(item)`')
|
|
235
|
+
this.onItemHeightDidChange(item)
|
|
227
236
|
}
|
|
228
237
|
|
|
229
238
|
/**
|
|
230
239
|
* Forces a re-measure of an item's height.
|
|
231
|
-
* @param
|
|
240
|
+
* @param {any} item — Item. Legacy argument variant: Item index.
|
|
232
241
|
*/
|
|
233
|
-
onItemHeightDidChange(
|
|
242
|
+
onItemHeightDidChange(itemOrIndex) {
|
|
234
243
|
// See the comments in the `setItemState()` function below for the rationale
|
|
235
244
|
// on why the `hasToBeStarted()` check was commented out.
|
|
236
245
|
// this.hasToBeStarted()
|
|
237
|
-
this._onItemHeightDidChange(
|
|
246
|
+
this._onItemHeightDidChange(itemOrIndex)
|
|
238
247
|
}
|
|
239
248
|
|
|
240
249
|
/**
|
|
241
250
|
* Updates an item's state in `state.itemStates[]`.
|
|
242
|
-
* @param {
|
|
243
|
-
* @param {any}
|
|
251
|
+
* @param {any} item — Item. Legacy argument variant: Item index.
|
|
252
|
+
* @param {any} newItemState — Item's new state
|
|
244
253
|
*/
|
|
245
|
-
setItemState(
|
|
254
|
+
setItemState(itemOrIndex, newItemState) {
|
|
246
255
|
// There is an issue in React 18.2.0 when `useInsertionEffect()` doesn't run twice
|
|
247
256
|
// on mount unlike `useLayoutEffect()` in "strict" mode. That causes a bug in a React
|
|
248
257
|
// implementation of the `virtual-scroller`.
|
|
@@ -285,13 +294,13 @@ export default class VirtualScroller {
|
|
|
285
294
|
// Commenting it out wouldn't result in any potential bugs because the code would work correctly
|
|
286
295
|
// in both cases.
|
|
287
296
|
// this.hasToBeStarted()
|
|
288
|
-
this._setItemState(
|
|
297
|
+
this._setItemState(itemOrIndex, newItemState)
|
|
289
298
|
}
|
|
290
299
|
|
|
291
300
|
// (deprecated)
|
|
292
301
|
// Use `.setItemState()` method name instead.
|
|
293
|
-
onItemStateChange(
|
|
294
|
-
this.setItemState(
|
|
302
|
+
onItemStateChange(item, newItemState) {
|
|
303
|
+
this.setItemState(item, newItemState)
|
|
295
304
|
}
|
|
296
305
|
|
|
297
306
|
/**
|
|
@@ -101,7 +101,7 @@ export default function() {
|
|
|
101
101
|
// or an "Expand YouTube video" button, which would result
|
|
102
102
|
// in the actual height of the list item being different
|
|
103
103
|
// from what has been initially measured in `this.itemHeights[i]`,
|
|
104
|
-
// if the developer didn't call `.setItemState(
|
|
104
|
+
// if the developer didn't call `.setItemState(item, newState)` and `.onItemHeightDidChange(item)`.
|
|
105
105
|
if (!validateWillBeHiddenItemHeightsAreAccurate.call(this, firstShownItemIndex, lastShownItemIndex)) {
|
|
106
106
|
log('~ Because some of the will-be-hidden item heights (listed above) have changed since they\'ve last been measured, redo layout. ~')
|
|
107
107
|
// Redo layout, now with the correct item heights.
|
|
@@ -135,7 +135,7 @@ export default function() {
|
|
|
135
135
|
log('Last shown item index', lastShownItemIndex)
|
|
136
136
|
log('Before items height', beforeItemsHeight)
|
|
137
137
|
log('After items height (actual or estimated)', afterItemsHeight)
|
|
138
|
-
log('Average item height (used for estimated after items height calculation)', this.
|
|
138
|
+
log('Average item height (used for estimated after items height calculation)', this.getAverageItemHeight())
|
|
139
139
|
if (isDebug()) {
|
|
140
140
|
log('Item heights', this.getState().itemHeights.slice())
|
|
141
141
|
log('Item states', this.getState().itemStates.slice())
|
|
@@ -177,9 +177,9 @@ export default function() {
|
|
|
177
177
|
// Instead of using a `this.previouslyCalculatedLayout` instance variable,
|
|
178
178
|
// this code could use `this.getState()` because it reflects what's currently on screen,
|
|
179
179
|
// but there's a single edge case when it could go out of sync —
|
|
180
|
-
// updating item heights externally via `.onItemHeightDidChange(
|
|
180
|
+
// updating item heights externally via `.onItemHeightDidChange(item)`.
|
|
181
181
|
//
|
|
182
|
-
// If, for example, an item height was updated externally via `.onItemHeightDidChange(
|
|
182
|
+
// If, for example, an item height was updated externally via `.onItemHeightDidChange(item)`
|
|
183
183
|
// then `this.getState().itemHeights` would get updated immediately but
|
|
184
184
|
// `this.getState().beforeItemsHeight` or `this.getState().afterItemsHeight`
|
|
185
185
|
// would still correspond to the previous item height, so those would be "stale".
|
|
@@ -270,7 +270,7 @@ export default function() {
|
|
|
270
270
|
* or an "Expand YouTube video" button, which would result
|
|
271
271
|
* in the actual height of the list item being different
|
|
272
272
|
* from what has been initially measured in `this.itemHeights[i]`,
|
|
273
|
-
* if the developer didn't call `.setItemState(
|
|
273
|
+
* if the developer didn't call `.setItemState(item, newState)` and `.onItemHeightDidChange(item)`.
|
|
274
274
|
*/
|
|
275
275
|
function validateWillBeHiddenItemHeightsAreAccurate(firstShownItemIndex, lastShownItemIndex) {
|
|
276
276
|
let isValid = true
|
|
@@ -282,26 +282,26 @@ export default function() {
|
|
|
282
282
|
// The item will be hidden. Re-measure its height.
|
|
283
283
|
// The rationale is that there could be a situation when an item's
|
|
284
284
|
// height has changed, and the developer has properly added an
|
|
285
|
-
// `.onItemHeightDidChange(
|
|
285
|
+
// `.onItemHeightDidChange(item)` call to notify `VirtualScroller`
|
|
286
286
|
// about that change, but at the same time that wouldn't work.
|
|
287
287
|
// For example, suppose there's a list of several items on a page,
|
|
288
288
|
// and those items are in "minimized" state (having height 100px).
|
|
289
289
|
// Then, a user clicks an "Expand all items" button, and all items
|
|
290
290
|
// in the list are expanded (expanded item height is gonna be 700px).
|
|
291
|
-
// `VirtualScroller` demands that `.onItemHeightDidChange(
|
|
291
|
+
// `VirtualScroller` demands that `.onItemHeightDidChange(item)` is called
|
|
292
292
|
// in such cases, and the developer has properly added the code to do that.
|
|
293
293
|
// So, if there were 10 "minimized" items visible on a page, then there
|
|
294
|
-
// will be 10 individual `.onItemHeightDidChange(
|
|
295
|
-
// But, as the first `.onItemHeightDidChange(
|
|
294
|
+
// will be 10 individual `.onItemHeightDidChange(item)` calls. No issues so far.
|
|
295
|
+
// But, as the first `.onItemHeightDidChange(item)` call executes, it immediately
|
|
296
296
|
// ("synchronously") triggers a re-layout, and that re-layout finds out
|
|
297
297
|
// that now, because the first item is big, it occupies most of the screen
|
|
298
298
|
// space, and only the first 3 items are visible on screen instead of 10,
|
|
299
299
|
// and so it leaves the first 3 items mounted and unmounts the rest 7.
|
|
300
300
|
// Then, after `VirtualScroller` has rerendered, the code returns to
|
|
301
|
-
// where it was executing, and calls `.onItemHeightDidChange(
|
|
301
|
+
// where it was executing, and calls `.onItemHeightDidChange(item)` for the
|
|
302
302
|
// second item. It also triggers an immediate re-layout that finds out
|
|
303
303
|
// that only the first 2 items are visible on screen, and it unmounts
|
|
304
|
-
// the third one too. After that, it calls `.onItemHeightDidChange(
|
|
304
|
+
// the third one too. After that, it calls `.onItemHeightDidChange(item)`
|
|
305
305
|
// for the third item, but that item is no longer rendered, so its height
|
|
306
306
|
// can't be measured, and the same's for all the rest of the original 10 items.
|
|
307
307
|
// So, even though the developer has written their code properly, the
|
|
@@ -319,7 +319,7 @@ export default function() {
|
|
|
319
319
|
updatePreviouslyCalculatedLayoutOnItemHeightChange.call(this, i, previouslyMeasuredItemHeight, actualItemHeight)
|
|
320
320
|
}
|
|
321
321
|
isValid = false
|
|
322
|
-
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 `onItemHeightDidChange(
|
|
322
|
+
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 `onItemHeightDidChange(item)` 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.')
|
|
323
323
|
}
|
|
324
324
|
}
|
|
325
325
|
i++
|
|
@@ -372,9 +372,15 @@ export default function() {
|
|
|
372
372
|
return listTopOffset
|
|
373
373
|
}
|
|
374
374
|
|
|
375
|
-
this._onItemHeightDidChange = (
|
|
376
|
-
|
|
377
|
-
|
|
375
|
+
this._onItemHeightDidChange = (itemOrIndex) => {
|
|
376
|
+
// Item index.
|
|
377
|
+
const i = this._getItemIndexByItemOrIndex(itemOrIndex)
|
|
378
|
+
|
|
379
|
+
// If the item wasn't found, the error was already reported,
|
|
380
|
+
// so just don't do anything else.
|
|
381
|
+
if (i === undefined) {
|
|
382
|
+
return
|
|
383
|
+
}
|
|
378
384
|
|
|
379
385
|
const {
|
|
380
386
|
itemHeights,
|
|
@@ -382,39 +388,42 @@ export default function() {
|
|
|
382
388
|
lastShownItemIndex
|
|
383
389
|
} = this.getState()
|
|
384
390
|
|
|
391
|
+
log('~ On Item Height Did Change was called ~')
|
|
392
|
+
log('Item index', i)
|
|
393
|
+
|
|
385
394
|
// Check if the item is still rendered.
|
|
386
395
|
if (!(i >= firstShownItemIndex && i <= lastShownItemIndex)) {
|
|
387
396
|
// There could be valid cases when an item is no longer rendered
|
|
388
|
-
// by the time `.onItemHeightDidChange(
|
|
397
|
+
// by the time `.onItemHeightDidChange(item)` gets called.
|
|
389
398
|
// For example, suppose there's a list of several items on a page,
|
|
390
399
|
// and those items are in "minimized" state (having height 100px).
|
|
391
400
|
// Then, a user clicks an "Expand all items" button, and all items
|
|
392
401
|
// in the list are expanded (expanded item height is gonna be 700px).
|
|
393
|
-
// `VirtualScroller` demands that `.onItemHeightDidChange(
|
|
402
|
+
// `VirtualScroller` demands that `.onItemHeightDidChange(item)` is called
|
|
394
403
|
// in such cases, and the developer has properly added the code to do that.
|
|
395
404
|
// So, if there were 10 "minimized" items visible on a page, then there
|
|
396
|
-
// will be 10 individual `.onItemHeightDidChange(
|
|
397
|
-
// But, as the first `.onItemHeightDidChange(
|
|
405
|
+
// will be 10 individual `.onItemHeightDidChange(item)` calls. No issues so far.
|
|
406
|
+
// But, as the first `.onItemHeightDidChange(item)` call executes, it immediately
|
|
398
407
|
// ("synchronously") triggers a re-layout, and that re-layout finds out
|
|
399
408
|
// that now, because the first item is big, it occupies most of the screen
|
|
400
409
|
// space, and only the first 3 items are visible on screen instead of 10,
|
|
401
410
|
// and so it leaves the first 3 items mounted and unmounts the rest 7.
|
|
402
411
|
// Then, after `VirtualScroller` has rerendered, the code returns to
|
|
403
|
-
// where it was executing, and calls `.onItemHeightDidChange(
|
|
412
|
+
// where it was executing, and calls `.onItemHeightDidChange(item)` for the
|
|
404
413
|
// second item. It also triggers an immediate re-layout that finds out
|
|
405
414
|
// that only the first 2 items are visible on screen, and it unmounts
|
|
406
|
-
// the third one too. After that, it calls `.onItemHeightDidChange(
|
|
415
|
+
// the third one too. After that, it calls `.onItemHeightDidChange(item)`
|
|
407
416
|
// for the third item, but that item is no longer rendered, so its height
|
|
408
417
|
// can't be measured, and the same's for all the rest of the original 10 items.
|
|
409
418
|
// So, even though the developer has written their code properly, there're
|
|
410
419
|
// still situations when the item could be no longer rendered by the time
|
|
411
|
-
// `.onItemHeightDidChange(
|
|
412
|
-
return warn('The item is no longer rendered. This is not necessarily a bug, and could happen, for example, when when a developer calls `onItemHeightDidChange(
|
|
420
|
+
// `.onItemHeightDidChange(item)` gets called.
|
|
421
|
+
return warn('The item is no longer rendered. This is not necessarily a bug, and could happen, for example, when when a developer calls `onItemHeightDidChange(item)` while looping through a batch of items.')
|
|
413
422
|
}
|
|
414
423
|
|
|
415
424
|
const previousHeight = itemHeights[i]
|
|
416
425
|
if (previousHeight === undefined) {
|
|
417
|
-
// There're valid cases when the item still hasn't been measured and `onItemHeightDidChange()`
|
|
426
|
+
// There're valid cases when the item still hasn't been measured and `onItemHeightDidChange(item)`
|
|
418
427
|
// function was called for it. That's because measuring items is only done after the `VirtualScroller`
|
|
419
428
|
// has `start()`ed. But it's not neccessarily `start()`ed by the time it has been rendered (mounted).
|
|
420
429
|
// For example, the React component `<VirtualScroller/>` provides an `readyToStart={false}` property
|
|
@@ -435,7 +444,7 @@ export default function() {
|
|
|
435
444
|
try {
|
|
436
445
|
newHeight = remeasureItemHeight.call(this, i)
|
|
437
446
|
} catch (error) {
|
|
438
|
-
// Successfully finishing an `onItemHeightDidChange(
|
|
447
|
+
// Successfully finishing an `onItemHeightDidChange(item)` call is not considered
|
|
439
448
|
// critical for `VirtualScroller`'s operation, so such errors could be ignored.
|
|
440
449
|
if (error instanceof ItemNotRenderedError) {
|
|
441
450
|
return reportError(`"onItemHeightDidChange()" has been called for item index ${i} but the item is not currently rendered and can\'t be measured. The exact error was: ${error.message}`)
|
|
@@ -460,7 +469,7 @@ export default function() {
|
|
|
460
469
|
// Recalculate layout.
|
|
461
470
|
//
|
|
462
471
|
// If the `VirtualScroller` is already waiting for a state update to be rendered,
|
|
463
|
-
// delay `onItemHeightDidChange(
|
|
472
|
+
// delay `onItemHeightDidChange(item)`'s re-layout until that state update is rendered.
|
|
464
473
|
// The reason is that React `<VirtualScroller/>`'s `onHeightDidChange()` is meant to
|
|
465
474
|
// be called inside `useLayoutEffect()` hook. Due to how React is implemented internally,
|
|
466
475
|
// that might happen in the middle of the currently pending `setState()` operation
|
|
@@ -503,8 +512,12 @@ export default function() {
|
|
|
503
512
|
// scroll past the screen height, because they'd stop to read through
|
|
504
513
|
// the newly visible items first, and when they do stop scrolling, that's
|
|
505
514
|
// when layout gets recalculated).
|
|
506
|
-
|
|
507
|
-
|
|
515
|
+
return this.scrollableContainer.getHeight() * this.getPrerenderMarginRatio()
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
this.getPrerenderMarginRatio = () => {
|
|
519
|
+
// See the readme for the description of `prerenderMarginRatio` option.
|
|
520
|
+
return 1 // in scrollable container heights.
|
|
508
521
|
}
|
|
509
522
|
|
|
510
523
|
/**
|