virtual-scroller 1.7.6 → 1.8.0

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