virtual-scroller 1.7.9 → 1.9.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 (283) hide show
  1. package/.gitlab-ci.yml +1 -1
  2. package/CHANGELOG.md +71 -1
  3. package/README.md +434 -151
  4. package/bundle/index-bypass.html +1 -1
  5. package/bundle/index-dom.html +1 -1
  6. package/bundle/index-grid.html +1 -2
  7. package/bundle/index-scrollableContainer.html +1 -1
  8. package/bundle/index-tbody-scrollableContainer.html +2 -0
  9. package/bundle/index-tbody.html +2 -0
  10. package/bundle/virtual-scroller-dom.js +1 -1
  11. package/bundle/virtual-scroller-dom.js.map +1 -1
  12. package/bundle/virtual-scroller-react.js +1 -1
  13. package/bundle/virtual-scroller-react.js.map +1 -1
  14. package/bundle/virtual-scroller.js +1 -1
  15. package/bundle/virtual-scroller.js.map +1 -1
  16. package/commonjs/BeforeResize.js +315 -0
  17. package/commonjs/BeforeResize.js.map +1 -0
  18. package/commonjs/DOM/Engine.js +46 -0
  19. package/commonjs/DOM/Engine.js.map +1 -0
  20. package/commonjs/DOM/ItemsContainer.js +78 -0
  21. package/commonjs/DOM/ItemsContainer.js.map +1 -0
  22. package/commonjs/DOM/{WaitForStylesToLoad.js → ListTopOffsetWatcher.js} +71 -44
  23. package/commonjs/DOM/ListTopOffsetWatcher.js.map +1 -0
  24. package/commonjs/DOM/ScrollableContainer.js +69 -101
  25. package/commonjs/DOM/ScrollableContainer.js.map +1 -1
  26. package/commonjs/DOM/VirtualScroller.js +37 -29
  27. package/commonjs/DOM/VirtualScroller.js.map +1 -1
  28. package/commonjs/DOM/tbody.js +17 -11
  29. package/commonjs/DOM/tbody.js.map +1 -1
  30. package/commonjs/ItemHeights.js +33 -34
  31. package/commonjs/ItemHeights.js.map +1 -1
  32. package/commonjs/Layout.js +591 -216
  33. package/commonjs/Layout.js.map +1 -1
  34. package/commonjs/Layout.test.js +196 -0
  35. package/commonjs/Layout.test.js.map +1 -0
  36. package/commonjs/ListHeightMeasurement.js +124 -0
  37. package/commonjs/ListHeightMeasurement.js.map +1 -0
  38. package/commonjs/Resize.js +50 -39
  39. package/commonjs/Resize.js.map +1 -1
  40. package/commonjs/Scroll.js +139 -95
  41. package/commonjs/Scroll.js.map +1 -1
  42. package/commonjs/VirtualScroller.columns.js +43 -0
  43. package/commonjs/VirtualScroller.columns.js.map +1 -0
  44. package/commonjs/VirtualScroller.constructor.js +408 -0
  45. package/commonjs/VirtualScroller.constructor.js.map +1 -0
  46. package/commonjs/VirtualScroller.items.js +305 -0
  47. package/commonjs/VirtualScroller.items.js.map +1 -0
  48. package/commonjs/VirtualScroller.js +160 -1021
  49. package/commonjs/VirtualScroller.js.map +1 -1
  50. package/commonjs/VirtualScroller.layout.js +562 -0
  51. package/commonjs/VirtualScroller.layout.js.map +1 -0
  52. package/commonjs/VirtualScroller.onRender.js +357 -0
  53. package/commonjs/VirtualScroller.onRender.js.map +1 -0
  54. package/commonjs/VirtualScroller.resize.js +186 -0
  55. package/commonjs/VirtualScroller.resize.js.map +1 -0
  56. package/commonjs/VirtualScroller.state.js +301 -0
  57. package/commonjs/VirtualScroller.state.js.map +1 -0
  58. package/commonjs/VirtualScroller.verticalSpacing.js +65 -0
  59. package/commonjs/VirtualScroller.verticalSpacing.js.map +1 -0
  60. package/commonjs/getItemCoordinates.js.map +1 -1
  61. package/commonjs/getItemsDiff.js.map +1 -1
  62. package/commonjs/getVerticalSpacing.js +8 -8
  63. package/commonjs/getVerticalSpacing.js.map +1 -1
  64. package/commonjs/package.json +5 -0
  65. package/commonjs/react/VirtualScroller.js +182 -628
  66. package/commonjs/react/VirtualScroller.js.map +1 -1
  67. package/commonjs/react/useClassName.js +26 -0
  68. package/commonjs/react/useClassName.js.map +1 -0
  69. package/commonjs/react/useHandleItemsChange.js +116 -0
  70. package/commonjs/react/useHandleItemsChange.js.map +1 -0
  71. package/commonjs/react/useInstanceMethods.js +37 -0
  72. package/commonjs/react/useInstanceMethods.js.map +1 -0
  73. package/commonjs/react/useItemKeys.js +60 -0
  74. package/commonjs/react/useItemKeys.js.map +1 -0
  75. package/commonjs/react/useOnItemHeightChange.js +32 -0
  76. package/commonjs/react/useOnItemHeightChange.js.map +1 -0
  77. package/commonjs/react/useOnItemStateChange.js +32 -0
  78. package/commonjs/react/useOnItemStateChange.js.map +1 -0
  79. package/commonjs/react/useState.js +140 -0
  80. package/commonjs/react/useState.js.map +1 -0
  81. package/commonjs/react/useStyle.js +29 -0
  82. package/commonjs/react/useStyle.js.map +1 -0
  83. package/commonjs/react/useVirtualScroller.js +62 -0
  84. package/commonjs/react/useVirtualScroller.js.map +1 -0
  85. package/commonjs/react/useVirtualScrollerStartStop.js +20 -0
  86. package/commonjs/react/useVirtualScrollerStartStop.js.map +1 -0
  87. package/commonjs/test/Engine.js +23 -0
  88. package/commonjs/test/Engine.js.map +1 -0
  89. package/commonjs/test/ItemsContainer.js +127 -0
  90. package/commonjs/test/ItemsContainer.js.map +1 -0
  91. package/commonjs/test/ScrollableContainer.js +130 -0
  92. package/commonjs/test/ScrollableContainer.js.map +1 -0
  93. package/commonjs/test/VirtualScroller.js +281 -0
  94. package/commonjs/test/VirtualScroller.js.map +1 -0
  95. package/commonjs/utility/debounce.js +28 -6
  96. package/commonjs/utility/debounce.js.map +1 -1
  97. package/commonjs/utility/debug.js +51 -12
  98. package/commonjs/utility/debug.js.map +1 -1
  99. package/commonjs/utility/getStateSnapshot.js +50 -0
  100. package/commonjs/utility/getStateSnapshot.js.map +1 -0
  101. package/commonjs/utility/px.js +1 -1
  102. package/commonjs/utility/px.js.map +1 -1
  103. package/commonjs/utility/px.test.js +14 -0
  104. package/commonjs/utility/px.test.js.map +1 -0
  105. package/commonjs/utility/shallowEqual.js +1 -1
  106. package/commonjs/utility/shallowEqual.js.map +1 -1
  107. package/commonjs/utility/throttle.js.map +1 -1
  108. package/dom/index.cjs +4 -0
  109. package/dom/index.cjs.js +9 -0
  110. package/dom/index.d.ts +25 -0
  111. package/dom/index.js +1 -1
  112. package/dom/package.json +10 -4
  113. package/index.cjs +4 -0
  114. package/index.cjs.js +9 -0
  115. package/index.d.ts +99 -0
  116. package/index.js +1 -1
  117. package/modules/BeforeResize.js +305 -0
  118. package/modules/BeforeResize.js.map +1 -0
  119. package/modules/DOM/Engine.js +27 -0
  120. package/modules/DOM/Engine.js.map +1 -0
  121. package/modules/DOM/ItemsContainer.js +71 -0
  122. package/modules/DOM/ItemsContainer.js.map +1 -0
  123. package/modules/DOM/{WaitForStylesToLoad.js → ListTopOffsetWatcher.js} +72 -44
  124. package/modules/DOM/ListTopOffsetWatcher.js.map +1 -0
  125. package/modules/DOM/ScrollableContainer.js +68 -100
  126. package/modules/DOM/ScrollableContainer.js.map +1 -1
  127. package/modules/DOM/VirtualScroller.js +32 -28
  128. package/modules/DOM/VirtualScroller.js.map +1 -1
  129. package/modules/DOM/tbody.js +11 -9
  130. package/modules/DOM/tbody.js.map +1 -1
  131. package/modules/ItemHeights.js +28 -33
  132. package/modules/ItemHeights.js.map +1 -1
  133. package/modules/Layout.js +585 -214
  134. package/modules/Layout.js.map +1 -1
  135. package/modules/Layout.test.js +190 -0
  136. package/modules/Layout.test.js.map +1 -0
  137. package/modules/ListHeightMeasurement.js +117 -0
  138. package/modules/ListHeightMeasurement.js.map +1 -0
  139. package/modules/Resize.js +50 -39
  140. package/modules/Resize.js.map +1 -1
  141. package/modules/Scroll.js +139 -94
  142. package/modules/Scroll.js.map +1 -1
  143. package/modules/VirtualScroller.columns.js +36 -0
  144. package/modules/VirtualScroller.columns.js.map +1 -0
  145. package/modules/VirtualScroller.constructor.js +371 -0
  146. package/modules/VirtualScroller.constructor.js.map +1 -0
  147. package/modules/VirtualScroller.items.js +288 -0
  148. package/modules/VirtualScroller.items.js.map +1 -0
  149. package/modules/VirtualScroller.js +159 -1014
  150. package/modules/VirtualScroller.js.map +1 -1
  151. package/modules/VirtualScroller.layout.js +549 -0
  152. package/modules/VirtualScroller.layout.js.map +1 -0
  153. package/modules/VirtualScroller.onRender.js +337 -0
  154. package/modules/VirtualScroller.onRender.js.map +1 -0
  155. package/modules/VirtualScroller.resize.js +176 -0
  156. package/modules/VirtualScroller.resize.js.map +1 -0
  157. package/modules/VirtualScroller.state.js +283 -0
  158. package/modules/VirtualScroller.state.js.map +1 -0
  159. package/modules/VirtualScroller.verticalSpacing.js +54 -0
  160. package/modules/VirtualScroller.verticalSpacing.js.map +1 -0
  161. package/modules/getItemCoordinates.js.map +1 -1
  162. package/modules/getItemsDiff.js.map +1 -1
  163. package/modules/getVerticalSpacing.js +8 -8
  164. package/modules/getVerticalSpacing.js.map +1 -1
  165. package/modules/react/VirtualScroller.js +179 -634
  166. package/modules/react/VirtualScroller.js.map +1 -1
  167. package/modules/react/useClassName.js +18 -0
  168. package/modules/react/useClassName.js.map +1 -0
  169. package/modules/react/useHandleItemsChange.js +108 -0
  170. package/modules/react/useHandleItemsChange.js.map +1 -0
  171. package/modules/react/useInstanceMethods.js +28 -0
  172. package/modules/react/useInstanceMethods.js.map +1 -0
  173. package/modules/react/useItemKeys.js +52 -0
  174. package/modules/react/useItemKeys.js.map +1 -0
  175. package/modules/react/useOnItemHeightChange.js +24 -0
  176. package/modules/react/useOnItemHeightChange.js.map +1 -0
  177. package/modules/react/useOnItemStateChange.js +24 -0
  178. package/modules/react/useOnItemStateChange.js.map +1 -0
  179. package/modules/react/useState.js +132 -0
  180. package/modules/react/useState.js.map +1 -0
  181. package/modules/react/useStyle.js +19 -0
  182. package/modules/react/useStyle.js.map +1 -0
  183. package/modules/react/useVirtualScroller.js +51 -0
  184. package/modules/react/useVirtualScroller.js.map +1 -0
  185. package/modules/react/useVirtualScrollerStartStop.js +12 -0
  186. package/modules/react/useVirtualScrollerStartStop.js.map +1 -0
  187. package/modules/test/Engine.js +11 -0
  188. package/modules/test/Engine.js.map +1 -0
  189. package/modules/test/ItemsContainer.js +120 -0
  190. package/modules/test/ItemsContainer.js.map +1 -0
  191. package/modules/test/ScrollableContainer.js +123 -0
  192. package/modules/test/ScrollableContainer.js.map +1 -0
  193. package/modules/test/VirtualScroller.js +270 -0
  194. package/modules/test/VirtualScroller.js.map +1 -0
  195. package/modules/utility/debounce.js +28 -6
  196. package/modules/utility/debounce.js.map +1 -1
  197. package/modules/utility/debug.js +47 -10
  198. package/modules/utility/debug.js.map +1 -1
  199. package/modules/utility/getStateSnapshot.js +43 -0
  200. package/modules/utility/getStateSnapshot.js.map +1 -0
  201. package/modules/utility/px.js +1 -1
  202. package/modules/utility/px.js.map +1 -1
  203. package/modules/utility/px.test.js +9 -0
  204. package/modules/utility/px.test.js.map +1 -0
  205. package/modules/utility/shallowEqual.js +1 -1
  206. package/modules/utility/shallowEqual.js.map +1 -1
  207. package/modules/utility/throttle.js.map +1 -1
  208. package/package.json +54 -29
  209. package/react/index.cjs +4 -0
  210. package/react/index.cjs.js +9 -0
  211. package/react/index.d.ts +28 -0
  212. package/react/index.js +1 -1
  213. package/react/package.json +10 -4
  214. package/rollup.config.mjs +62 -0
  215. package/runnable/create-commonjs-package-json.js +11 -0
  216. package/source/BeforeResize.js +312 -0
  217. package/source/DOM/Engine.js +30 -0
  218. package/source/DOM/ItemsContainer.js +48 -0
  219. package/source/DOM/{WaitForStylesToLoad.js → ListTopOffsetWatcher.js} +61 -30
  220. package/source/DOM/ScrollableContainer.js +51 -73
  221. package/source/DOM/VirtualScroller.js +33 -18
  222. package/source/DOM/tbody.js +30 -21
  223. package/source/ItemHeights.js +27 -27
  224. package/source/Layout.js +629 -252
  225. package/source/Layout.test.js +176 -0
  226. package/source/ListHeightMeasurement.js +95 -0
  227. package/source/Resize.js +56 -32
  228. package/source/Scroll.js +135 -82
  229. package/source/VirtualScroller.columns.js +26 -0
  230. package/source/VirtualScroller.constructor.js +336 -0
  231. package/source/VirtualScroller.items.js +302 -0
  232. package/source/VirtualScroller.js +162 -936
  233. package/source/VirtualScroller.layout.js +539 -0
  234. package/source/VirtualScroller.onRender.js +345 -0
  235. package/source/VirtualScroller.resize.js +189 -0
  236. package/source/VirtualScroller.state.js +284 -0
  237. package/source/VirtualScroller.verticalSpacing.js +51 -0
  238. package/source/getVerticalSpacing.js +7 -7
  239. package/source/react/VirtualScroller.js +243 -603
  240. package/source/react/useClassName.js +14 -0
  241. package/source/react/useHandleItemsChange.js +115 -0
  242. package/source/react/useInstanceMethods.js +25 -0
  243. package/source/react/useItemKeys.js +59 -0
  244. package/source/react/useOnItemHeightChange.js +28 -0
  245. package/source/react/useOnItemStateChange.js +28 -0
  246. package/source/react/useState.js +114 -0
  247. package/source/react/useStyle.js +20 -0
  248. package/source/react/useVirtualScroller.js +59 -0
  249. package/source/react/useVirtualScrollerStartStop.js +12 -0
  250. package/source/test/Engine.js +11 -0
  251. package/source/test/ItemsContainer.js +87 -0
  252. package/source/test/ScrollableContainer.js +88 -0
  253. package/source/test/VirtualScroller.js +232 -0
  254. package/source/utility/debounce.js +22 -5
  255. package/source/utility/debug.js +34 -3
  256. package/source/utility/getStateSnapshot.js +36 -0
  257. package/source/utility/px.js +1 -1
  258. package/source/utility/px.test.js +9 -0
  259. package/website/index-bypass.html +195 -0
  260. package/website/index-grid.html +0 -1
  261. package/website/index-scrollableContainer.html +208 -0
  262. package/website/index-tbody-scrollableContainer.html +68 -0
  263. package/website/index-tbody.html +55 -0
  264. package/commonjs/DOM/RenderingEngine.js +0 -33
  265. package/commonjs/DOM/RenderingEngine.js.map +0 -1
  266. package/commonjs/DOM/Screen.js +0 -87
  267. package/commonjs/DOM/Screen.js.map +0 -1
  268. package/commonjs/DOM/WaitForStylesToLoad.js.map +0 -1
  269. package/commonjs/RestoreScroll.js +0 -118
  270. package/commonjs/RestoreScroll.js.map +0 -1
  271. package/dom/index.commonjs.js +0 -4
  272. package/index.commonjs.js +0 -4
  273. package/modules/DOM/RenderingEngine.js +0 -19
  274. package/modules/DOM/RenderingEngine.js.map +0 -1
  275. package/modules/DOM/Screen.js +0 -80
  276. package/modules/DOM/Screen.js.map +0 -1
  277. package/modules/DOM/WaitForStylesToLoad.js.map +0 -1
  278. package/modules/RestoreScroll.js +0 -111
  279. package/modules/RestoreScroll.js.map +0 -1
  280. package/react/index.commonjs.js +0 -4
  281. package/source/DOM/RenderingEngine.js +0 -22
  282. package/source/DOM/Screen.js +0 -51
  283. package/source/RestoreScroll.js +0 -86
