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.
Files changed (154) hide show
  1. package/.gitlab-ci.yml +1 -1
  2. package/CHANGELOG.md +24 -1
  3. package/README.md +139 -33
  4. package/babel.config.js +25 -0
  5. package/babel.js +5 -0
  6. package/bundle/index-bypass.html +1 -1
  7. package/bundle/index-dom.html +1 -1
  8. package/bundle/index-grid.html +1 -2
  9. package/bundle/index-scrollableContainer.html +1 -1
  10. package/bundle/index-tbody-scrollableContainer.html +2 -0
  11. package/bundle/index-tbody.html +2 -0
  12. package/bundle/virtual-scroller-dom.js +1 -1
  13. package/bundle/virtual-scroller-dom.js.map +1 -1
  14. package/bundle/virtual-scroller-react.js +1 -1
  15. package/bundle/virtual-scroller-react.js.map +1 -1
  16. package/bundle/virtual-scroller.js +1 -1
  17. package/bundle/virtual-scroller.js.map +1 -1
  18. package/commonjs/BeforeResize.js +319 -0
  19. package/commonjs/BeforeResize.js.map +1 -0
  20. package/commonjs/DOM/Engine.js +46 -0
  21. package/commonjs/DOM/Engine.js.map +1 -0
  22. package/commonjs/DOM/ItemsContainer.js +78 -0
  23. package/commonjs/DOM/ItemsContainer.js.map +1 -0
  24. package/commonjs/DOM/{WaitForStylesToLoad.js → ListTopOffsetWatcher.js} +56 -35
  25. package/commonjs/DOM/ListTopOffsetWatcher.js.map +1 -0
  26. package/commonjs/DOM/ScrollableContainer.js +56 -81
  27. package/commonjs/DOM/ScrollableContainer.js.map +1 -1
  28. package/commonjs/DOM/VirtualScroller.js +20 -15
  29. package/commonjs/DOM/VirtualScroller.js.map +1 -1
  30. package/commonjs/DOM/tbody.js +2 -2
  31. package/commonjs/ItemHeights.js +22 -29
  32. package/commonjs/ItemHeights.js.map +1 -1
  33. package/commonjs/Layout.js +588 -215
  34. package/commonjs/Layout.js.map +1 -1
  35. package/commonjs/Layout.test.js +191 -0
  36. package/commonjs/Layout.test.js.map +1 -0
  37. package/commonjs/ListHeightChangeWatcher.js +126 -0
  38. package/commonjs/ListHeightChangeWatcher.js.map +1 -0
  39. package/commonjs/Resize.js +22 -21
  40. package/commonjs/Resize.js.map +1 -1
  41. package/commonjs/Scroll.js +148 -88
  42. package/commonjs/Scroll.js.map +1 -1
  43. package/commonjs/VirtualScroller.js +1269 -390
  44. package/commonjs/VirtualScroller.js.map +1 -1
  45. package/commonjs/getItemCoordinates.js.map +1 -1
  46. package/commonjs/getItemsDiff.js.map +1 -1
  47. package/commonjs/getVerticalSpacing.js +8 -8
  48. package/commonjs/getVerticalSpacing.js.map +1 -1
  49. package/commonjs/react/VirtualScroller.js +31 -37
  50. package/commonjs/react/VirtualScroller.js.map +1 -1
  51. package/commonjs/utility/debounce.js +26 -4
  52. package/commonjs/utility/debounce.js.map +1 -1
  53. package/commonjs/utility/debug.js +51 -12
  54. package/commonjs/utility/debug.js.map +1 -1
  55. package/commonjs/utility/getStateSnapshot.js +50 -0
  56. package/commonjs/utility/getStateSnapshot.js.map +1 -0
  57. package/commonjs/utility/px.js +1 -1
  58. package/commonjs/utility/px.js.map +1 -1
  59. package/commonjs/utility/px.test.js +14 -0
  60. package/commonjs/utility/px.test.js.map +1 -0
  61. package/commonjs/utility/shallowEqual.js +1 -1
  62. package/commonjs/utility/shallowEqual.js.map +1 -1
  63. package/commonjs/utility/throttle.js.map +1 -1
  64. package/dom/index.d.ts +23 -0
  65. package/index.d.ts +84 -0
  66. package/modules/BeforeResize.js +310 -0
  67. package/modules/BeforeResize.js.map +1 -0
  68. package/modules/DOM/Engine.js +27 -0
  69. package/modules/DOM/Engine.js.map +1 -0
  70. package/modules/DOM/ItemsContainer.js +71 -0
  71. package/modules/DOM/ItemsContainer.js.map +1 -0
  72. package/modules/DOM/{WaitForStylesToLoad.js → ListTopOffsetWatcher.js} +57 -35
  73. package/modules/DOM/ListTopOffsetWatcher.js.map +1 -0
  74. package/modules/DOM/ScrollableContainer.js +55 -80
  75. package/modules/DOM/ScrollableContainer.js.map +1 -1
  76. package/modules/DOM/VirtualScroller.js +15 -14
  77. package/modules/DOM/VirtualScroller.js.map +1 -1
  78. package/modules/ItemHeights.js +17 -28
  79. package/modules/ItemHeights.js.map +1 -1
  80. package/modules/Layout.js +582 -213
  81. package/modules/Layout.js.map +1 -1
  82. package/modules/Layout.test.js +185 -0
  83. package/modules/Layout.test.js.map +1 -0
  84. package/modules/ListHeightChangeWatcher.js +119 -0
  85. package/modules/ListHeightChangeWatcher.js.map +1 -0
  86. package/modules/Resize.js +21 -20
  87. package/modules/Resize.js.map +1 -1
  88. package/modules/Scroll.js +148 -87
  89. package/modules/Scroll.js.map +1 -1
  90. package/modules/VirtualScroller.js +1263 -390
  91. package/modules/VirtualScroller.js.map +1 -1
  92. package/modules/getItemCoordinates.js.map +1 -1
  93. package/modules/getItemsDiff.js.map +1 -1
  94. package/modules/getVerticalSpacing.js +8 -8
  95. package/modules/getVerticalSpacing.js.map +1 -1
  96. package/modules/react/VirtualScroller.js +31 -37
  97. package/modules/react/VirtualScroller.js.map +1 -1
  98. package/modules/utility/debounce.js +26 -4
  99. package/modules/utility/debounce.js.map +1 -1
  100. package/modules/utility/debug.js +47 -10
  101. package/modules/utility/debug.js.map +1 -1
  102. package/modules/utility/getStateSnapshot.js +43 -0
  103. package/modules/utility/getStateSnapshot.js.map +1 -0
  104. package/modules/utility/px.js +1 -1
  105. package/modules/utility/px.js.map +1 -1
  106. package/modules/utility/px.test.js +9 -0
  107. package/modules/utility/px.test.js.map +1 -0
  108. package/modules/utility/shallowEqual.js +1 -1
  109. package/modules/utility/shallowEqual.js.map +1 -1
  110. package/modules/utility/throttle.js.map +1 -1
  111. package/package.json +24 -22
  112. package/react/index.d.ts +27 -0
  113. package/source/BeforeResize.js +317 -0
  114. package/source/DOM/Engine.js +32 -0
  115. package/source/DOM/ItemsContainer.js +48 -0
  116. package/source/DOM/{WaitForStylesToLoad.js → ListTopOffsetWatcher.js} +48 -22
  117. package/source/DOM/ScrollableContainer.js +39 -56
  118. package/source/DOM/VirtualScroller.js +6 -7
  119. package/source/ItemHeights.js +19 -24
  120. package/source/Layout.js +626 -252
  121. package/source/Layout.test.js +171 -0
  122. package/source/ListHeightChangeWatcher.js +94 -0
  123. package/source/Resize.js +23 -15
  124. package/source/Scroll.js +139 -78
  125. package/source/VirtualScroller.js +1243 -286
  126. package/source/getVerticalSpacing.js +7 -7
  127. package/source/react/VirtualScroller.js +2 -18
  128. package/source/utility/debounce.js +20 -3
  129. package/source/utility/debug.js +34 -3
  130. package/source/utility/getStateSnapshot.js +36 -0
  131. package/source/utility/px.js +1 -1
  132. package/source/utility/px.test.js +9 -0
  133. package/website/index-bypass.html +195 -0
  134. package/website/index-grid.html +0 -1
  135. package/website/index-scrollableContainer.html +208 -0
  136. package/website/index-tbody-scrollableContainer.html +68 -0
  137. package/website/index-tbody.html +55 -0
  138. package/commonjs/DOM/RenderingEngine.js +0 -33
  139. package/commonjs/DOM/RenderingEngine.js.map +0 -1
  140. package/commonjs/DOM/Screen.js +0 -87
  141. package/commonjs/DOM/Screen.js.map +0 -1
  142. package/commonjs/DOM/WaitForStylesToLoad.js.map +0 -1
  143. package/commonjs/RestoreScroll.js +0 -118
  144. package/commonjs/RestoreScroll.js.map +0 -1
  145. package/modules/DOM/RenderingEngine.js +0 -19
  146. package/modules/DOM/RenderingEngine.js.map +0 -1
  147. package/modules/DOM/Screen.js +0 -80
  148. package/modules/DOM/Screen.js.map +0 -1
  149. package/modules/DOM/WaitForStylesToLoad.js.map +0 -1
  150. package/modules/RestoreScroll.js +0 -111
  151. package/modules/RestoreScroll.js.map +0 -1
  152. package/source/DOM/RenderingEngine.js +0 -22
  153. package/source/DOM/Screen.js +0 -51
  154. package/source/RestoreScroll.js +0 -86
