virtual-scroller 1.8.0 → 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. package/.gitlab-ci.yml +1 -1
  2. package/CHANGELOG.md +57 -0
  3. package/README.md +337 -160
  4. package/bundle/virtual-scroller-dom.js +1 -1
  5. package/bundle/virtual-scroller-dom.js.map +1 -1
  6. package/bundle/virtual-scroller-react.js +1 -1
  7. package/bundle/virtual-scroller-react.js.map +1 -1
  8. package/bundle/virtual-scroller.js +1 -1
  9. package/bundle/virtual-scroller.js.map +1 -1
  10. package/commonjs/BeforeResize.js +23 -27
  11. package/commonjs/BeforeResize.js.map +1 -1
  12. package/commonjs/DOM/Engine.js +7 -7
  13. package/commonjs/DOM/Engine.js.map +1 -1
  14. package/commonjs/DOM/ItemsContainer.js +1 -1
  15. package/commonjs/DOM/ItemsContainer.js.map +1 -1
  16. package/commonjs/DOM/ListTopOffsetWatcher.js +15 -9
  17. package/commonjs/DOM/ListTopOffsetWatcher.js.map +1 -1
  18. package/commonjs/DOM/ScrollableContainer.js +28 -28
  19. package/commonjs/DOM/ScrollableContainer.js.map +1 -1
  20. package/commonjs/DOM/VirtualScroller.js +20 -17
  21. package/commonjs/DOM/VirtualScroller.js.map +1 -1
  22. package/commonjs/DOM/tbody.js +16 -10
  23. package/commonjs/DOM/tbody.js.map +1 -1
  24. package/commonjs/ItemHeights.js +23 -17
  25. package/commonjs/ItemHeights.js.map +1 -1
  26. package/commonjs/Layout.js +15 -13
  27. package/commonjs/Layout.js.map +1 -1
  28. package/commonjs/Layout.test.js +8 -3
  29. package/commonjs/Layout.test.js.map +1 -1
  30. package/commonjs/{ListHeightChangeWatcher.js → ListHeightMeasurement.js} +26 -28
  31. package/commonjs/ListHeightMeasurement.js.map +1 -0
  32. package/commonjs/Resize.js +38 -28
  33. package/commonjs/Resize.js.map +1 -1
  34. package/commonjs/Scroll.js +28 -44
  35. package/commonjs/Scroll.js.map +1 -1
  36. package/commonjs/VirtualScroller.columns.js +43 -0
  37. package/commonjs/VirtualScroller.columns.js.map +1 -0
  38. package/commonjs/VirtualScroller.constructor.js +408 -0
  39. package/commonjs/VirtualScroller.constructor.js.map +1 -0
  40. package/commonjs/VirtualScroller.items.js +305 -0
  41. package/commonjs/VirtualScroller.items.js.map +1 -0
  42. package/commonjs/VirtualScroller.js +132 -1872
  43. package/commonjs/VirtualScroller.js.map +1 -1
  44. package/commonjs/VirtualScroller.layout.js +562 -0
  45. package/commonjs/VirtualScroller.layout.js.map +1 -0
  46. package/commonjs/VirtualScroller.onRender.js +357 -0
  47. package/commonjs/VirtualScroller.onRender.js.map +1 -0
  48. package/commonjs/VirtualScroller.resize.js +186 -0
  49. package/commonjs/VirtualScroller.resize.js.map +1 -0
  50. package/commonjs/VirtualScroller.state.js +301 -0
  51. package/commonjs/VirtualScroller.state.js.map +1 -0
  52. package/commonjs/VirtualScroller.verticalSpacing.js +65 -0
  53. package/commonjs/VirtualScroller.verticalSpacing.js.map +1 -0
  54. package/commonjs/getItemCoordinates.js.map +1 -1
  55. package/commonjs/getItemsDiff.js.map +1 -1
  56. package/commonjs/getVerticalSpacing.js.map +1 -1
  57. package/commonjs/package.json +5 -0
  58. package/commonjs/react/VirtualScroller.js +184 -618
  59. package/commonjs/react/VirtualScroller.js.map +1 -1
  60. package/commonjs/react/useClassName.js +26 -0
  61. package/commonjs/react/useClassName.js.map +1 -0
  62. package/commonjs/react/useHandleItemsChange.js +116 -0
  63. package/commonjs/react/useHandleItemsChange.js.map +1 -0
  64. package/commonjs/react/useInstanceMethods.js +37 -0
  65. package/commonjs/react/useInstanceMethods.js.map +1 -0
  66. package/commonjs/react/useItemKeys.js +60 -0
  67. package/commonjs/react/useItemKeys.js.map +1 -0
  68. package/commonjs/react/useOnItemHeightChange.js +32 -0
  69. package/commonjs/react/useOnItemHeightChange.js.map +1 -0
  70. package/commonjs/react/useOnItemStateChange.js +32 -0
  71. package/commonjs/react/useOnItemStateChange.js.map +1 -0
  72. package/commonjs/react/useState.js +140 -0
  73. package/commonjs/react/useState.js.map +1 -0
  74. package/commonjs/react/useStyle.js +29 -0
  75. package/commonjs/react/useStyle.js.map +1 -0
  76. package/commonjs/react/useVirtualScroller.js +62 -0
  77. package/commonjs/react/useVirtualScroller.js.map +1 -0
  78. package/commonjs/react/useVirtualScrollerStartStop.js +20 -0
  79. package/commonjs/react/useVirtualScrollerStartStop.js.map +1 -0
  80. package/commonjs/test/Engine.js +23 -0
  81. package/commonjs/test/Engine.js.map +1 -0
  82. package/commonjs/test/ItemsContainer.js +127 -0
  83. package/commonjs/test/ItemsContainer.js.map +1 -0
  84. package/commonjs/test/ScrollableContainer.js +130 -0
  85. package/commonjs/test/ScrollableContainer.js.map +1 -0
  86. package/commonjs/test/VirtualScroller.js +281 -0
  87. package/commonjs/test/VirtualScroller.js.map +1 -0
  88. package/commonjs/utility/debounce.js +2 -2
  89. package/commonjs/utility/debounce.js.map +1 -1
  90. package/commonjs/utility/debug.js.map +1 -1
  91. package/commonjs/utility/getStateSnapshot.js +2 -2
  92. package/commonjs/utility/getStateSnapshot.js.map +1 -1
  93. package/commonjs/utility/px.js.map +1 -1
  94. package/commonjs/utility/px.test.js +1 -1
  95. package/commonjs/utility/px.test.js.map +1 -1
  96. package/commonjs/utility/shallowEqual.js +1 -1
  97. package/commonjs/utility/shallowEqual.js.map +1 -1
  98. package/commonjs/utility/throttle.js.map +1 -1
  99. package/dom/index.cjs +4 -0
  100. package/dom/index.cjs.js +9 -0
  101. package/dom/index.d.ts +6 -4
  102. package/dom/index.js +1 -1
  103. package/dom/package.json +10 -4
  104. package/index.cjs +4 -0
  105. package/index.cjs.js +9 -0
  106. package/index.d.ts +30 -15
  107. package/index.js +1 -1
  108. package/modules/BeforeResize.js +22 -27
  109. package/modules/BeforeResize.js.map +1 -1
  110. package/modules/DOM/Engine.js +6 -6
  111. package/modules/DOM/Engine.js.map +1 -1
  112. package/modules/DOM/ItemsContainer.js +1 -1
  113. package/modules/DOM/ItemsContainer.js.map +1 -1
  114. package/modules/DOM/ListTopOffsetWatcher.js +15 -9
  115. package/modules/DOM/ListTopOffsetWatcher.js.map +1 -1
  116. package/modules/DOM/ScrollableContainer.js +28 -28
  117. package/modules/DOM/ScrollableContainer.js.map +1 -1
  118. package/modules/DOM/VirtualScroller.js +19 -16
  119. package/modules/DOM/VirtualScroller.js.map +1 -1
  120. package/modules/DOM/tbody.js +11 -9
  121. package/modules/DOM/tbody.js.map +1 -1
  122. package/modules/ItemHeights.js +22 -16
  123. package/modules/ItemHeights.js.map +1 -1
  124. package/modules/Layout.js +14 -12
  125. package/modules/Layout.js.map +1 -1
  126. package/modules/Layout.test.js +8 -3
  127. package/modules/Layout.test.js.map +1 -1
  128. package/modules/{ListHeightChangeWatcher.js → ListHeightMeasurement.js} +25 -27
  129. package/modules/ListHeightMeasurement.js.map +1 -0
  130. package/modules/Resize.js +38 -28
  131. package/modules/Resize.js.map +1 -1
  132. package/modules/Scroll.js +28 -44
  133. package/modules/Scroll.js.map +1 -1
  134. package/modules/VirtualScroller.columns.js +36 -0
  135. package/modules/VirtualScroller.columns.js.map +1 -0
  136. package/modules/VirtualScroller.constructor.js +371 -0
  137. package/modules/VirtualScroller.constructor.js.map +1 -0
  138. package/modules/VirtualScroller.items.js +288 -0
  139. package/modules/VirtualScroller.items.js.map +1 -0
  140. package/modules/VirtualScroller.js +132 -1860
  141. package/modules/VirtualScroller.js.map +1 -1
  142. package/modules/VirtualScroller.layout.js +549 -0
  143. package/modules/VirtualScroller.layout.js.map +1 -0
  144. package/modules/VirtualScroller.onRender.js +337 -0
  145. package/modules/VirtualScroller.onRender.js.map +1 -0
  146. package/modules/VirtualScroller.resize.js +176 -0
  147. package/modules/VirtualScroller.resize.js.map +1 -0
  148. package/modules/VirtualScroller.state.js +283 -0
  149. package/modules/VirtualScroller.state.js.map +1 -0
  150. package/modules/VirtualScroller.verticalSpacing.js +54 -0
  151. package/modules/VirtualScroller.verticalSpacing.js.map +1 -0
  152. package/modules/getItemCoordinates.js.map +1 -1
  153. package/modules/getItemsDiff.js.map +1 -1
  154. package/modules/getVerticalSpacing.js.map +1 -1
  155. package/modules/react/VirtualScroller.js +176 -625
  156. package/modules/react/VirtualScroller.js.map +1 -1
  157. package/modules/react/useClassName.js +18 -0
  158. package/modules/react/useClassName.js.map +1 -0
  159. package/modules/react/useHandleItemsChange.js +108 -0
  160. package/modules/react/useHandleItemsChange.js.map +1 -0
  161. package/modules/react/useInstanceMethods.js +28 -0
  162. package/modules/react/useInstanceMethods.js.map +1 -0
  163. package/modules/react/useItemKeys.js +52 -0
  164. package/modules/react/useItemKeys.js.map +1 -0
  165. package/modules/react/useOnItemHeightChange.js +24 -0
  166. package/modules/react/useOnItemHeightChange.js.map +1 -0
  167. package/modules/react/useOnItemStateChange.js +24 -0
  168. package/modules/react/useOnItemStateChange.js.map +1 -0
  169. package/modules/react/useState.js +132 -0
  170. package/modules/react/useState.js.map +1 -0
  171. package/modules/react/useStyle.js +19 -0
  172. package/modules/react/useStyle.js.map +1 -0
  173. package/modules/react/useVirtualScroller.js +51 -0
  174. package/modules/react/useVirtualScroller.js.map +1 -0
  175. package/modules/react/useVirtualScrollerStartStop.js +12 -0
  176. package/modules/react/useVirtualScrollerStartStop.js.map +1 -0
  177. package/modules/test/Engine.js +11 -0
  178. package/modules/test/Engine.js.map +1 -0
  179. package/modules/test/ItemsContainer.js +120 -0
  180. package/modules/test/ItemsContainer.js.map +1 -0
  181. package/modules/test/ScrollableContainer.js +123 -0
  182. package/modules/test/ScrollableContainer.js.map +1 -0
  183. package/modules/test/VirtualScroller.js +270 -0
  184. package/modules/test/VirtualScroller.js.map +1 -0
  185. package/modules/utility/debounce.js +2 -2
  186. package/modules/utility/debounce.js.map +1 -1
  187. package/modules/utility/debug.js.map +1 -1
  188. package/modules/utility/getStateSnapshot.js +2 -2
  189. package/modules/utility/getStateSnapshot.js.map +1 -1
  190. package/modules/utility/px.js.map +1 -1
  191. package/modules/utility/px.test.js +1 -1
  192. package/modules/utility/px.test.js.map +1 -1
  193. package/modules/utility/shallowEqual.js +1 -1
  194. package/modules/utility/shallowEqual.js.map +1 -1
  195. package/modules/utility/throttle.js.map +1 -1
  196. package/package.json +46 -23
  197. package/react/index.cjs +4 -0
  198. package/react/index.cjs.js +9 -0
  199. package/react/index.d.ts +10 -9
  200. package/react/index.js +1 -1
  201. package/react/package.json +10 -4
  202. package/rollup.config.mjs +62 -0
  203. package/runnable/create-commonjs-package-json.js +11 -0
  204. package/source/BeforeResize.js +16 -21
  205. package/source/DOM/Engine.js +8 -10
  206. package/source/DOM/ListTopOffsetWatcher.js +13 -8
  207. package/source/DOM/ScrollableContainer.js +23 -21
  208. package/source/DOM/VirtualScroller.js +27 -11
  209. package/source/DOM/tbody.js +30 -21
  210. package/source/ItemHeights.js +19 -14
  211. package/source/Layout.js +12 -9
  212. package/source/Layout.test.js +8 -3
  213. package/source/{ListHeightChangeWatcher.js → ListHeightMeasurement.js} +21 -20
  214. package/source/Resize.js +41 -25
  215. package/source/Scroll.js +27 -35
  216. package/source/VirtualScroller.columns.js +26 -0
  217. package/source/VirtualScroller.constructor.js +336 -0
  218. package/source/VirtualScroller.items.js +302 -0
  219. package/source/VirtualScroller.js +144 -1872
  220. package/source/VirtualScroller.layout.js +539 -0
  221. package/source/VirtualScroller.onRender.js +345 -0
  222. package/source/VirtualScroller.resize.js +189 -0
  223. package/source/VirtualScroller.state.js +284 -0
  224. package/source/VirtualScroller.verticalSpacing.js +51 -0
  225. package/source/react/VirtualScroller.js +243 -587
  226. package/source/react/useClassName.js +14 -0
  227. package/source/react/useHandleItemsChange.js +115 -0
  228. package/source/react/useInstanceMethods.js +25 -0
  229. package/source/react/useItemKeys.js +59 -0
  230. package/source/react/useOnItemHeightChange.js +28 -0
  231. package/source/react/useOnItemStateChange.js +28 -0
  232. package/source/react/useState.js +114 -0
  233. package/source/react/useStyle.js +20 -0
  234. package/source/react/useVirtualScroller.js +59 -0
  235. package/source/react/useVirtualScrollerStartStop.js +12 -0
  236. package/source/test/Engine.js +11 -0
  237. package/source/test/ItemsContainer.js +87 -0
  238. package/source/test/ScrollableContainer.js +88 -0
  239. package/source/test/VirtualScroller.js +232 -0
  240. package/source/utility/debounce.js +2 -2
  241. package/source/utility/px.test.js +1 -1
  242. package/babel.config.js +0 -25
  243. package/babel.js +0 -5
  244. package/commonjs/ListHeightChangeWatcher.js.map +0 -1
  245. package/dom/index.commonjs.js +0 -4
  246. package/index.commonjs.js +0 -4
  247. package/modules/ListHeightChangeWatcher.js.map +0 -1
  248. package/react/index.commonjs.js +0 -4