@@ -0,0 +1,312 @@
1
+ import log from './utility/debug.js'
2
+
3
+ export default class BeforeResize {
4
+ constructor({
5
+ getState,
6
+ getVerticalSpacing,
7
+ getColumnsCount
8
+ }) {
9
+ this.getState = getState
10
+ this.getVerticalSpacing = getVerticalSpacing
11
+ this.getColumnsCount = getColumnsCount
12
+ }
13
+
14
+ initializeFromState(state) {
15
+ this._includesBeforeResizeInState = Boolean(state.beforeResize)
16
+ }
17
+
18
+ // Cleans up "before resize" item heights and adjusts the scroll position accordingly.
19
+ //
20
+ // Hypothetically, it could also wait for the user to stop scrolling and only then
21
+ // adjust the scroll position. The rationale is that if `window.scrollTo()` is called
22
+ // while the user is scrolling, the user would occasionally experience "lost" mouse wheel
23
+ // events when scrolling with a mouse wheel.
24
+ //
25
+ // Seems like Twitter's website waits for the user to stop scrolling before applying
26
+ // the scroll position correction after a window resize. This library could do that too,
27
+ // but that would require rewriting "before items height" top padding calculation
28
+ // so that it doesn't re-calculate it on every re-render and instead does so incrementally,
29
+ // and then, when the user stops, it re-calculates it from scratch removing the error
30
+ // and adjusting the scroll position accordingly so that there's no "jump of content".
31
+ //
32
+ // But, seems like it works fine as it is and there's no need to rewrite anything.
33
+ //
34
+ cleanUpBeforeResizeItemHeights() {
35
+ const {
36
+ firstShownItemIndex,
37
+ lastShownItemIndex,
38
+ itemHeights,
39
+ beforeResize
40
+ } = this.getState()
41
+
42
+ // If there're "before resize" properties in `state`
43
+ // then it means that the corresponding items are waiting to be
44
+ // re-measured after container resize. Since the resize,
45
+ // some of those non-re-measured items might have just been measured,
46
+ // so see if that's true, and if it is, remove those now-obsolete
47
+ // "before resize" item heights and ajust the scroll position
48
+ // so that there's no "content jumping".
49
+
50
+ if (beforeResize) {
51
+ // If the user has scrolled up to reveal a previously hidden item
52
+ // that has not yet been re-measured after a previous resize.
53
+ if (firstShownItemIndex < beforeResize.itemHeights.length) {
54
+ log('~ Clean up "before resize" item heights and correct scroll position ~')
55
+
56
+ // Some of the "before" items have been un-hidden and re-measured.
57
+ // Un-hiding those items would result in a "jump of content"
58
+ // because "before resize" heights of those un-hidden items
59
+ // could (and most likely will) be different from the current ones,
60
+ // or because "before resize" columns count is different from
61
+ // the current one.
62
+ // To prevent a "jump of content", calculate the scroll position
63
+ // difference and adjust the scroll position.
64
+
65
+ // The height of the item rows that have transitioned
66
+ // from hidden to shown.
67
+ let newlyShownItemRowsHeight = 0
68
+
69
+ // Some of the `itemHeights` between the current `firstShownItemIndex` and
70
+ // the previous `firstShownItemIndex` could stay `undefined` if the user
71
+ // scrolled "abruptly": for example, by using a `window.scrollTo()` call.
72
+ // In that case, the items below the visible ones won't be rendered and measured.
73
+ // In such case, limit the items being iterated over to the current `lastShownItemIndex`
74
+ // rather than the previous `firstShownItemIndex`.
75
+ const prevFirstReMeasuredItemsRowIndex = Math.floor(beforeResize.itemHeights.length / this.getColumnsCount())
76
+ const newlyShownItemsToIndex = Math.min(
77
+ prevFirstReMeasuredItemsRowIndex * this.getColumnsCount() - 1,
78
+ lastShownItemIndex
79
+ )
80
+
81
+ let i = firstShownItemIndex
82
+ while (i <= newlyShownItemsToIndex) {
83
+ // Calculate newly shown row height.
84
+ let rowHeight = 0
85
+ let columnIndex = 0
86
+ while (columnIndex < this.getColumnsCount() && i <= newlyShownItemsToIndex) {
87
+ let itemHeight = itemHeights[i]
88
+ if (itemHeight === undefined) {
89
+ // `itemHeight` can only be `undefined` when not `beforeResize`.
90
+ // Use the current "average item height" as a substitute.
91
+ itemHeight = this.getAverageItemHeight()
92
+ }
93
+ rowHeight = Math.max(rowHeight, itemHeight)
94
+ i++
95
+ columnIndex++
96
+ }
97
+ // Append to the total "newly shown item rows height".
98
+ newlyShownItemRowsHeight += rowHeight
99
+ newlyShownItemRowsHeight += this.getVerticalSpacing()
100
+ }
101
+
102
+ // The height of the "before resize" item rows
103
+ // that will be "cleaned up" in this function call.
104
+ let cleanedUpBeforeResizeItemRowsHeight = 0
105
+
106
+ // Some of the `beforeResize` item rows might have been skipped if the user
107
+ // scrolled up "abruptly": for example, by using a `window.scrollTo()` call.
108
+ // In that case, the "before resize" items below the bottom border of the screen
109
+ // shouldn't be accounted for when calculating the scrollbar adjustment shift
110
+ // because items after `lastShownItemIndex` aren't participating in the calculation
111
+ // of `newlyShownItemRowsHeight`.
112
+ const maxParticipatingBeforeResizeItemsCount = Math.min(beforeResize.itemHeights.length, lastShownItemIndex + 1)
113
+ const participatingBeforeResizeItemRowsCount = Math.ceil(maxParticipatingBeforeResizeItemsCount / beforeResize.columnsCount)
114
+
115
+ const firstCleanedUpBeforeResizeItemsRowIndex = firstShownItemIndex === 0
116
+ ? 0
117
+ : Math.floor((firstShownItemIndex - 1) / beforeResize.columnsCount) + 1
118
+
119
+ let k = firstCleanedUpBeforeResizeItemsRowIndex
120
+ while (k < participatingBeforeResizeItemRowsCount) {
121
+ const rowHeight = beforeResize.itemHeights[k * beforeResize.columnsCount]
122
+ cleanedUpBeforeResizeItemRowsHeight += rowHeight
123
+ cleanedUpBeforeResizeItemRowsHeight += beforeResize.verticalSpacing
124
+ k++
125
+ }
126
+
127
+ // Schedule an asynchronous `this.updateState()` call that will update
128
+ // `beforeResize` property of `state`. Ideally, it should be updated
129
+ // immediately, but since `this.updateState()` calls are asynchronous,
130
+ // the code updates just the underlying `beforeResize.itemHeights`
131
+ // array immediately instead, which is still a hack but still a lesser one.
132
+ if (firstShownItemIndex === 0) {
133
+ log('Drop all "before resize" item heights')
134
+ } else {
135
+ const firstDroppedBeforeResizeItemIndex = firstShownItemIndex
136
+ const lastDroppedBeforeResizeItemIndex = beforeResize.itemHeights.length - 1
137
+ if (firstDroppedBeforeResizeItemIndex === lastDroppedBeforeResizeItemIndex) {
138
+ log('For item index', firstDroppedBeforeResizeItemIndex, '— drop "before resize" height', beforeResize.itemHeights[firstDroppedBeforeResizeItemIndex], )
139
+ } else {
140
+ log('For item indexes from', firstDroppedBeforeResizeItemIndex, 'to', lastDroppedBeforeResizeItemIndex, '— drop "before resize" heights', beforeResize.itemHeights.slice(firstDroppedBeforeResizeItemIndex))
141
+ }
142
+ }
143
+
144
+ // Immediately update `beforeResize.itemHeights`
145
+ // so that the component isn't left in an inconsistent state
146
+ // before a `this.updateState()` call below is applied.
147
+ beforeResize.itemHeights.splice(
148
+ firstShownItemIndex,
149
+ beforeResize.itemHeights.length - firstShownItemIndex
150
+ )
151
+
152
+ // Return the "scroll by" amount that would correct the scroll position.
153
+ // Also return a state update.
154
+ return {
155
+ scrollBy: newlyShownItemRowsHeight - cleanedUpBeforeResizeItemRowsHeight,
156
+ beforeResize: firstShownItemIndex === 0 ? undefined : {
157
+ // Simply change the "reference" to `beforeResize` while leaving
158
+ // its contents unchanged. That simply indicates that it has been updated:
159
+ // `beforeResize.itemHeights` array length has been changed "directly".
160
+ ...beforeResize
161
+ }
162
+ }
163
+ }
164
+ }
165
+ }
166
+
167
+ // Snapshots "before resize" values in order to preserve the currently
168
+ // shown items' vertical position on screen so that there's no "content jumping".
169
+ //
170
+ // `newFirstShownItemIndex` is `> 0`.
171
+ //
172
+ snapshotBeforeResizeItemHeights({
173
+ firstShownItemIndex,
174
+ newFirstShownItemIndex,
175
+ newColumnsCount
176
+ }) {
177
+ const columnsCount = this.getColumnsCount()
178
+ const verticalSpacing = this.getVerticalSpacing()
179
+
180
+ this._includesBeforeResizeInState = true
181
+
182
+ const {
183
+ beforeResize: prevBeforeResize,
184
+ itemHeights
185
+ } = this.getState()
186
+
187
+ const prevBeforeResizeItemsCount = prevBeforeResize
188
+ ? prevBeforeResize.itemHeights.length
189
+ : 0
190
+
191
+ // If there already are "before resize" values in `state`
192
+ // then it means that those should be merged with the new ones.
193
+ //
194
+ // `beforeResize.itemHeights` could be empty in an edge case
195
+ // when there's a pending state update that sets `beforeResize`
196
+ // to `undefined`, and in that case empty `beforeResize.itemHeights`
197
+ // signals about that type of a situation.
198
+ //
199
+ if (prevBeforeResizeItemsCount > 0) {
200
+ // Because the "previous" before resize values might have been captured
201
+ // for a window width corresponding to a layout with a different columns count
202
+ // and different vertical spacing, re-calculate those item heights as if
203
+ // they corresponded to the current columns count and current vertical spacing,
204
+ // since "previous" and "new" before resize item heights are gonna be merged.
205
+ if (
206
+ prevBeforeResize.columnsCount !== columnsCount ||
207
+ prevBeforeResize.verticalSpacing !== verticalSpacing
208
+ ) {
209
+ let prevBeforeResizeBeforeItemsHeight = 0
210
+
211
+ const prevBeforeResizeItemRowsCount = Math.ceil(prevBeforeResizeItemsCount / prevBeforeResize.columnsCount)
212
+ let rowIndex = 0
213
+ while (rowIndex < prevBeforeResizeItemRowsCount) {
214
+ // Since all "before resize" item heights are equal within a row,
215
+ // the height of the first "before resize" item in a row is that row's height.
216
+ const rowHeight = prevBeforeResize.itemHeights[rowIndex * prevBeforeResize.columnsCount]
217
+ prevBeforeResizeBeforeItemsHeight += rowHeight
218
+ prevBeforeResizeBeforeItemsHeight += prevBeforeResize.verticalSpacing
219
+ rowIndex++
220
+ }
221
+
222
+ let newBeforeResizeAdditionalBeforeItemsHeight = 0
223
+ let i = firstShownItemIndex
224
+ while (i < newFirstShownItemIndex) {
225
+ let rowHeight = 0
226
+ let k = 0
227
+ while (k < columnsCount && i < newFirstShownItemIndex) {
228
+ rowHeight = Math.max(rowHeight, itemHeights[i])
229
+ k++
230
+ i++
231
+ }
232
+ newBeforeResizeAdditionalBeforeItemsHeight += rowHeight
233
+ newBeforeResizeAdditionalBeforeItemsHeight += verticalSpacing
234
+ }
235
+
236
+ const newBeforeResizeBeforeItemsHeight = prevBeforeResizeBeforeItemsHeight + newBeforeResizeAdditionalBeforeItemsHeight
237
+ const newBeforeResizeBeforeItemRowsCount = Math.ceil(newFirstShownItemIndex / columnsCount)
238
+
239
+ return new Array(newFirstShownItemIndex).fill(
240
+ // Re-calculate "before resize" item heights so that "previous" and "new" ones
241
+ // correspond to the same (new) columns count.
242
+ // Also don't occasionally set item heights to `< 0`.
243
+ Math.max(0, newBeforeResizeBeforeItemsHeight / newBeforeResizeBeforeItemRowsCount - verticalSpacing)
244
+ )
245
+ } else {
246
+ // Add new item heights to the previously snapshotted ones.
247
+ return prevBeforeResize.itemHeights.concat(
248
+ equalizeItemHeights(
249
+ itemHeights,
250
+ newFirstShownItemIndex,
251
+ columnsCount
252
+ ).slice(prevBeforeResize.itemHeights.length)
253
+ )
254
+ }
255
+ } else {
256
+ return equalizeItemHeights(
257
+ itemHeights,
258
+ newFirstShownItemIndex,
259
+ columnsCount
260
+ )
261
+ }
262
+ }
263
+
264
+ shouldIncludeBeforeResizeValuesInState() {
265
+ return this._includesBeforeResizeInState
266
+ }
267
+ }
268
+
269
+ // Equalizes all item heights within a given row, for each row.
270
+ //
271
+ // The reason is that `beforeResize.itemHeights` is not necessarily divisible by
272
+ // `beforeResize.columnsCount`, which would result in varying last row height
273
+ // as items get removed from `beforeResize.itemHeights` as the user scrolls up.
274
+ //
275
+ // By equalizing all item heights within a given row, for each row, such "jumping"
276
+ // last "before resize" row height is prevented when the user scrolls up.
277
+ //
278
+ function equalizeItemHeights(itemHeights, maxItemsCount, columnsCount) {
279
+ itemHeights = itemHeights.slice(0, Math.ceil(maxItemsCount / columnsCount) * columnsCount)
280
+
281
+ let rowIndex = 0
282
+ while (rowIndex * columnsCount < maxItemsCount) {
283
+ // Calculate row height.
284
+ let rowHeight = 0
285
+ let k = 0
286
+ while (k < columnsCount) {
287
+ rowHeight = Math.max(rowHeight, itemHeights[rowIndex * columnsCount + k])
288
+ k++
289
+ }
290
+
291
+ // Equalize all item heights within the row.
292
+ k = 0
293
+ while (k < columnsCount) {
294
+ itemHeights[rowIndex * columnsCount + k] = rowHeight
295
+ k++
296
+ }
297
+
298
+ // Proceed with the next row.
299
+ rowIndex++
300
+ }
301
+
302
+ return itemHeights.slice(0, maxItemsCount)
303
+ }
304
+
305
+ export function cleanUpBeforeResizeState(state) {
306
+ if (state.beforeResize) {
307
+ if (state.beforeResize.itemHeights.length === 0) {
308
+ state.beforeResize = undefined
309
+ }
310
+ }
311
+ return state
312
+ }
@@ -0,0 +1,30 @@
1
+ import ItemsContainer from './ItemsContainer.js'
2
+ import ScrollableContainer, { ScrollableWindowContainer } from './ScrollableContainer.js'
3
+ import ListTopOffsetWatcher from './ListTopOffsetWatcher.js'
4
+
5
+ export default {
6
+ createItemsContainer(getItemsContainerElement) {
7
+ return new ItemsContainer(getItemsContainerElement)
8
+ },
9
+
10
+ // Creates a `scrollableContainer`.
11
+ // On client side, `scrollableContainer` is always created.
12
+ // On server side, `scrollableContainer` is not created (and not used).
13
+ createScrollableContainer(getScrollableContainerElement, getItemsContainerElement) {
14
+ if (getScrollableContainerElement) {
15
+ return new ScrollableContainer(getScrollableContainerElement, getItemsContainerElement)
16
+ } else if (typeof window !== 'undefined') {
17
+ return new ScrollableWindowContainer(getItemsContainerElement)
18
+ }
19
+ },
20
+
21
+ watchListTopOffset({
22
+ getListTopOffset,
23
+ onListTopOffsetChange
24
+ }) {
25
+ return new ListTopOffsetWatcher({
26
+ getListTopOffset,
27
+ onListTopOffsetChange
28
+ })
29
+ }
30
+ }
@@ -0,0 +1,48 @@
1
+ export default class ItemsContainer {
2
+ /**
3
+ * Constructs a new "container" from an element.
4
+ * @param {function} getElement
5
+ */
6
+ constructor(getElement) {
7
+ this.getElement = getElement
8
+ }
9
+
10
+ /**
11
+ * Returns an item element's "top offset", relative to the items `container`'s top edge.
12
+ * @param {number} renderedElementIndex — An index of an item relative to the "first shown item index". For example, if the list is showing items from index 8 to index 12 then `renderedElementIndex = 0` would mean the item at index `8`.
13
+ * @return {number}
14
+ */
15
+ getNthRenderedItemTopOffset(renderedElementIndex) {
16
+ return this.getElement().childNodes[renderedElementIndex].getBoundingClientRect().top - this.getElement().getBoundingClientRect().top
17
+ }
18
+
19
+ /**
20
+ * Returns an item element's height.
21
+ * @param {number} renderedElementIndex — An index of an item relative to the "first shown item index". For example, if the list is showing items from index 8 to index 12 then `renderedElementIndex = 0` would mean the item at index `8`.
22
+ * @return {number}
23
+ */
24
+ getNthRenderedItemHeight(renderedElementIndex) {
25
+ // `offsetHeight` is not precise enough (doesn't return fractional pixels).
26
+ // return this.getElement().childNodes[renderedElementIndex].offsetHeight
27
+ return this.getElement().childNodes[renderedElementIndex].getBoundingClientRect().height
28
+ }
29
+
30
+ /**
31
+ * Returns items container height.
32
+ * @return {number}
33
+ */
34
+ getHeight() {
35
+ // `offsetHeight` is not precise enough (doesn't return fractional pixels).
36
+ // return this.getElement().offsetHeight
37
+ return this.getElement().getBoundingClientRect().height
38
+ }
39
+
40
+ /**
41
+ * Removes all item elements of an items container.
42
+ */
43
+ clear() {
44
+ while (this.getElement().firstChild) {
45
+ this.getElement().removeChild(this.getElement().firstChild)
46
+ }
47
+ }
48
+ }
@@ -4,47 +4,73 @@
4
4
  // https://github.com/bvaughn/react-virtualized/issues/722