@@ -1,4 +1,6 @@
1
- function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; var ownKeys = Object.keys(source); if (typeof Object.getOwnPropertySymbols === 'function') { ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) { return Object.getOwnPropertyDescriptor(source, sym).enumerable; })); } ownKeys.forEach(function (key) { _defineProperty(target, key, source[key]); }); } return target; }
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 DOMRenderingEngine from './DOM/RenderingEngine';
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 RestoreScroll from './RestoreScroll';
24
+ import ListHeightChangeWatcher from './ListHeightChangeWatcher';
23
25
  import ItemHeights from './ItemHeights';
24
26
  import _getItemsDiff from './getItemsDiff';
25
- import getVerticalSpacing from './getVerticalSpacing'; // import getItemCoordinates from './getItemCoordinates'
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} getContainerElement — Returns the container DOM `Element`.
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(getContainerElement, items) {
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.getTopOffset(_this.getContainerElement());
55
+ var listTopOffset = _this.scrollableContainer.getItemsContainerTopOffset();
48
56
 
49
- _this.waitForStylesToLoad.onGotListTopOffset(listTopOffset);
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 (newItems !== previousItems) {
91
- var layoutNeedsReCalculating = true;
108
+ if (isDebug()) {
109
+ log('State', getStateSnapshot(newState));
110
+ }
92
111
 
93
- var itemsDiff = _this.getItemsDiff(previousItems, newItems); // If it's an "incremental" update.
112
+ var layoutUpdateReason;
94
113
 
114
+ if (_this.firstNonMeasuredItemIndex !== undefined) {
115
+ layoutUpdateReason = LAYOUT_REASON.ACTUAL_ITEM_HEIGHTS_HAVE_BEEN_MEASURED;
116
+ }
95
117
 
96
- if (itemsDiff) {
97
- var prependedItemsCount = itemsDiff.prependedItemsCount,
98
- appendedItemsCount = itemsDiff.appendedItemsCount;
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
- if (_this.restoreScroll.shouldRestoreScrollAfterRender()) {
107
- layoutNeedsReCalculating = false;
108
- log('~ Restore Scroll Position ~');
134
+ var resetLayoutAfterResize = _this.resetLayoutAfterResize; // Reset `this.firstNonMeasuredItemIndex`.
109
135
 
110
- var scrollByY = _this.restoreScroll.getScrollDifference();
136
+ _this.firstNonMeasuredItemIndex = undefined; // Reset `this.resetLayoutAfterResize` flag.
111
137
 
112
- if (scrollByY) {
113
- log('Scroll down by', scrollByY);
138
+ _this.resetLayoutAfterResize = undefined; // Reset `this.newItemsWillBeRendered` flag.
114
139
 
115
- _this.scroll.scrollByY(scrollByY);
116
- } else {
117
- log('Scroll position hasn\'t changed');
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
- _this.itemHeights.initialize(_this.getState().itemHeights);
177
+
178
+ _this.itemHeights.initialize(newState.itemHeights);
125
179
  }
126
180
 
127
- if (layoutNeedsReCalculating) {
128
- redoLayoutReason = LAYOUT_REASON.ITEMS_CHANGED;
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
- } // Call `.onRendered()` if shown items configuration changed.
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.onRenderedNewLayout();
135
- }
207
+ if (newState.firstShownItemIndex !== prevState.firstShownItemIndex || newState.lastShownItemIndex !== prevState.lastShownItemIndex || newState.items !== prevState.items || resetLayoutAfterResize) {
208
+ var verticalSpacingStateUpdate = _this.measureItemHeightsAndSpacingAndUpdateTablePadding();
136
209
 
137
- if (redoLayoutReason) {
138
- return _this.redoLayoutRightAfterRender({
139
- reason: redoLayoutReason
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 visibleAreaIncludingMargins = _this.getVisibleAreaBoundsIncludingMargins();
219
+ var cleanedUpBeforeResize = _this.beforeResize.cleanUpBeforeResizeItemHeights(prevState);
148
220
 
149
- _this.latestLayoutVisibleAreaIncludingMargins = visibleAreaIncludingMargins;
221
+ if (cleanedUpBeforeResize !== undefined) {
222
+ var scrollBy = cleanedUpBeforeResize.scrollBy,
223
+ beforeResize = cleanedUpBeforeResize.beforeResize;
224
+ log('Correct scroll position by', scrollBy);
150
225
 
151
- var listTopOffsetInsideScrollableContainer = _this.getListTopOffsetInsideScrollableContainer(); // Get shown item indexes.
226
+ _this.scroll.scrollByY(scrollBy);
152
227
 
228
+ stateUpdate = _objectSpread(_objectSpread({}, stateUpdate), {}, {
229
+ beforeResize: beforeResize
230
+ });
231
+ }
153
232
 
154
- var _this$layout$getShown = _this.layout.getShownItemIndexes({
155
- listHeight: _this.screen.getElementHeight(_this.getContainerElement()),
156
- itemsCount: _this.getItemsCount(),
157
- visibleAreaIncludingMargins: visibleAreaIncludingMargins,
158
- listTopOffsetInsideScrollableContainer: listTopOffsetInsideScrollableContainer
159
- }),
160
- firstShownItemIndex = _this$layout$getShown.firstShownItemIndex,
161
- lastShownItemIndex = _this$layout$getShown.lastShownItemIndex,
162
- redoLayoutAfterMeasuringItemHeights = _this$layout$getShown.redoLayoutAfterMeasuringItemHeights; // If scroll position is scheduled to be restored after render,
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.restoreScroll.shouldRestoreScrollAfterRender()) {
170
- if (lastShownItemIndex < _this.restoreScroll.getAnchorItemIndex()) {
171
- lastShownItemIndex = _this.restoreScroll.getAnchorItemIndex();
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 `redoLayoutAfterMeasuringItemHeights` to `true`
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
- redoLayoutAfterMeasuringItemHeights = false;
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
- log('~ Some of the will-be-hidden item heights have changed since they\'ve last been measured. Redo layout. ~');
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, lastShownItemIndex); // Measure "after" items height.
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 afterItemsHeight = _this.layout.getAfterItemsHeight(firstShownItemIndex, lastShownItemIndex, _this.getItemsCount()); // Debugging.
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 (calculated on previous render)', _this.itemHeights.getAverage());
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); // Render.
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
- _this.setState({
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 // // Average item height is stored in state to differentiate between
240
- // // the initial state and "anything has been measured already" state.
241
- // averageItemHeight: this.itemHeights.getAverage()
242
-
243
- });
386
+ afterItemsHeight: afterItemsHeight
387
+ }, stateUpdate));
244
388
  });
245
389
 
246
- _defineProperty(this, "onUpdateShownItemIndexes", function (_ref) {
247
- var reason = _ref.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.onLayout(); // Cancel a re-layout that is scheduled to run at the next "frame",
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
- if (_this.layoutTimer) {
260
- clearTimeout(_this.layoutTimer);
261
- _this.layoutTimer = undefined;
262
- } // Perform a re-layout.
263
-
425
+ stateUpdate = _this.cancelLayoutTimer({
426
+ stateUpdate: stateUpdate
427
+ }); // Perform a re-layout.
264
428
 
265
- log("~ Calculate Layout (on ".concat(reason, ") ~"));
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 getState = options.getState,
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
- renderingEngine = options.renderingEngine;
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 (!renderingEngine) {
317
- renderingEngine = DOMRenderingEngine;
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.screen = renderingEngine.createScreen();
321
- this.scrollableContainer = renderingEngine.createScrollableContainer(scrollableContainer); // if (margin === undefined) {
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
- // margin = this.scrollableContainer ? this.scrollableContainer.getHeight() : 0
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 (renderingEngine.name !== 'DOM') {
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.margin = margin
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
- this.onItemInitialRender = function (item) {
389
- warn('`onItemFirstRender(i)` is deprecated, use `onItemInitialRender(item)` instead.');
390
-
391
- var _this$getState = _this.getState(),
392
- items = _this$getState.items;
393
-
394
- var i = items.indexOf(item); // The `item` could also be non-found due to the inconsistency bug:
395
- // The reason is that `i` can be non-consistent with the `items`
396
- // passed to `<VirtualScroller/>` in React due to `setState()` not being
397
- // instanteneous: when new `items` are passed to `<VirtualScroller/>`,
398
- // `VirtualScroller.setState({ items })` is called, and if `onItemFirstRender(i)`
399
- // is called after the aforementioned `setState()` is called but before it finishes,
400
- // `i` would point to an index in "previous" `items` while the application
401
- // would assume that `i` points to an index in the "new" `items`,
402
- // resulting in an incorrect item being assumed by the application
403
- // or even in an "array index out of bounds" error.
404
-
405
- if (i >= 0) {
406
- onItemFirstRender(i);
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
- if (setState) {
418
- this.getState = getState;
419
-
420
- this.setState = function (state) {
421
- log('Set state', state);
422
- setState(state, {
423
- willUpdateState: _this.willUpdateState,
424
- didUpdateState: _this.didUpdateState
425
- });
426
- };
427
- } else {
428
- this.getState = function () {
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
- this.setState = function (state) {
433
- log('Set state', state);
434
-
435
- var prevState = _this.getState(); // Because this variant of `.setState()` is "synchronous" (immediate),
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
- var newState = _objectSpread({}, prevState, state);
444
-
445
- _this.willUpdateState(newState, prevState);
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
- _this.state = newState;
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
- log('Initial state (passed)', state);
455
- } // Sometimes, when `new VirtualScroller()` instance is created,
456
- // `getContainerElement()` might not be ready to return the "container" DOM Element yet
457
- // (for example, because it's not rendered yet). That's the reason why it's a getter function.
458
- // For example, in React `<VirtualScroller/>` component, a `VirtualScroller`
459
- // instance is created in the React component's `constructor()`, and at that time
460
- // the container Element is not yet available. The container Element is available
461
- // in `componentDidMount()`, but `componentDidMount()` is not executed on server,
462
- // which would mean that React `<VirtualScroller/>` wouldn't render at all
463
- // on server side, while with the `getContainerElement()` approach, on server side,
464
- // it still "renders" a list with a predefined amount of items in it by default.
465
- // (`initiallyRenderedItemsCount`, or `1`).
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
- this.getContainerElement = getContainerElement; // Remove any accidental text nodes from container (like whitespace).
469
- // Also guards against cases when someone accidentally tries
470
- // using `VirtualScroller` on a non-empty element.
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.screen, this.getContainerElement, function (i) {
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
- getContainerElement: this.getContainerElement,
507
- updateLayout: function updateLayout(_ref2) {
508
- var reason = _ref2.reason;
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: reason
790
+ reason: LAYOUT_REASON.VIEWPORT_HEIGHT_CHANGED
511
791
  });
512
792
  },
513
- resetStateAndLayout: function resetStateAndLayout() {
514
- // Reset item heights, because if scrollable container's width (or height)
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.setState(_this.getInitialLayoutState(_this.newItemsPending || _this.getState().items));
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
- updateLayout: function updateLayout(_ref3) {
538
- var reason = _ref3.reason;
539
- return _this.onUpdateShownItemIndexes({
540
- reason: reason
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
- getLatestLayoutVisibleAreaIncludingMargins: function getLatestLayoutVisibleAreaIncludingMargins() {
555
- return _this.latestLayoutVisibleAreaIncludingMargins;
823
+ getLatestLayoutVisibleArea: function getLatestLayoutVisibleArea() {
824
+ return _this.latestLayoutVisibleArea;
556
825
  },
557
- preserveScrollPositionOfTheBottomOfTheListOnMount: this.preserveScrollPositionOfTheBottomOfTheListOnMount
826
+ getListTopOffset: this.getListTopOffsetInsideScrollableContainer,
827
+ getPrerenderMargin: function getPrerenderMargin() {
828
+ return _this.getPrerenderMargin();
829
+ }
558
830
  });
559
- this.restoreScroll = new RestoreScroll({
560
- screen: this.screen,
561
- getContainerElement: this.getContainerElement
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
- log('Initial state (autogenerated)', state);
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$getInitialLayou = this.getInitialLayoutValues({
902
+ var _this$layout$getIniti = this.layout.getInitialLayoutValues({
613
903
  itemsCount: itemsCount,
614
- bypass: this.preserveScrollPositionOfTheBottomOfTheListOnMount
904
+ columnsCount: this.getActualColumnsCount()
615
905
  }),
616
- firstShownItemIndex = _this$getInitialLayou.firstShownItemIndex,
617
- lastShownItemIndex = _this$getInitialLayou.lastShownItemIndex,
618
- beforeItemsHeight = _this$getInitialLayou.beforeItemsHeight,
619
- afterItemsHeight = _this$getInitialLayou.afterItemsHeight;
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); // This "initial" state object must include all possible state properties
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._getColumnsCount ? this._getColumnsCount(this.scrollableContainer) : undefined,
632
- verticalSpacing: undefined,
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: "getVerticalSpacing",
641
- value: function getVerticalSpacing() {
642
- return this.getState() && this.getState().verticalSpacing || 0;
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: "getMargin",
656
- value: function getMargin() {
657
- // `VirtualScroller` also items that are outside of the screen
658
- // by the amount of this "render ahead margin" (both on top and bottom).
659
- // The default "render ahead margin" is equal to the screen height:
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 of a screen, and when they stop scrolling,
663
- // the list is re-rendered).
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.onRenderedNewLayout();
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.getContainerElement());
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
- if (this.preserveScrollPositionOfTheBottomOfTheListOnMount) {// In this case, all items are shown, so there's no need to call
729
- // `this.onUpdateShownItemIndexes()` after the initial render.
730
- } else {
731
- this.onUpdateShownItemIndexes({
732
- reason: LAYOUT_REASON.MOUNT
733
- });
734
- }
1035
+
1036
+ this.onUpdateShownItemIndexes({
1037
+ reason: LAYOUT_REASON.MOUNTED,
1038
+ stateUpdate: stateUpdate
1039
+ });
735
1040
  }
736
1041
  }, {
737
- key: "onRenderedNewLayout",
738
- value: function onRenderedNewLayout() {
739
- // Update item vertical spacing.
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
- this.itemHeights.measureItemHeights(this.getState().firstShownItemIndex, this.getState().lastShownItemIndex); // Update `<tbody/>` `padding`.
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.getContainerElement(), this.getState().beforeItemsHeight, this.getState().afterItemsHeight);
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: "getVisibleAreaBoundsIncludingMargins",
753
- value: function getVisibleAreaBoundsIncludingMargins() {
1064
+ key: "getVisibleArea",
1065
+ value: function getVisibleArea() {
754
1066
  var visibleArea = this.scroll.getVisibleAreaBounds();
755
- visibleArea.top -= this.getMargin();
756
- visibleArea.bottom += this.getMargin();
757
- return visibleArea;
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: "stop",
778
- value: function stop() {
779
- this.isRendered = false;
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: "redoLayoutRightAfterRender",
797
- value: function redoLayoutRightAfterRender(_ref6) {
798
- var _this2 = this;
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
- var reason = _ref6.reason;
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
- if (this.layoutTimer) {
810
- clearTimeout(this.layoutTimer);
811
- } // Schedule a new re-layout.
812
-
1269
+ stateUpdate = this.cancelLayoutTimer({
1270
+ stateUpdate: stateUpdate
1271
+ }); // Schedule a new re-layout.
813
1272
 
814
- this.layoutTimer = setTimeout(function () {
815
- _this2.layoutTimer = undefined;
816
-
817
- _this2.onUpdateShownItemIndexes({
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.getState().verticalSpacing === undefined) {
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
- container: this.getContainerElement(),
834
- screen: this.screen
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.setState({
842
- verticalSpacing: verticalSpacing
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$getState2 = this.getState(),
851
- firstShownItemIndex = _this$getState2.firstShownItemIndex;
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, itemState) {
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(itemState, null, 2));
1330
+ log('New state' + '\n' + JSON.stringify(newItemState, null, 2));
863
1331
  }
864
1332
 
865
- this.getState().itemStates[i] = itemState;
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$getState3 = this.getState(),
874
- itemHeights = _this$getState3.itemHeights;
875
-
876
- var previousHeight = itemHeights[i];
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 (newHeight === undefined) {
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 ~'); // log('Item', i)
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 at next render. Its height has changed from', previouslyMeasuredItemHeight, 'to', actualItemHeight, 'since it was last measured. This is not necessarily a bug, and could happen, for example, 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.');
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
- * `redoLayoutAfterMeasuringItemHeights` is `true`.
1554
+ * `firstNonMeasuredItemIndex` is defined.
992
1555
  * If the list is visible and all items being shown have been encountered
993
- * (and measured) before, then `redoLayoutAfterMeasuringItemHeights` is `false`.
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
- value: function updateItems(newItems, options) {
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$getState4 = this.getState(),
1019
- previousItems = _this$getState4.items;
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
- var _this$getState5 = this.getState(),
1022
- itemStates = _this$getState5.itemStates,
1023
- itemHeights = _this$getState5.itemHeights;
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 layout;
1027
- var itemsDiff = this.getItemsDiff(previousItems, newItems); // If it's an "incremental" update.
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 && !this.layoutResetPending) {
1030
- var _this$getState6 = this.getState(),
1031
- firstShownItemIndex = _this$getState6.firstShownItemIndex,
1032
- lastShownItemIndex = _this$getState6.lastShownItemIndex,
1033
- beforeItemsHeight = _this$getState6.beforeItemsHeight,
1034
- afterItemsHeight = _this$getState6.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
- layout = {
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
- var prependedItemsCount = itemsDiff.prependedItemsCount,
1043
- appendedItemsCount = itemsDiff.appendedItemsCount;
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
- this.layout.updateLayoutForItemsDiff(layout, itemsDiff, {
1064
- itemsCount: newItems.length
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
- layout = this.getInitialLayoutValues({
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
- log('First shown item index', layout.firstShownItemIndex);
1094
- log('Last shown item index', layout.lastShownItemIndex);
1095
- log('Before items height', layout.beforeItemsHeight);
1096
- log('After items height (actual or estimated)', layout.afterItemsHeight); // Optionally preload items to be rendered.
1097
-
1098
- this.onBeforeShowItems(newItems, itemHeights, layout.firstShownItemIndex, layout.lastShownItemIndex); // `this.newItemsPending` will be cleared in `didUpdateState()`.
1099
-
1100
- this.newItemsPending = newItems; // Update state.
1101
-
1102
- this.setState(_objectSpread({}, layout, {
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