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