5
5
  import { setTimeout, clearTimeout } from 'request-animation-frame-timeout'
6
6
 
7
- import { LAYOUT_REASON } from '../Layout'
7
+ // Refreshing two times every seconds seems reasonable.
8
+ const WATCH_LIST_TOP_OFFSET_INTERVAL = 500
9
+
10
+ // Refreshing for 3 seconds after the initial page load seems reasonable.
11
+ const WATCH_LIST_TOP_OFFSET_MAX_DURATION = 3000
8
12
 
9
13
  // `VirtualScroller` calls `this.layout.layOut()` on mount,
10
14
  // but if the page styles are applied after `VirtualScroller` mounts
11
15
  // (for example, if styles are applied via javascript, like Webpack does)
12
- // then the list might not render correctly and will only show the first item.
13
- // The reason for that would be that calling `.getListTopOffsetInsideScrollableContainer()`
14
- // on mount returns "incorrect" `top` position because the styles haven't been applied yet.
16
+ // then the list might not render correctly and it will only show the first item.
17
+ // The reason is that in that case calling `.getListTopOffset()` on mount
18
+ // returns "incorrect" `top` position because the styles haven't been applied yet.
19
+ //
15
20
  // For example, consider a page:
21
+ //
16
22
  // <div class="page">
17
23
  // <nav class="sidebar">...</nav>
