virtual-scroller 1.11.2 → 1.12.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 +5 -0
- package/README.md +13 -11
- 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/DOM/ItemsContainer.js +10 -3
- package/commonjs/DOM/ItemsContainer.js.map +1 -1
- package/commonjs/DOM/VirtualScroller.js +13 -1
- package/commonjs/DOM/VirtualScroller.js.map +1 -1
- package/commonjs/ItemHeights.js +5 -5
- package/commonjs/ItemHeights.js.map +1 -1
- package/commonjs/ItemNotRenderedError.js +64 -0
- package/commonjs/ItemNotRenderedError.js.map +1 -0
- package/commonjs/Layout.test.js +10 -0
- package/commonjs/Layout.test.js.map +1 -1
- package/commonjs/VirtualScroller.js +23 -5
- package/commonjs/VirtualScroller.js.map +1 -1
- package/commonjs/VirtualScroller.layout.js +81 -39
- package/commonjs/VirtualScroller.layout.js.map +1 -1
- package/commonjs/VirtualScroller.onRender.js +97 -45
- package/commonjs/VirtualScroller.onRender.js.map +1 -1
- package/commonjs/VirtualScroller.state.js +50 -18
- package/commonjs/VirtualScroller.state.js.map +1 -1
- package/commonjs/react/VirtualScroller.js +31 -46
- package/commonjs/react/VirtualScroller.js.map +1 -1
- package/commonjs/react/useItemKeys.js +11 -3
- package/commonjs/react/useItemKeys.js.map +1 -1
- package/commonjs/react/useOnChange.js +19 -0
- package/commonjs/react/useOnChange.js.map +1 -0
- package/commonjs/react/{useOnItemHeightChange.js → useOnItemHeightDidChange.js} +7 -7
- package/commonjs/react/useOnItemHeightDidChange.js.map +1 -0
- package/commonjs/react/{useHandleItemsPropertyChange.js → useSetNewItemsOnItemsPropertyChange.js} +15 -14
- package/commonjs/react/useSetNewItemsOnItemsPropertyChange.js.map +1 -0
- package/commonjs/react/useState.js +162 -69
- package/commonjs/react/useState.js.map +1 -1
- package/commonjs/react/useStyle.js +3 -5
- package/commonjs/react/useStyle.js.map +1 -1
- package/commonjs/react/useUpdateItemKeysOnItemsChange.js +61 -0
- package/commonjs/react/useUpdateItemKeysOnItemsChange.js.map +1 -0
- package/commonjs/test/ItemsContainer.js +22 -1
- package/commonjs/test/ItemsContainer.js.map +1 -1
- package/commonjs/utility/debug.js +30 -6
- package/commonjs/utility/debug.js.map +1 -1
- package/dom/index.d.ts +1 -1
- package/index.cjs +2 -0
- package/index.d.ts +7 -1
- package/index.js +1 -0
- package/modules/DOM/ItemsContainer.js +8 -3
- package/modules/DOM/ItemsContainer.js.map +1 -1
- package/modules/DOM/VirtualScroller.js +13 -1
- package/modules/DOM/VirtualScroller.js.map +1 -1
- package/modules/ItemHeights.js +5 -5
- package/modules/ItemHeights.js.map +1 -1
- package/modules/ItemNotRenderedError.js +57 -0
- package/modules/ItemNotRenderedError.js.map +1 -0
- package/modules/Layout.test.js +10 -0
- package/modules/Layout.test.js.map +1 -1
- package/modules/VirtualScroller.js +17 -5
- package/modules/VirtualScroller.js.map +1 -1
- package/modules/VirtualScroller.layout.js +78 -39
- package/modules/VirtualScroller.layout.js.map +1 -1
- package/modules/VirtualScroller.onRender.js +98 -46
- package/modules/VirtualScroller.onRender.js.map +1 -1
- package/modules/VirtualScroller.state.js +50 -18
- package/modules/VirtualScroller.state.js.map +1 -1
- package/modules/react/VirtualScroller.js +31 -46
- package/modules/react/VirtualScroller.js.map +1 -1
- package/modules/react/useItemKeys.js +8 -3
- package/modules/react/useItemKeys.js.map +1 -1
- package/modules/react/useOnChange.js +11 -0
- package/modules/react/useOnChange.js.map +1 -0
- package/modules/react/{useOnItemHeightChange.js → useOnItemHeightDidChange.js} +6 -6
- package/modules/react/useOnItemHeightDidChange.js.map +1 -0
- package/modules/react/{useHandleItemsPropertyChange.js → useSetNewItemsOnItemsPropertyChange.js} +11 -13
- package/modules/react/useSetNewItemsOnItemsPropertyChange.js.map +1 -0
- package/modules/react/useState.js +156 -73
- package/modules/react/useState.js.map +1 -1
- package/modules/react/useStyle.js +3 -5
- package/modules/react/useStyle.js.map +1 -1
- package/{commonjs/react/useHandleItemIndexesChange.js → modules/react/useUpdateItemKeysOnItemsChange.js} +18 -21
- package/modules/react/useUpdateItemKeysOnItemsChange.js.map +1 -0
- package/modules/test/ItemsContainer.js +20 -1
- package/modules/test/ItemsContainer.js.map +1 -1
- package/modules/utility/debug.js +31 -6
- package/modules/utility/debug.js.map +1 -1
- package/package.json +1 -1
- package/source/DOM/ItemsContainer.js +8 -3
- package/source/DOM/VirtualScroller.js +11 -1
- package/source/ItemHeights.js +5 -5
- package/source/ItemNotRenderedError.js +16 -0
- package/source/Layout.test.js +9 -0
- package/source/VirtualScroller.js +14 -3
- package/source/VirtualScroller.layout.js +77 -38
- package/source/VirtualScroller.onRender.js +95 -42
- package/source/VirtualScroller.state.js +57 -20
- package/source/react/VirtualScroller.js +28 -39
- package/source/react/useItemKeys.js +9 -2
- package/source/react/useOnChange.js +11 -0
- package/source/react/{useOnItemHeightChange.js → useOnItemHeightDidChange.js} +5 -5
- package/source/react/{useHandleItemsPropertyChange.js → useSetNewItemsOnItemsPropertyChange.js} +11 -11
- package/source/react/useState.js +159 -71
- package/source/react/useStyle.js +2 -2
- package/source/react/{useHandleItemIndexesChange.js → useUpdateItemKeysOnItemsChange.js} +17 -9
- package/source/test/ItemsContainer.js +22 -1
- package/source/utility/debug.js +18 -4
- package/commonjs/react/useHandleItemIndexesChange.js.map +0 -1
- package/commonjs/react/useHandleItemsPropertyChange.js.map +0 -1
- package/commonjs/react/useOnItemHeightChange.js.map +0 -1
- package/modules/react/useHandleItemIndexesChange.js +0 -45
- package/modules/react/useHandleItemIndexesChange.js.map +0 -1
- package/modules/react/useHandleItemsPropertyChange.js.map +0 -1
- package/modules/react/useOnItemHeightChange.js.map +0 -1
|
@@ -141,8 +141,17 @@ export default class VirtualScroller {
|
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
/**
|
|
145
|
+
* @deprecated
|
|
146
|
+
* `.onItemHeightChange()` has been renamed to `.onItemHeightDidChange()`.
|
|
147
|
+
*/
|
|
144
148
|
onItemHeightChange(i) {
|
|
145
|
-
|
|
149
|
+
warn('`.onItemHeightChange(i)` method was renamed to `.onItemHeightDidChange(i)`')
|
|
150
|
+
this.onItemHeightDidChange(i)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
onItemHeightDidChange(i) {
|
|
154
|
+
this.virtualScroller.onItemHeightDidChange(i)
|
|
146
155
|
}
|
|
147
156
|
|
|
148
157
|
setItemState(i, newState) {
|
|
@@ -154,6 +163,7 @@ export default class VirtualScroller {
|
|
|
154
163
|
* `.updateItems()` has been renamed to `.setItems()`.
|
|
155
164
|
*/
|
|
156
165
|
updateItems(newItems, options) {
|
|
166
|
+
warn('`.updateItems()` method was renamed to `.setItems(i)`')
|
|
157
167
|
this.setItems(newItems, options)
|
|
158
168
|
}
|
|
159
169
|
|
package/source/ItemHeights.js
CHANGED
|
@@ -119,7 +119,7 @@ export default class ItemHeights {
|
|
|
119
119
|
// Measure item heights that haven't been measured previously.
|
|
120
120
|
// Don't re-measure item heights that have been measured previously.
|
|
121
121
|
// The rationale is that developers are supposed to manually call
|
|
122
|
-
// `.
|
|
122
|
+
// `.onItemHeightDidChange()` immediately every time an item's height has changed.
|
|
123
123
|
// If developers don't neglect that rule, item heights won't
|
|
124
124
|
// change unexpectedly.
|
|
125
125
|
if (this._get(i) === undefined) {
|
|
@@ -169,7 +169,7 @@ export default class ItemHeights {
|
|
|
169
169
|
const previousHeight = this._get(i)
|
|
170
170
|
const height = this._measureItemHeight(i, firstShownItemIndex)
|
|
171
171
|
if (previousHeight !== height) {
|
|
172
|
-
warn('Item index', i, 'height changed unexpectedly: it was', previousHeight, 'before, but now it is', height, '. An item\'s height is allowed to change only in two cases: when the item\'s "state" changes and the developer calls `setItemState(i, newState)`, or when the item\'s height changes for any reason and the developer calls `
|
|
172
|
+
warn('Item index', i, 'height changed unexpectedly: it was', previousHeight, 'before, but now it is', height, '. An item\'s height is allowed to change only in two cases: when the item\'s "state" changes and the developer calls `setItemState(i, newState)`, or when the item\'s height changes for any reason and the developer calls `onItemHeightDidChange(i)` right after that happens. Perhaps you forgot to persist the item\'s "state" by calling `setItemState(i, newState)` when it changed, and that "state" got lost when the item element was unmounted, which resulted in a different height when the item was shown again having its "state" reset.')
|
|
173
173
|
// Update the item's height as an attempt to fix things.
|
|
174
174
|
this._set(i, height)
|
|
175
175
|
}
|
|
@@ -189,18 +189,18 @@ export default class ItemHeights {
|
|
|
189
189
|
remeasureItemHeight(i, firstShownItemIndex) {
|
|
190
190
|
const previousHeight = this._get(i)
|
|
191
191
|
const height = this._measureItemHeight(i, firstShownItemIndex)
|
|
192
|
-
// // Because this function is called from `.
|
|
192
|
+
// // Because this function is called from `.onItemHeightDidChange()`,
|
|
193
193
|
// // there're no guarantees in which circumstances a developer calls it,
|
|
194
194
|
// // and for which item indexes.
|
|
195
195
|
// // Therefore, to guard against cases of incorrect usage,
|
|
196
196
|
// // this function won't crash anything if the item isn't rendered
|
|
197
197
|
// // or hasn't been previously rendered.
|
|
198
198
|
// if (height !== undefined) {
|
|
199
|
-
// reportError(`"
|
|
199
|
+
// reportError(`"onItemHeightDidChange()" has been called for item ${i}, but that item isn't currently rendered.`)
|
|
200
200
|
// return
|
|
201
201
|
// }
|
|
202
202
|
// if (previousHeight === undefined) {
|
|
203
|
-
// reportError(`"
|
|
203
|
+
// reportError(`"onItemHeightDidChange()" has been called for item ${i}, but that item hasn't been rendered before.`)
|
|
204
204
|
// return
|
|
205
205
|
// }
|
|
206
206
|
this._set(i, height)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export default class ItemNotRenderedError extends Error {
|
|
2
|
+
constructor({
|
|
3
|
+
renderedElementIndex,
|
|
4
|
+
renderedElementsCount,
|
|
5
|
+
message
|
|
6
|
+
}) {
|
|
7
|
+
super(message || getDefaultMessage({ renderedElementIndex, renderedElementsCount }))
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function getDefaultMessage({
|
|
12
|
+
renderedElementIndex,
|
|
13
|
+
renderedElementsCount
|
|
14
|
+
}) {
|
|
15
|
+
return `Element with index ${renderedElementIndex} was not found in the list of Rendered Item Elements in the Items Container of Virtual Scroller. There're only ${renderedElementsCount} Elements there.`
|
|
16
|
+
}
|
package/source/Layout.test.js
CHANGED
|
@@ -148,6 +148,10 @@ describe('Layout', function() {
|
|
|
148
148
|
|
|
149
149
|
let shouldResetGridLayout
|
|
150
150
|
|
|
151
|
+
const errors = []
|
|
152
|
+
|
|
153
|
+
global.VirtualScrollerCatchError = (error) => errors.push(error)
|
|
154
|
+
|
|
151
155
|
layout.getLayoutUpdateForItemsDiff(
|
|
152
156
|
{
|
|
153
157
|
firstShownItemIndex: 3,
|
|
@@ -171,6 +175,11 @@ describe('Layout', function() {
|
|
|
171
175
|
afterItemsHeight: 5 * (ITEM_HEIGHT + VERTICAL_SPACING)
|
|
172
176
|
})
|
|
173
177
|
|
|
178
|
+
global.VirtualScrollerCatchError = undefined
|
|
179
|
+
errors.length.should.equal(2)
|
|
180
|
+
errors[0].message.should.equal('[virtual-scroller] ~ Prepended items count 5 is not divisible by Columns Count 4 ~')
|
|
181
|
+
errors[1].message.should.equal('[virtual-scroller] Layout reset required')
|
|
182
|
+
|
|
174
183
|
shouldResetGridLayout.should.equal(true)
|
|
175
184
|
})
|
|
176
185
|
})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import VirtualScrollerConstructor from './VirtualScroller.constructor.js'
|
|
2
2
|
import { hasTbodyStyles, addTbodyStyles } from './DOM/tbody.js'
|
|
3
3
|
import { LAYOUT_REASON } from './Layout.js'
|
|
4
|
-
import log from './utility/debug.js'
|
|
4
|
+
import log, { warn } from './utility/debug.js'
|
|
5
5
|
|
|
6
6
|
export default class VirtualScroller {
|
|
7
7
|
/**
|
|
@@ -35,6 +35,8 @@ export default class VirtualScroller {
|
|
|
35
35
|
const isRestart = this._isActive === false
|
|
36
36
|
|
|
37
37
|
if (!isRestart) {
|
|
38
|
+
this.waitingForRender = true
|
|
39
|
+
|
|
38
40
|
// If no custom state storage has been configured, use the default one.
|
|
39
41
|
// Also sets the initial state.
|
|
40
42
|
if (!this._usesCustomStateStorage) {
|
|
@@ -205,13 +207,22 @@ export default class VirtualScroller {
|
|
|
205
207
|
return this.getListTopOffsetInsideScrollableContainer() + itemTopOffsetInList
|
|
206
208
|
}
|
|
207
209
|
|
|
210
|
+
/**
|
|
211
|
+
* @deprecated
|
|
212
|
+
* `.onItemHeightChange()` has been renamed to `.onItemHeightDidChange()`.
|
|
213
|
+
*/
|
|
214
|
+
onItemHeightChange(i) {
|
|
215
|
+
warn('`.onItemHeightChange(i)` method was renamed to `.onItemHeightDidChange(i)`')
|
|
216
|
+
this.onItemHeightDidChange(i)
|
|
217
|
+
}
|
|
218
|
+
|
|
208
219
|
/**
|
|
209
220
|
* Forces a re-measure of an item's height.
|
|
210
221
|
* @param {number} i — Item index
|
|
211
222
|
*/
|
|
212
|
-
|
|
223
|
+
onItemHeightDidChange(i) {
|
|
213
224
|
this.hasToBeStarted()
|
|
214
|
-
this.
|
|
225
|
+
this._onItemHeightDidChange(i)
|
|
215
226
|
}
|
|
216
227
|
|
|
217
228
|
/**
|
|
@@ -7,6 +7,8 @@ import { setTimeout, clearTimeout } from 'request-animation-frame-timeout'
|
|
|
7
7
|
import log, { warn, isDebug, reportError } from './utility/debug.js'
|
|
8
8
|
import { LAYOUT_REASON } from './Layout.js'
|
|
9
9
|
|
|
10
|
+
import ItemNotRenderedError from './ItemNotRenderedError.js'
|
|
11
|
+
|
|
10
12
|
export default function() {
|
|
11
13
|
this.onUpdateShownItemIndexes = ({ reason, stateUpdate }) => {
|
|
12
14
|
// In case of "don't do anything".
|
|
@@ -99,7 +101,7 @@ export default function() {
|
|
|
99
101
|
// or an "Expand YouTube video" button, which would result
|
|
100
102
|
// in the actual height of the list item being different
|
|
101
103
|
// from what has been initially measured in `this.itemHeights[i]`,
|
|
102
|
-
// if the developer didn't call `.setItemState(i, newState)` and `.
|
|
104
|
+
// if the developer didn't call `.setItemState(i, newState)` and `.onItemHeightDidChange(i)`.
|
|
103
105
|
if (!validateWillBeHiddenItemHeightsAreAccurate.call(this, firstShownItemIndex, lastShownItemIndex)) {
|
|
104
106
|
log('~ Because some of the will-be-hidden item heights (listed above) have changed since they\'ve last been measured, redo layout. ~')
|
|
105
107
|
// Redo layout, now with the correct item heights.
|
|
@@ -149,6 +151,9 @@ export default function() {
|
|
|
149
151
|
|
|
150
152
|
// Set `this.firstNonMeasuredItemIndex`.
|
|
151
153
|
this.firstNonMeasuredItemIndex = firstNonMeasuredItemIndex
|
|
154
|
+
// if (firstNonMeasuredItemIndex !== undefined) {
|
|
155
|
+
// log('Non-measured item index that will be measured at next layout', firstNonMeasuredItemIndex)
|
|
156
|
+
// }
|
|
152
157
|
|
|
153
158
|
// Set "previously calculated layout".
|
|
154
159
|
//
|
|
@@ -172,9 +177,9 @@ export default function() {
|
|
|
172
177
|
// Instead of using a `this.previouslyCalculatedLayout` instance variable,
|
|
173
178
|
// this code could use `this.getState()` because it reflects what's currently on screen,
|
|
174
179
|
// but there's a single edge case when it could go out of sync —
|
|
175
|
-
// updating item heights externally via `.
|
|
180
|
+
// updating item heights externally via `.onItemHeightDidChange(i)`.
|
|
176
181
|
//
|
|
177
|
-
// If, for example, an item height was updated externally via `.
|
|
182
|
+
// If, for example, an item height was updated externally via `.onItemHeightDidChange(i)`
|
|
178
183
|
// then `this.getState().itemHeights` would get updated immediately but
|
|
179
184
|
// `this.getState().beforeItemsHeight` or `this.getState().afterItemsHeight`
|
|
180
185
|
// would still correspond to the previous item height, so those would be "stale".
|
|
@@ -269,7 +274,7 @@ export default function() {
|
|
|
269
274
|
* or an "Expand YouTube video" button, which would result
|
|
270
275
|
* in the actual height of the list item being different
|
|
271
276
|
* from what has been initially measured in `this.itemHeights[i]`,
|
|
272
|
-
* if the developer didn't call `.setItemState(i, newState)` and `.
|
|
277
|
+
* if the developer didn't call `.setItemState(i, newState)` and `.onItemHeightDidChange(i)`.
|
|
273
278
|
*/
|
|
274
279
|
function validateWillBeHiddenItemHeightsAreAccurate(firstShownItemIndex, lastShownItemIndex) {
|
|
275
280
|
let isValid = true
|
|
@@ -281,26 +286,26 @@ export default function() {
|
|
|
281
286
|
// The item will be hidden. Re-measure its height.
|
|
282
287
|
// The rationale is that there could be a situation when an item's
|
|
283
288
|
// height has changed, and the developer has properly added an
|
|
284
|
-
// `.
|
|
289
|
+
// `.onItemHeightDidChange(i)` call to notify `VirtualScroller`
|
|
285
290
|
// about that change, but at the same time that wouldn't work.
|
|
286
291
|
// For example, suppose there's a list of several items on a page,
|
|
287
292
|
// and those items are in "minimized" state (having height 100px).
|
|
288
293
|
// Then, a user clicks an "Expand all items" button, and all items
|
|
289
294
|
// in the list are expanded (expanded item height is gonna be 700px).
|
|
290
|
-
// `VirtualScroller` demands that `.
|
|
295
|
+
// `VirtualScroller` demands that `.onItemHeightDidChange(i)` is called
|
|
291
296
|
// in such cases, and the developer has properly added the code to do that.
|
|
292
297
|
// So, if there were 10 "minimized" items visible on a page, then there
|
|
293
|
-
// will be 10 individual `.
|
|
294
|
-
// But, as the first `.
|
|
298
|
+
// will be 10 individual `.onItemHeightDidChange(i)` calls. No issues so far.
|
|
299
|
+
// But, as the first `.onItemHeightDidChange(i)` call executes, it immediately
|
|
295
300
|
// ("synchronously") triggers a re-layout, and that re-layout finds out
|
|
296
301
|
// that now, because the first item is big, it occupies most of the screen
|
|
297
302
|
// space, and only the first 3 items are visible on screen instead of 10,
|
|
298
303
|
// and so it leaves the first 3 items mounted and unmounts the rest 7.
|
|
299
304
|
// Then, after `VirtualScroller` has rerendered, the code returns to
|
|
300
|
-
// where it was executing, and calls `.
|
|
305
|
+
// where it was executing, and calls `.onItemHeightDidChange(i)` for the
|
|
301
306
|
// second item. It also triggers an immediate re-layout that finds out
|
|
302
307
|
// that only the first 2 items are visible on screen, and it unmounts
|
|
303
|
-
// the third one too. After that, it calls `.
|
|
308
|
+
// the third one too. After that, it calls `.onItemHeightDidChange(i)`
|
|
304
309
|
// for the third item, but that item is no longer rendered, so its height
|
|
305
310
|
// can't be measured, and the same's for all the rest of the original 10 items.
|
|
306
311
|
// So, even though the developer has written their code properly, the
|
|
@@ -318,7 +323,7 @@ export default function() {
|
|
|
318
323
|
updatePreviouslyCalculatedLayoutOnItemHeightChange.call(this, i, previouslyMeasuredItemHeight, actualItemHeight)
|
|
319
324
|
}
|
|
320
325
|
isValid = false
|
|
321
|
-
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 `
|
|
326
|
+
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(i)` calls issued at the same time, and the first one triggers a re-layout before the rest of them have had a chance to be executed.')
|
|
322
327
|
}
|
|
323
328
|
}
|
|
324
329
|
i++
|
|
@@ -340,20 +345,21 @@ export default function() {
|
|
|
340
345
|
// rather than from scratch, which would be an optimization.
|
|
341
346
|
//
|
|
342
347
|
function updatePreviouslyCalculatedLayoutOnItemHeightChange(i, previousHeight, newHeight) {
|
|
343
|
-
|
|
348
|
+
const prevLayout = this.previouslyCalculatedLayout
|
|
349
|
+
if (prevLayout) {
|
|
344
350
|
const heightDifference = newHeight - previousHeight
|
|
345
|
-
if (i <
|
|
346
|
-
// Patch `
|
|
347
|
-
|
|
348
|
-
} else if (i >
|
|
349
|
-
// Could patch `.afterItemsHeight` of `
|
|
350
|
-
// if `.afterItemsHeight` property existed in `
|
|
351
|
-
if (
|
|
352
|
-
|
|
351
|
+
if (i < prevLayout.firstShownItemIndex) {
|
|
352
|
+
// Patch `prevLayout`'s `.beforeItemsHeight`.
|
|
353
|
+
prevLayout.beforeItemsHeight += heightDifference
|
|
354
|
+
} else if (i > prevLayout.lastShownItemIndex) {
|
|
355
|
+
// Could patch `.afterItemsHeight` of `prevLayout` here,
|
|
356
|
+
// if `.afterItemsHeight` property existed in `prevLayout`.
|
|
357
|
+
if (prevLayout.afterItemsHeight !== undefined) {
|
|
358
|
+
prevLayout.afterItemsHeight += heightDifference
|
|
353
359
|
}
|
|
354
360
|
} else {
|
|
355
|
-
// Patch `
|
|
356
|
-
|
|
361
|
+
// Patch `prevLayout`'s shown items height.
|
|
362
|
+
prevLayout.shownItemsHeight += newHeight - previousHeight
|
|
357
363
|
}
|
|
358
364
|
}
|
|
359
365
|
}
|
|
@@ -370,8 +376,8 @@ export default function() {
|
|
|
370
376
|
return listTopOffset
|
|
371
377
|
}
|
|
372
378
|
|
|
373
|
-
this.
|
|
374
|
-
log('~
|
|
379
|
+
this._onItemHeightDidChange = (i) => {
|
|
380
|
+
log('~ On Item Height Did Change was called ~')
|
|
375
381
|
log('Item index', i)
|
|
376
382
|
|
|
377
383
|
const {
|
|
@@ -383,53 +389,86 @@ export default function() {
|
|
|
383
389
|
// Check if the item is still rendered.
|
|
384
390
|
if (!(i >= firstShownItemIndex && i <= lastShownItemIndex)) {
|
|
385
391
|
// There could be valid cases when an item is no longer rendered
|
|
386
|
-
// by the time `.
|
|
392
|
+
// by the time `.onItemHeightDidChange(i)` gets called.
|
|
387
393
|
// For example, suppose there's a list of several items on a page,
|
|
388
394
|
// and those items are in "minimized" state (having height 100px).
|
|
389
395
|
// Then, a user clicks an "Expand all items" button, and all items
|
|
390
396
|
// in the list are expanded (expanded item height is gonna be 700px).
|
|
391
|
-
// `VirtualScroller` demands that `.
|
|
397
|
+
// `VirtualScroller` demands that `.onItemHeightDidChange(i)` is called
|
|
392
398
|
// in such cases, and the developer has properly added the code to do that.
|
|
393
399
|
// So, if there were 10 "minimized" items visible on a page, then there
|
|
394
|
-
// will be 10 individual `.
|
|
395
|
-
// But, as the first `.
|
|
400
|
+
// will be 10 individual `.onItemHeightDidChange(i)` calls. No issues so far.
|
|
401
|
+
// But, as the first `.onItemHeightDidChange(i)` call executes, it immediately
|
|
396
402
|
// ("synchronously") triggers a re-layout, and that re-layout finds out
|
|
397
403
|
// that now, because the first item is big, it occupies most of the screen
|
|
398
404
|
// space, and only the first 3 items are visible on screen instead of 10,
|
|
399
405
|
// and so it leaves the first 3 items mounted and unmounts the rest 7.
|
|
400
406
|
// Then, after `VirtualScroller` has rerendered, the code returns to
|
|
401
|
-
// where it was executing, and calls `.
|
|
407
|
+
// where it was executing, and calls `.onItemHeightDidChange(i)` for the
|
|
402
408
|
// second item. It also triggers an immediate re-layout that finds out
|
|
403
409
|
// that only the first 2 items are visible on screen, and it unmounts
|
|
404
|
-
// the third one too. After that, it calls `.
|
|
410
|
+
// the third one too. After that, it calls `.onItemHeightDidChange(i)`
|
|
405
411
|
// for the third item, but that item is no longer rendered, so its height
|
|
406
412
|
// can't be measured, and the same's for all the rest of the original 10 items.
|
|
407
413
|
// So, even though the developer has written their code properly, there're
|
|
408
414
|
// still situations when the item could be no longer rendered by the time
|
|
409
|
-
// `.
|
|
410
|
-
return warn('The item is no longer rendered. This is not necessarily a bug, and could happen, for example, when when a developer calls `
|
|
415
|
+
// `.onItemHeightDidChange(i)` gets called.
|
|
416
|
+
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(i)` while looping through a batch of items.')
|
|
411
417
|
}
|
|
412
418
|
|
|
413
419
|
const previousHeight = itemHeights[i]
|
|
414
420
|
if (previousHeight === undefined) {
|
|
415
|
-
return reportError(`"
|
|
421
|
+
return reportError(`"onItemHeightDidChange()" has been called for item index ${i} but the item hasn't been rendered before.`)
|
|
416
422
|
}
|
|
417
423
|
|
|
418
|
-
|
|
424
|
+
log('~ Re-measure item height ~')
|
|
425
|
+
|
|
426
|
+
let newHeight
|
|
427
|
+
|
|
428
|
+
try {
|
|
429
|
+
newHeight = remeasureItemHeight.call(this, i)
|
|
430
|
+
} catch (error) {
|
|
431
|
+
// Successfully finishing an `onItemHeightDidChange(i)` call is not considered
|
|
432
|
+
// critical for `VirtualScroller`'s operation, so such errors could be ignored.
|
|
433
|
+
if (error instanceof ItemNotRenderedError) {
|
|
434
|
+
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}`)
|
|
435
|
+
}
|
|
436
|
+
}
|
|
419
437
|
|
|
420
438
|
log('Previous height', previousHeight)
|
|
421
439
|
log('New height', newHeight)
|
|
422
440
|
|
|
423
441
|
if (previousHeight !== newHeight) {
|
|
424
|
-
log('~ Item height has changed ~')
|
|
442
|
+
log('~ Item height has changed. Should update layout. ~')
|
|
425
443
|
|
|
426
|
-
// Update or reset previously calculated layout
|
|
444
|
+
// Update or reset a previously calculated layout
|
|
445
|
+
// so that the "diff"s based on that layout in the future
|
|
446
|
+
// produce correct results.
|
|
427
447
|
updatePreviouslyCalculatedLayoutOnItemHeightChange.call(this, i, previousHeight, newHeight)
|
|
428
448
|
|
|
429
449
|
// Recalculate layout.
|
|
430
|
-
|
|
450
|
+
//
|
|
451
|
+
// If the `VirtualScroller` is already waiting for a state update to be rendered,
|
|
452
|
+
// delay `onItemHeightDidChange(i)`'s re-layout until that state update is rendered.
|
|
453
|
+
// The reason is that React `<VirtualScroller/>`'s `onHeightDidChange()` is meant to
|
|
454
|
+
// be called inside `useLayoutEffect()` hook. Due to how React is implemented internally,
|
|
455
|
+
// that might happen in the middle of the currently pending `setState()` operation
|
|
456
|
+
// being applied, resulting in weird "race condition" bugs.
|
|
457
|
+
//
|
|
458
|
+
if (this.waitingForRender) {
|
|
459
|
+
log('~ Another state update is already waiting to be rendered. Delay the layout update until then. ~')
|
|
460
|
+
this.updateLayoutAfterRenderBecauseItemHeightChanged = true
|
|
461
|
+
} else {
|
|
462
|
+
this.onUpdateShownItemIndexes({ reason: LAYOUT_REASON.ITEM_HEIGHT_CHANGED })
|
|
463
|
+
}
|
|
431
464
|
|
|
432
|
-
//
|
|
465
|
+
// If there was a request for `setState()` with new `items`, then the changes
|
|
466
|
+
// to `currentState.itemHeights[]` made above in a `remeasureItemHeight()` call
|
|
467
|
+
// would be overwritten when that pending `setState()` call gets applied.
|
|
468
|
+
// To fix that, the updates to current `itemHeights[]` are noted in
|
|
469
|
+
// `this.itemHeightsThatChangedWhileNewItemsWereBeingRendered` variable.
|
|
470
|
+
// That variable is then checked when the `setState()` call with the new `items`
|
|
471
|
+
// has been updated.
|
|
433
472
|
if (this.newItemsWillBeRendered) {
|
|
434
473
|
if (!this.itemHeightsThatChangedWhileNewItemsWereBeingRendered) {
|
|
435
474
|
this.itemHeightsThatChangedWhileNewItemsWereBeingRendered = {}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import log, { warn, isDebug } from './utility/debug.js'
|
|
1
|
+
import log, { warn, reportError, isDebug } from './utility/debug.js'
|
|
2
2
|
import getStateSnapshot from './utility/getStateSnapshot.js'
|
|
3
3
|
import shallowEqual from './utility/shallowEqual.js'
|
|
4
4
|
import { LAYOUT_REASON } from './Layout.js'
|
|
@@ -11,6 +11,8 @@ export default function() {
|
|
|
11
11
|
* @param {object} [prevState]
|
|
12
12
|
*/
|
|
13
13
|
this._onRender = (newState, prevState) => {
|
|
14
|
+
this.waitingForRender = false
|
|
15
|
+
|
|
14
16
|
log('~ Rendered ~')
|
|
15
17
|
if (isDebug()) {
|
|
16
18
|
log('State', getStateSnapshot(newState))
|
|
@@ -33,19 +35,58 @@ export default function() {
|
|
|
33
35
|
)
|
|
34
36
|
}
|
|
35
37
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
// `this.mostRecentlySetState` checks that state management behavior is correct:
|
|
39
|
+
// that in situations when there're multiple new states waiting to be set,
|
|
40
|
+
// only the latest one gets applied.
|
|
41
|
+
// It keeps the code simpler and prevents possible race condition bugs.
|
|
42
|
+
// For example, `VirtualScroller` keeps track of its latest requested
|
|
43
|
+
// state update in different instance variable flags which assume that
|
|
44
|
+
// only that latest requested state update gets actually applied.
|
|
45
|
+
//
|
|
46
|
+
// This check should also be performed for the initial render in order to
|
|
47
|
+
// guarantee that no potentially incorrect state update goes unnoticed.
|
|
48
|
+
// Incorrect state updates could happen when `VirtualScroller` state
|
|
49
|
+
// is managed externally by passing `getState()`/`updateState()` options.
|
|
50
|
+
//
|
|
51
|
+
// Perform the check only when `this.mostRecentSetStateValue` is defined.
|
|
52
|
+
// `this.mostRecentSetStateValue` is normally gonna be `undefined` at the initial render
|
|
53
|
+
// because the initial state is not set by calling `this.updateState()`.
|
|
54
|
+
// At the same time, it is possible that the initial render is delayed
|
|
55
|
+
// for whatever reason, and `this.updateState()` gets called before the initial render,
|
|
56
|
+
// so `this.mostRecentSetStateValue` could also be defined at the initial render,
|
|
57
|
+
// in which case the check should be performed.
|
|
58
|
+
//
|
|
59
|
+
if (this.mostRecentSetStateValue) {
|
|
60
|
+
// "Shallow equality" is used here instead of "strict equality"
|
|
61
|
+
// because a developer might choose to supply an `updateState()` function
|
|
62
|
+
// rather than a `setState()` function, in which case the `updateState()` function
|
|
63
|
+
// would construct its own state object.
|
|
64
|
+
if (!shallowEqual(newState, this.mostRecentSetStateValue)) {
|
|
65
|
+
warn('The most recent state that was set', getStateSnapshot(this.mostRecentSetStateValue))
|
|
66
|
+
reportError('The state that has been rendered is not the most recent one that was set')
|
|
67
|
+
}
|
|
38
68
|
}
|
|
39
69
|
|
|
40
70
|
// `this.resetStateUpdateFlags()` must be called before calling
|
|
41
71
|
// `this.measureItemHeightsAndSpacing()`.
|
|
42
72
|
const {
|
|
43
73
|
nonMeasuredItemsHaveBeenRendered,
|
|
74
|
+
itemHeightHasChanged,
|
|
44
75
|
widthHasChanged
|
|
45
76
|
} = resetStateUpdateFlags.call(this)
|
|
46
77
|
|
|
47
78
|
let layoutUpdateReason
|
|
48
79
|
|
|
80
|
+
if (this.updateLayoutAfterRenderBecauseItemHeightChanged) {
|
|
81
|
+
layoutUpdateReason = LAYOUT_REASON.ITEM_HEIGHT_CHANGED
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!prevState) {
|
|
85
|
+
if (!layoutUpdateReason) {
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
49
90
|
// If the `VirtualScroller`, while calculating layout parameters, encounters
|
|
50
91
|
// a not-shown item with a non-measured height, it calls `updateState()` just to
|
|
51
92
|
// render that item first, and then, after the list has been re-rendered, it measures
|
|
@@ -74,41 +115,43 @@ export default function() {
|
|
|
74
115
|
this.verticalSpacing = undefined
|
|
75
116
|
}
|
|
76
117
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
118
|
+
if (prevState) {
|
|
119
|
+
const { items: previousItems } = prevState
|
|
120
|
+
const { items: newItems } = newState
|
|
121
|
+
// Even if `this.newItemsWillBeRendered` flag is `true`,
|
|
122
|
+
// `newItems` could still be equal to `previousItems`.
|
|
123
|
+
// For example, when `updateState()` calls don't update `state` immediately
|
|
124
|
+
// and a developer first calls `setItems(newItems)` and then calls `setItems(oldItems)`:
|
|
125
|
+
// in that case, `this.newItemsWillBeRendered` flag will be `true` but the actual `items`
|
|
126
|
+
// in state wouldn't have changed due to the first `updateState()` call being overwritten
|
|
127
|
+
// by the second `updateState()` call (that's called "batching state updates" in React).
|
|
128
|
+
if (newItems !== previousItems) {
|
|
129
|
+
const itemsDiff = this.getItemsDiff(previousItems, newItems)
|
|
130
|
+
if (itemsDiff) {
|
|
131
|
+
// The call to `.onPrepend()` must precede the call to `.measureItemHeights()`
|
|
132
|
+
// which is called in `.onRender()`.
|
|
133
|
+
// `this.itemHeights.onPrepend()` updates `firstMeasuredItemIndex`
|
|
134
|
+
// and `lastMeasuredItemIndex` of `this.itemHeights`.
|
|
135
|
+
const { prependedItemsCount } = itemsDiff
|
|
136
|
+
this.itemHeights.onPrepend(prependedItemsCount)
|
|
137
|
+
} else {
|
|
138
|
+
this.itemHeights.reset()
|
|
139
|
+
}
|
|
98
140
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
141
|
+
if (!widthHasChanged) {
|
|
142
|
+
// The call to `this.onNewItemsRendered()` must precede the call to
|
|
143
|
+
// `.measureItemHeights()` which is called in `.onRender()` because
|
|
144
|
+
// `this.onNewItemsRendered()` updates `firstMeasuredItemIndex` and
|
|
145
|
+
// `lastMeasuredItemIndex` of `this.itemHeights` in case of a prepend.
|
|
146
|
+
//
|
|
147
|
+
// If after prepending items the scroll position
|
|
148
|
+
// should be "restored" so that there's no "jump" of content
|
|
149
|
+
// then it means that all previous items have just been rendered
|
|
150
|
+
// in a single pass, and there's no need to update layout again.
|
|
151
|
+
//
|
|
152
|
+
if (onNewItemsRendered.call(this, itemsDiff, newState) !== 'SEAMLESS_PREPEND') {
|
|
153
|
+
layoutUpdateReason = LAYOUT_REASON.ITEMS_CHANGED
|
|
154
|
+
}
|
|
112
155
|
}
|
|
113
156
|
}
|
|
114
157
|
}
|
|
@@ -124,9 +167,11 @@ export default function() {
|
|
|
124
167
|
// item height measurements is required.
|
|
125
168
|
//
|
|
126
169
|
if (
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
170
|
+
(prevState && (
|
|
171
|
+
newState.firstShownItemIndex !== prevState.firstShownItemIndex ||
|
|
172
|
+
newState.lastShownItemIndex !== prevState.lastShownItemIndex ||
|
|
173
|
+
newState.items !== prevState.items
|
|
174
|
+
)) ||
|
|
130
175
|
widthHasChanged
|
|
131
176
|
) {
|
|
132
177
|
const verticalSpacingStateUpdate = this.measureItemHeightsAndSpacing()
|
|
@@ -202,14 +247,14 @@ export default function() {
|
|
|
202
247
|
// See if any items' heights changed while new items were being rendered.
|
|
203
248
|
if (this.itemHeightsThatChangedWhileNewItemsWereBeingRendered) {
|
|
204
249
|
for (const i of Object.keys(this.itemHeightsThatChangedWhileNewItemsWereBeingRendered)) {
|
|
205
|
-
itemHeights[prependedItemsCount +
|
|
250
|
+
itemHeights[prependedItemsCount + Number(i)] = this.itemHeightsThatChangedWhileNewItemsWereBeingRendered[i]
|
|
206
251
|
}
|
|
207
252
|
}
|
|
208
253
|
|
|
209
254
|
// See if any items' states changed while new items were being rendered.
|
|
210
255
|
if (this.itemStatesThatChangedWhileNewItemsWereBeingRendered) {
|
|
211
256
|
for (const i of Object.keys(this.itemStatesThatChangedWhileNewItemsWereBeingRendered)) {
|
|
212
|
-
itemStates[prependedItemsCount +
|
|
257
|
+
itemStates[prependedItemsCount + Number(i)] = this.itemStatesThatChangedWhileNewItemsWereBeingRendered[i]
|
|
213
258
|
}
|
|
214
259
|
}
|
|
215
260
|
|
|
@@ -325,6 +370,9 @@ export default function() {
|
|
|
325
370
|
|
|
326
371
|
// Read `this.firstNonMeasuredItemIndex` flag.
|
|
327
372
|
const nonMeasuredItemsHaveBeenRendered = this.firstNonMeasuredItemIndex !== undefined
|
|
373
|
+
if (nonMeasuredItemsHaveBeenRendered) {
|
|
374
|
+
log('Non-measured item index', this.firstNonMeasuredItemIndex)
|
|
375
|
+
}
|
|
328
376
|
// Reset `this.firstNonMeasuredItemIndex` flag.
|
|
329
377
|
this.firstNonMeasuredItemIndex = undefined
|
|
330
378
|
|
|
@@ -337,8 +385,13 @@ export default function() {
|
|
|
337
385
|
// Reset `this.itemStatesThatChangedWhileNewItemsWereBeingRendered`.
|
|
338
386
|
this.itemStatesThatChangedWhileNewItemsWereBeingRendered = undefined
|
|
339
387
|
|
|
388
|
+
// Reset `this.updateLayoutAfterRenderBecauseItemHeightChanged`.
|
|
389
|
+
const itemHeightHasChanged = this.updateLayoutAfterRenderBecauseItemHeightChanged
|
|
390
|
+
this.updateLayoutAfterRenderBecauseItemHeightChanged = undefined
|
|
391
|
+
|
|
340
392
|
return {
|
|
341
393
|
nonMeasuredItemsHaveBeenRendered,
|
|
394
|
+
itemHeightHasChanged,
|
|
342
395
|
widthHasChanged
|
|
343
396
|
}
|
|
344
397
|
}
|