@@ -0,0 +1,345 @@
1
+ import log, { warn, isDebug } from './utility/debug.js'
2
+ import getStateSnapshot from './utility/getStateSnapshot.js'
3
+ import shallowEqual from './utility/shallowEqual.js'
4
+ import { LAYOUT_REASON } from './Layout.js'
5
+ import { setTbodyPadding } from './DOM/tbody.js'
6
+
7
+ export default function() {
8
+ /**
9
+ * Should be called right after updates to `state` have been rendered.
10
+ * @param {object} newState
11
+ * @param {object} [prevState]
12
+ */
13
+ this._onRender = (newState, prevState) => {
14
+ log('~ Rendered ~')
15
+ if (isDebug()) {
16
+ log('State', getStateSnapshot(newState))
17
+ }
18
+
19
+ if (this.onStateChange) {
20
+ if (!shallowEqual(newState, prevState)) {
21
+ this.onStateChange(newState)
22
+ }
23
+ }
24
+
25
+ // Update `<tbody/>` `padding`.
26
+ // (`<tbody/>` is different in a way that it can't have `margin`, only `padding`).
27
+ // https://gitlab.com/catamphetamine/virtual-scroller/-/issues/1
28
+ if (this.tbody) {
29
+ setTbodyPadding(
30
+ this.getItemsContainerElement(),
31
+ newState.beforeItemsHeight,
32
+ newState.afterItemsHeight
33
+ )
34
+ }
35
+
36
+ if (!prevState) {
37
+ return
38
+ }
39
+
40
+ // `this.resetStateUpdateFlags()` must be called before calling
41
+ // `this.measureItemHeightsAndSpacing()`.
42
+ const {
43
+ nonMeasuredItemsHaveBeenRendered,
44
+ widthHasChanged
45
+ } = resetStateUpdateFlags.call(this)
46
+
47
+ let layoutUpdateReason
48
+
49
+ // If the `VirtualScroller`, while calculating layout parameters, encounters
50
+ // a not-shown item with a non-measured height, it calls `updateState()` just to
51
+ // render that item first, and then, after the list has been re-rendered, it measures
52
+ // the item's height and then proceeds with calculating the correct layout parameters.
53
+ if (nonMeasuredItemsHaveBeenRendered) {
54
+ layoutUpdateReason = LAYOUT_REASON.NON_MEASURED_ITEMS_HAVE_BEEN_MEASURED
55
+ }
56
+
57
+ // If scrollable container width has changed, and it has been re-rendered,
58
+ // then it's time to measure the new item heights and then perform a re-layout
59
+ // with the correctly calculated layout parameters.
60
+ //
61
+ // A re-layout is required because the layout parameters calculated on resize
62
+ // are approximate ones, and the exact item heights aren't known at that point.
63
+ // So on resize, it calls `updateState()` just to re-render the `VirtualScroller`.
64
+ // After it has been re-rendered, it will measure item heights and then calculate
65
+ // correct layout parameters.
66
+ //
67
+ if (widthHasChanged) {
68
+ layoutUpdateReason = LAYOUT_REASON.VIEWPORT_WIDTH_CHANGED
69
+
70
+ // Reset measured item heights on viewport width change.
71
+ this.itemHeights.reset()
72
+
73
+ // Reset `verticalSpacing` (will be re-measured).
74
+ this.verticalSpacing = undefined
75
+ }
76
+
77
+ const { items: previousItems } = prevState
78
+ const { items: newItems } = newState
79
+ // Even if `this.newItemsWillBeRendered` flag is `true`,
80
+ // `newItems` could still be equal to `previousItems`.
81
+ // For example, when `updateState()` calls don't update `state` immediately
82
+ // and a developer first calls `setItems(newItems)` and then calls `setItems(oldItems)`:
83
+ // in that case, `this.newItemsWillBeRendered` flag will be `true` but the actual `items`
84
+ // in state wouldn't have changed due to the first `updateState()` call being overwritten
85
+ // by the second `updateState()` call (that's called "batching state updates" in React).
86
+ if (newItems !== previousItems) {
87
+ const itemsDiff = this.getItemsDiff(previousItems, newItems)
88
+ if (itemsDiff) {
89
+ // The call to `.onPrepend()` must precede the call to `.measureItemHeights()`
90
+ // which is called in `.onRender()`.
91
+ // `this.itemHeights.onPrepend()` updates `firstMeasuredItemIndex`
92
+ // and `lastMeasuredItemIndex` of `this.itemHeights`.
93
+ const { prependedItemsCount } = itemsDiff
94
+ this.itemHeights.onPrepend(prependedItemsCount)
95
+ } else {
96
+ this.itemHeights.reset()
97
+ }
98
+
99
+ if (!widthHasChanged) {
100
+ // The call to `this.onNewItemsRendered()` must precede the call to
101
+ // `.measureItemHeights()` which is called in `.onRender()` because
102
+ // `this.onNewItemsRendered()` updates `firstMeasuredItemIndex` and
103
+ // `lastMeasuredItemIndex` of `this.itemHeights` in case of a prepend.
104
+ //
105
+ // If after prepending items the scroll position
106
+ // should be "restored" so that there's no "jump" of content
107
+ // then it means that all previous items have just been rendered
108
+ // in a single pass, and there's no need to update layout again.
109
+ //
110
+ if (onNewItemsRendered.call(this, itemsDiff, newState) !== 'SEAMLESS_PREPEND') {
111
+ layoutUpdateReason = LAYOUT_REASON.ITEMS_CHANGED
112
+ }
113
+ }
114
+ }
115
+
116
+ let stateUpdate
117
+
118
+ // Re-measure item heights.
119
+ // Also, measure vertical spacing (if not measured) and fix `<table/>` padding.
120
+ //
121
+ // This block should go after `if (newItems !== previousItems) {}`
122
+ // because `this.itemHeights` can get `.reset()` there, which would
123
+ // discard all the measurements done here, and having currently shown
124
+ // item height measurements is required.
125
+ //
126
+ if (
127
+ newState.firstShownItemIndex !== prevState.firstShownItemIndex ||
128
+ newState.lastShownItemIndex !== prevState.lastShownItemIndex ||
129
+ newState.items !== prevState.items ||
130
+ widthHasChanged
131
+ ) {
132
+ const verticalSpacingStateUpdate = this.measureItemHeightsAndSpacing()
133
+ if (verticalSpacingStateUpdate) {
134
+ stateUpdate = {
135
+ ...stateUpdate,
136
+ ...verticalSpacingStateUpdate
137
+ }
138
+ }
139
+ }
140
+
141
+ // Clean up "before resize" item heights and adjust the scroll position accordingly.
142
+ // Calling `this.beforeResize.cleanUpBeforeResizeItemHeights()` might trigger
143
+ // a `this.updateState()` call but that wouldn't matter because `beforeResize`
144
+ // properties have already been modified directly in `state` (a hacky technique)
145
+ const cleanedUpBeforeResize = this.beforeResize.cleanUpBeforeResizeItemHeights()
146
+ if (cleanedUpBeforeResize !== undefined) {
147
+ const { scrollBy, beforeResize } = cleanedUpBeforeResize
148
+ log('Correct scroll position by', scrollBy)
149
+ this.scroll.scrollByY(scrollBy)
150
+ stateUpdate = {
151
+ ...stateUpdate,
152
+ beforeResize
153
+ }
154
+ }
155
+
156
+ if (!this._isActive) {
157
+ this._stoppedStateUpdate = stateUpdate
158
+ return
159
+ }
160
+
161
+ if (layoutUpdateReason) {
162
+ updateStateRightAfterRender.call(this, {
163
+ stateUpdate,
164
+ reason: layoutUpdateReason
165
+ })
166
+ } else if (stateUpdate) {
167
+ this.updateState(stateUpdate)
168
+ } else {
169
+ log('~ Finished Layout ~')
170
+ }
171
+ }
172
+
173
+ // After a new set of items has been rendered:
174
+ //
175
+ // * Restores scroll position when using `preserveScrollPositionOnPrependItems`
176
+ // and items have been prepended.
177
+ //
178
+ // * Applies any "pending" `itemHeights` updates — those ones that happened
179
+ // while an asynchronous `updateState()` call in `setItems()` was pending.
180
+ //
181
+ // * Either creates or resets the snapshot of the current layout.
182
+ //
183
+ // The current layout snapshot could be stored as a "previously calculated layout" variable
184
+ // so that it could theoretically be used when calculating new layout incrementally
185
+ // rather than from scratch, which would be an optimization.
186
+ //
187
+ // The "previously calculated layout" feature is not currently used.
188
+ //
189
+ function onNewItemsRendered(itemsDiff, newLayout) {
190
+ // If it's an "incremental" update.
191
+ if (itemsDiff) {
192
+ const {
193
+ prependedItemsCount,
194
+ appendedItemsCount
195
+ } = itemsDiff
196
+
197
+ const {
198
+ itemHeights,
199
+ itemStates
200
+ } = this.getState()
201
+
202
+ // See if any items' heights changed while new items were being rendered.
203
+ if (this.itemHeightsThatChangedWhileNewItemsWereBeingRendered) {
204
+ for (const i of Object.keys(this.itemHeightsThatChangedWhileNewItemsWereBeingRendered)) {
205
+ itemHeights[prependedItemsCount + parseInt(i)] = this.itemHeightsThatChangedWhileNewItemsWereBeingRendered[i]
206
+ }
207
+ }
208
+
209
+ // See if any items' states changed while new items were being rendered.
210
+ if (this.itemStatesThatChangedWhileNewItemsWereBeingRendered) {
211
+ for (const i of Object.keys(this.itemStatesThatChangedWhileNewItemsWereBeingRendered)) {
212
+ itemStates[prependedItemsCount + parseInt(i)] = this.itemStatesThatChangedWhileNewItemsWereBeingRendered[i]
213
+ }
214
+ }
215
+
216
+ if (prependedItemsCount === 0) {
217
+ // Adjust `this.previouslyCalculatedLayout`.
218
+ if (this.previouslyCalculatedLayout) {
219
+ if (
220
+ this.previouslyCalculatedLayout.firstShownItemIndex === newLayout.firstShownItemIndex &&
221
+ this.previouslyCalculatedLayout.lastShownItemIndex === newLayout.lastShownItemIndex
222
+ ) {
223
+ // `this.previouslyCalculatedLayout` stays the same.
224
+ // `firstShownItemIndex` / `lastShownItemIndex` didn't get changed in `setItems()`,
225
+ // so `beforeItemsHeight` and `shownItemsHeight` also stayed the same.
226
+ } else {
227
+ warn('Unexpected (non-matching) "firstShownItemIndex" or "lastShownItemIndex" encountered in "onRender()" after appending items')
228
+ warn('Previously calculated layout', this.previouslyCalculatedLayout)
229
+ warn('New layout', newLayout)
230
+ this.previouslyCalculatedLayout = undefined
231
+ }
232
+ }
233
+ return 'SEAMLESS_APPEND'
234
+ } else {
235
+ if (this.listHeightMeasurement.hasSnapshot()) {
236
+ if (newLayout.firstShownItemIndex === 0) {
237
+ // Restore (adjust) scroll position.
238
+ log('~ Restore Scroll Position ~')
239
+ const listBottomOffsetChange = this.listHeightMeasurement.getListBottomOffsetChange({
240
+ beforeItemsHeight: newLayout.beforeItemsHeight
241
+ })
242
+ this.listHeightMeasurement.reset()
243
+ if (listBottomOffsetChange) {
244
+ log('Scroll down by', listBottomOffsetChange)
245
+ this.scroll.scrollByY(listBottomOffsetChange)
246
+ } else {
247
+ log('Scroll position hasn\'t changed')
248
+ }
249
+ // Create new `this.previouslyCalculatedLayout`.
250
+ if (this.previouslyCalculatedLayout) {
251
+ if (
252
+ this.previouslyCalculatedLayout.firstShownItemIndex === 0 &&
253
+ this.previouslyCalculatedLayout.lastShownItemIndex === newLayout.lastShownItemIndex - prependedItemsCount
254
+ ) {
255
+ this.previouslyCalculatedLayout = {
256
+ beforeItemsHeight: 0,
257
+ shownItemsHeight: this.previouslyCalculatedLayout.shownItemsHeight + listBottomOffsetChange,
258
+ firstShownItemIndex: 0,
259
+ lastShownItemIndex: newLayout.lastShownItemIndex
260
+ }
261
+ } else {
262
+ warn('Unexpected (non-matching) "firstShownItemIndex" or "lastShownItemIndex" encountered in "onRender()" after prepending items')
263
+ warn('Previously calculated layout', this.previouslyCalculatedLayout)
264
+ warn('New layout', newLayout)
265
+ this.previouslyCalculatedLayout = undefined
266
+ }
267
+ }
268
+ return 'SEAMLESS_PREPEND'
269
+ } else {
270
+ warn(`Unexpected "firstShownItemIndex" ${newLayout.firstShownItemIndex} encountered in "onRender()" after prepending items. Expected 0.`)
271
+ }
272
+ }
273
+ }
274
+ }
275
+
276
+ // Reset `this.previouslyCalculatedLayout` in any case other than
277
+ // SEAMLESS_PREPEND or SEAMLESS_APPEND.
278
+ this.previouslyCalculatedLayout = undefined
279
+ }
280
+
281
+ function updateStateRightAfterRender({
282
+ reason,
283
+ stateUpdate
284
+ }) {
285
+ // In React, `setTimeout()` is used to prevent a React error:
286
+ // "Maximum update depth exceeded.
287
+ // This can happen when a component repeatedly calls
288
+ // `.updateState()` inside `componentWillUpdate()` or `componentDidUpdate()`.
289
+ // React limits the number of nested updates to prevent infinite loops."
290
+ if (this._useTimeoutInRenderLoop) {
291
+ // Cancel a previously scheduled re-layout.
292
+ stateUpdate = this.cancelLayoutTimer({ stateUpdate })
293
+ // Schedule a new re-layout.
294
+ this.scheduleLayoutTimer({
295
+ reason,
296
+ stateUpdate
297
+ })
298
+ } else {
299
+ this.onUpdateShownItemIndexes({
300
+ reason,
301
+ stateUpdate
302
+ })
303
+ }
304
+ }
305
+
306
+ function resetStateUpdateFlags() {
307
+ // Read and reset `this.widthHasChanged` flag.
308
+ //
309
+ // If `this.widthHasChanged` flag was reset after calling
310
+ // `this.measureWidthHeightsAndSpacingAndUpdateTablePadding()`
311
+ // then there would be a bug because
312
+ // `this.measureWidthHeightsAndSpacingAndUpdateTablePadding()`
313
+ // calls `this.updateState({ verticalSpacing })` which calls
314
+ // `this.onRender()` immediately, so `this.widthHasChanged`
315
+ // flag wouldn't be reset by that time and would trigger things
316
+ // like `this.itemHeights.reset()` a second time.
317
+ //
318
+ // So, instead read the value of `this.widthHasChanged` flag
319
+ // and reset it right away to prevent any such potential bugs.
320
+ //
321
+ const widthHasChanged = Boolean(this.widthHasChanged)
322
+ //
323
+ // Reset `this.widthHasChanged` flag.
324
+ this.widthHasChanged = undefined
325
+
326
+ // Read `this.firstNonMeasuredItemIndex` flag.
327
+ const nonMeasuredItemsHaveBeenRendered = this.firstNonMeasuredItemIndex !== undefined
328
+ // Reset `this.firstNonMeasuredItemIndex` flag.
329
+ this.firstNonMeasuredItemIndex = undefined
330
+
331
+ // Reset `this.newItemsWillBeRendered` flag.
332
+ this.newItemsWillBeRendered = undefined
333
+
334
+ // Reset `this.itemHeightsThatChangedWhileNewItemsWereBeingRendered`.
335
+ this.itemHeightsThatChangedWhileNewItemsWereBeingRendered = undefined
336
+
337
+ // Reset `this.itemStatesThatChangedWhileNewItemsWereBeingRendered`.
338
+ this.itemStatesThatChangedWhileNewItemsWereBeingRendered = undefined
339
+
340
+ return {
341
+ nonMeasuredItemsHaveBeenRendered,
342
+ widthHasChanged
343
+ }
344
+ }
345
+ }
@@ -0,0 +1,189 @@
1
+ import log from './utility/debug.js'
2
+
3
+ export default function() {
4
+ this.onResize = () => {
5
+ // Reset "previously calculated layout".
6
+ //
7
+ // The "previously calculated layout" feature is not currently used.
8
+ //
9
+ // The current layout snapshot could be stored as a "previously calculated layout" variable
10
+ // so that it could theoretically be used when calculating new layout incrementally
11
+ // rather than from scratch, which would be an optimization.
12
+ //
13
+ this.previouslyCalculatedLayout = undefined
14
+
15
+ // Cancel any potential scheduled scroll position restoration.
16
+ this.listHeightMeasurement.reset()
17
+
18
+ // Get the most recent items count.
19
+ // If there're a "pending" `setItems()` call then use the items count from that call
20
+ // instead of using the count of currently shown `items` from `state`.
21
+ // A `setItems()` call is "pending" when `updateState()` operation is "asynchronous", that is
22
+ // when `updateState()` calls aren't applied immediately, like in React.
23
+ const itemsCount = this.newItemsWillBeRendered
24
+ ? this.newItemsWillBeRendered.count
25
+ : this.getState().itemHeights.length
26
+
27
+ // If layout values have been calculated as a result of a "pending" `setItems()` call,
28
+ // then don't discard those new layout values and use them instead of the ones from `state`.
29
+ //
30
+ // A `setItems()` call is "pending" when `updateState()` operation is "asynchronous", that is
31
+ // when `updateState()` calls aren't applied immediately, like in React.
32
+ //
33
+ const layout = this.newItemsWillBeRendered
34
+ ? this.newItemsWillBeRendered.layout
35
+ : this.getState()
36
+
37
+ // Update `VirtualScroller` state.
38
+ const newState = {
39
+ scrollableContainerWidth: this.scrollableContainer.getWidth(),
40
+
41
+ // This state update should also overwrite all the `state` properties
42
+ // that are also updated in the "on scroll" handler (`getShownItemIndexes()`):
43
+ //
44
+ // * `firstShownItemIndex`
45
+ // * `lastShownItemIndex`
46
+ // * `beforeItemsHeight`
47
+ // * `afterItemsHeight`
48
+ //
49
+ // That's because this `updateState()` update has a higher priority
50
+ // than that of the "on scroll" handler, so it should overwrite
51
+ // any potential state changes dispatched by the "on scroll" handler.
52
+ //
53
+ // All these properties might have changed, but they're not
54
+ // recalculated here becase they'll be recalculated after
55
+ // this new state is applied (rendered).
56
+ //
57
+ firstShownItemIndex: layout.firstShownItemIndex,
58
+ lastShownItemIndex: layout.lastShownItemIndex,
59
+ beforeItemsHeight: layout.beforeItemsHeight,
60
+ afterItemsHeight: layout.afterItemsHeight,
61
+
62
+ // Reset item heights, because if scrollable container's width (or height)
63
+ // has changed, then the list width (or height) most likely also has changed,
64
+ // and also some CSS `@media()` rules might have been added or removed.
65
+ // So re-render the list entirely.
66
+ itemHeights: new Array(itemsCount),
67
+
68
+ columnsCount: this.getActualColumnsCountForState(),
69
+
70
+ // Re-measure vertical spacing after render because new CSS styles
71
+ // might be applied for the new window width.
72
+ verticalSpacing: undefined
73
+ }
74
+
75
+ const { firstShownItemIndex, lastShownItemIndex } = layout
76
+
77
+ // Get the `columnsCount` for the new window width.
78
+ const newColumnsCount = this.getActualColumnsCount()
79
+
80
+ // Re-calculate `firstShownItemIndex` and `lastShownItemIndex`
81
+ // based on the new `columnsCount` so that the whole row is visible.
82
+ const newFirstShownItemIndex = Math.floor(firstShownItemIndex / newColumnsCount) * newColumnsCount
83
+ const newLastShownItemIndex = Math.min(
84
+ Math.ceil((lastShownItemIndex + 1) / newColumnsCount) * newColumnsCount,
85
+ itemsCount
86
+ ) - 1
87
+
88
+ // Potentially update `firstShownItemIndex` if it needs to be adjusted in order to
89
+ // correspond to the new `columnsCount`.
90
+ if (newFirstShownItemIndex !== firstShownItemIndex) {
91
+ log('Columns Count changed from', this.getState().columnsCount || 1, 'to', newColumnsCount)
92
+ log('First Shown Item Index needs to change from', firstShownItemIndex, 'to', newFirstShownItemIndex)
93
+ }
94
+
95
+ // Always rewrite `firstShownItemIndex` and `lastShownItemIndex`
96
+ // as part of the `state` update, even if it hasn't been modified.
97
+ //
98
+ // The reason is that there could be two subsequent `onResize()` calls:
99
+ // the first one could be user resizing the window to half of its width,
100
+ // resulting in an "asynchronous" `updateState()` call, and then, before that
101
+ // `updateState()` call is applied, a second resize event happens when the user
102
+ // has resized the window back to its original width, meaning that the
103
+ // `columnsCount` is back to its original value.
104
+ // In that case, the final `newFirstShownItemIndex` will be equal to the
105
+ // original `firstShownItemIndex` that was in `state` before the user
106
+ // has started resizing the window, so, in the end, `state.firstShownItemIndex`
107
+ // property wouldn't have changed, but it still has to be part of the final
108
+ // state update in order to overwrite the previous update of `firstShownItemIndex`
109
+ // property that has been scheduled to be applied in state after the first resize
110
+ // happened.
111
+ //
112
+ newState.firstShownItemIndex = newFirstShownItemIndex
113
+ newState.lastShownItemIndex = newLastShownItemIndex
114
+
115
+ const verticalSpacing = this.getVerticalSpacing()
116
+ const columnsCount = this.getColumnsCount()
117
+
118
+ // `beforeResize` is always overwritten in `state` here.
119
+ // (once it has started being tracked in `state`)
120
+ if (this.shouldDiscardBeforeResizeItemHeights() || newFirstShownItemIndex === 0) {
121
+ if (this.beforeResize.shouldIncludeBeforeResizeValuesInState()) {
122
+ newState.beforeResize = undefined
123
+ }
124
+ }
125
+ // Snapshot "before resize" values in order to preserve the currently
126
+ // shown items' vertical position on screen so that there's no "content jumping".
127
+ else {
128
+ // Keep "before resize" values in order to preserve the currently
129
+ // shown items' vertical position on screen so that there's no
130
+ // "content jumping". These "before resize" values will be discarded
131
+ // when (if) the user scrolls back to the top of the list.
132
+ newState.beforeResize = {
133
+ verticalSpacing,
134
+ columnsCount,
135
+ itemHeights: this.beforeResize.snapshotBeforeResizeItemHeights({
136
+ firstShownItemIndex,
137
+ newFirstShownItemIndex,
138
+ newColumnsCount
139
+ })
140
+ }
141
+ }
142
+
143
+ // `this.widthHasChanged` tells `VirtualScroller` that it should
144
+ // temporarily stop other updates (like "on scroll" updates) and wait
145
+ // for the new `state` to be applied, after which the `onRender()`
146
+ // function will clear this flag and perform a re-layout.
147
+ //
148
+ // A re-layout is required because the layout parameters calculated above
149
+ // are approximate ones, and the exact item heights aren't known at this point.
150
+ // So the `updateState()` call below is just to re-render the `VirtualScroller`.
151
+ // After it has been re-rendered, it will measure item heights and then calculate
152
+ // correct layout parameters.
153
+ //
154
+ this.widthHasChanged = {
155
+ stateUpdate: newState
156
+ }
157
+
158
+ // Rerender.
159
+ this.updateState(newState)
160
+ }
161
+
162
+ // Returns whether "before resize" item heights should be discarded
163
+ // as a result of calling `setItems()` with a new set of items
164
+ // when an asynchronous `updateState()` call inside that function
165
+ // hasn't been applied yet.
166
+ //
167
+ // If `setItems()` update was an "incremental" one and no items
168
+ // have been prepended, then `firstShownItemIndex` is preserved,
169
+ // and all items' heights before it should be kept in order to
170
+ // preserve the top offset of the first shown item so that there's
171
+ // no "content jumping".
172
+ //
173
+ // If `setItems()` update was an "incremental" one but there're
174
+ // some prepended items, then it means that now there're new items
175
+ // with unknown heights at the top, so the top offset of the first
176
+ // shown item won't be preserved because there're no "before resize"
177
+ // heights of those items.
178
+ //
179
+ // If `setItems()` update was not an "incremental" one, then don't
180
+ // attempt to restore previous item heights after a potential window
181
+ // width change because all item heights have been reset.
182
+ //
183
+ this.shouldDiscardBeforeResizeItemHeights = () => {
184
+ if (this.newItemsWillBeRendered) {
185
+ const { prepend, replace } = this.newItemsWillBeRendered
186
+ return prepend || replace
187
+ }
188
+ }
189
+ }