18
24
  // <main>...</main>
19
25
  // </div>
20
- // The sidebar is styled as `position: fixed`, but until
21
- // the page styles have been applied it's gonna be a regular `<div/>`
26
+ //
27
+ // The sidebar is styled as `position: fixed`, but, until
28
+ // the page styles have been applied, it's gonna be a regular `<div/>`
22
29
  // meaning that `<main/>` will be rendered below the sidebar
23
- // and will appear offscreen and so it will only render the first item.
24
- // Then, the page styles are loaded and applied and the sidebar
25
- // is now `position: fixed` so `<main/>` is now rendered at the top of the page
30
+ // and will appear offscreen, and so it will only render the first item.
31
+ //
32
+ // Then, the page styles are loaded and applied, and the sidebar
33
+ // is now `position: fixed`, so `<main/>` is now rendered at the top of the page,
26
34
  // but `VirtualScroller`'s `.render()` has already been called
27
35
  // and it won't re-render until the user scrolls or the window is resized.
28
- // This type of a bug doesn't occur in production, but it can appear
36
+ //
37
+ // This type of a bug doesn't seem to occur in production, but it can appear
29
38
  // in development mode when using Webpack. The workaround `VirtualScroller`
30
- // implements for such cases is calling `.getListTopOffsetInsideScrollableContainer()`
39
+ // implements for such cases is calling `.getListTopOffset()`
31
40
  // on the list container DOM element periodically (every second) to check
32
41
  // if the `top` coordinate has changed as a result of CSS being applied:
33
42
  // if it has then it recalculates the shown item indexes.
34
- export default class WaitForStylesToLoad {
43
+ //
44
+ // Maybe this bug could occur in production when using Webpack chunks.
45
+ // That depends on how a style of a chunk is added to the page:
46
+ // if it's added via `javascript` after the page has been rendered
47
+ // then this workaround will also work for that case.
48
+ //
49
+ // Another example would be a page having a really tall expanded "accordion"
50
+ // section, below which a `VirtualScroller` list resides. If the user un-expands
51
+ // such expanded "accordion" section, the list would become visible but
52
+ // it wouldn't get re-rendered because no `scroll` event has occured,
53
+ // and the list only re-renders automatically on `scroll` events.
54
+ // To work around such cases, call `virtualScroller.updateLayout()` method manually.
55
+ // The workaround below could be extended to refresh the list's top coordinate
56
+ // indefinitely and at higher intervals, but why waste CPU time on that.
57
+ // There doesn't seem to be any DOM API for tracking an element's top position.
58
+ // There is `IntersectionObserver` API but it doesn't exactly do that.
59
+ //
60
+ export default class ListTopOffsetWatcher {
35
61
  constructor({
36
- updateLayout,
37
- getListTopOffsetInsideScrollableContainer
62
+ getListTopOffset,
63
+ onListTopOffsetChange
38
64
  }) {
39
- this.updateLayout = updateLayout
40
- this.getListTopOffsetInsideScrollableContainer = getListTopOffsetInsideScrollableContainer
65
+ this.getListTopOffset = getListTopOffset
66
+ this.onListTopOffsetChange = onListTopOffsetChange
41
67
  }
42
68
 
43
- onGotListTopOffset(listTopOffset) {
69
+ onListTopOffset(listTopOffset) {
44
70
  if (this.listTopOffsetInsideScrollableContainer === undefined) {
45
71
  // Start periodical checks of the list's top offset
46
72
  // in order to perform a re-layout in case it changes.
47
- // See the comments in `WaitForStylesToLoad.js` file
73
+ // See the comments in `ListTopOffsetWatcher.js` file
48
74
  // on why can the list's top offset change, and in which circumstances.
49
75
  this.start()
50
76
  }
@@ -52,13 +78,21 @@ export default class WaitForStylesToLoad {
52
78
  }
53
79
 
54
80
  start() {
55
- this.isRendered = true
81
+ this._isActive = true
56
82
  this.watchListTopOffset()
57
83
  }
58
84
 
85
+ isStarted() {
86
+ return this._isActive
87
+ }
88
+
59
89
  stop() {
60
- this.isRendered = false
61
- clearTimeout(this.watchListTopOffsetTimer)
90
+ this._isActive = false
91
+
92
+ if (this.watchListTopOffsetTimer) {
93
+ clearTimeout(this.watchListTopOffsetTimer)
94
+ this.watchListTopOffsetTimer = undefined
95
+ }
62
96
  }
63
97
 
64
98
  watchListTopOffset() {
@@ -66,17 +100,17 @@ export default class WaitForStylesToLoad {
66
100
  const check = () => {
67
101
  // If `VirtualScroller` has been unmounted
68
102
  // while `setTimeout()` was waiting, then exit.
69
- if (!this.isRendered) {
103
+ if (!this._isActive) {
70
104
  return
71
105
  }
72
106
  // Skip comparing `top` coordinate of the list
73
107
  // when this function is called for the first time.
74
108
  if (this.listTopOffsetInsideScrollableContainer !== undefined) {
75
- // Calling `this.getListTopOffsetInsideScrollableContainer()`
76
- // on an element is about 0.003 milliseconds on a modern desktop CPU,
109
+ // Calling `this.getListTopOffset()` on an element
110
+ // runs about 0.003 milliseconds on a modern desktop CPU,
77
111
  // so I guess it's fine calling it twice a second.
78
- if (this.getListTopOffsetInsideScrollableContainer() !== this.listTopOffsetInsideScrollableContainer) {
79
- this.updateLayout({ reason: LAYOUT_REASON.TOP_OFFSET_CHANGED })
112
+ if (this.getListTopOffset() !== this.listTopOffsetInsideScrollableContainer) {
113
+ this.onListTopOffsetChange()
80
114
  }
81
115
  }
82
116
  // Compare `top` coordinate of the list twice a second
@@ -93,7 +127,4 @@ export default class WaitForStylesToLoad {
93
127
  // Run the cycle.
94
128
  check()
95
129
  }
96
- }
97
-
98
- const WATCH_LIST_TOP_OFFSET_INTERVAL = 500
99
- const WATCH_LIST_TOP_OFFSET_MAX_DURATION = 3000
130
+ }