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
package/source/Layout.js CHANGED
@@ -1,45 +1,59 @@
1
- import log from './utility/debug'
1
+ import log, { warn } from './utility/debug.js'
2
2
 
3
3
  export default class Layout {
4
4
  constructor({
5
5
  bypass,
6
6
  estimatedItemHeight,
7
7
  measureItemsBatchSize,
8
+ getPrerenderMargin,
8
9
  getVerticalSpacing,
10
+ getVerticalSpacingBeforeResize,
9
11
  getColumnsCount,
12
+ getColumnsCountBeforeResize,
10
13
  getItemHeight,
11
- getAverageItemHeight
14
+ getItemHeightBeforeResize,
15
+ getBeforeResizeItemsCount,
16
+ getAverageItemHeight,
17
+ getMaxVisibleAreaHeight,
18
+ getPreviouslyCalculatedLayout
12
19
  }) {
13
20
  this.bypass = bypass
14
21
  this.estimatedItemHeight = estimatedItemHeight
15
22
  this.measureItemsBatchSize = measureItemsBatchSize
23
+ this.getPrerenderMargin = getPrerenderMargin
16
24
  this.getVerticalSpacing = getVerticalSpacing
25
+ this.getVerticalSpacingBeforeResize = getVerticalSpacingBeforeResize
17
26
  this.getColumnsCount = getColumnsCount
27
+ this.getColumnsCountBeforeResize = getColumnsCountBeforeResize
18
28
  this.getItemHeight = getItemHeight
29
+ this.getItemHeightBeforeResize = getItemHeightBeforeResize
30
+ this.getBeforeResizeItemsCount = getBeforeResizeItemsCount
19
31
  this.getAverageItemHeight = getAverageItemHeight
32
+ this.getMaxVisibleAreaHeight = getMaxVisibleAreaHeight
33
+ //
34
+ // The "previously calculated layout" feature is not currently used.
35
+ //
36
+ // The current layout snapshot could be stored as a "previously calculated layout" variable
37
+ // so that it could theoretically be used when calculating new layout incrementally
38
+ // rather than from scratch, which would be an optimization.
39
+ //
40
+ this.getPreviouslyCalculatedLayout = getPreviouslyCalculatedLayout
20
41
  }
21
42
 
22
43
  getInitialLayoutValues({
23
- bypass,
24
44
  itemsCount,
25
- visibleAreaHeightIncludingMargins
45
+ columnsCount
26
46
  }) {
27
- // On server side, at initialization time, there's no "visible area height",
28
- // so default to `1` estimated rows count.
29
- const estimatedRowsCount = visibleAreaHeightIncludingMargins
30
- ? this.getEstimatedRowsCountForHeight(visibleAreaHeightIncludingMargins)
31
- : 1
32
47
  let firstShownItemIndex
33
48
  let lastShownItemIndex
34
49
  // If there're no items then `firstShownItemIndex` stays `undefined`.
35
50
  if (itemsCount > 0) {
36
51
  firstShownItemIndex = 0
37
- lastShownItemIndex = this.getLastShownItemIndex(
38
- firstShownItemIndex,
52
+ lastShownItemIndex = this.getInitialLastShownItemIndex({
39
53
  itemsCount,
40
- estimatedRowsCount,
41
- bypass
42
- )
54
+ columnsCount,
55
+ firstShownItemIndex
56
+ })
43
57
  }
44
58
  return {
45
59
  beforeItemsHeight: 0,
@@ -49,25 +63,32 @@ export default class Layout {
49
63
  }
50
64
  }
51
65
 
52
- getLastShownItemIndex(
53
- firstShownItemIndex,
66
+ getInitialLastShownItemIndex({
54
67
  itemsCount,
55
- estimatedRowsCount,
56
- bypass
57
- ) {
58
- if (this.bypass || bypass) {
68
+ columnsCount,
69
+ firstShownItemIndex
70
+ }) {
71
+ if (this.bypass) {
59
72
  return itemsCount - 1
60
73
  }
74
+ // On server side, at initialization time,
75
+ // `scrollableContainer` is `undefined`,
76
+ // so default to `1` estimated rows count.
77
+ let estimatedRowsCount = 1
78
+ if (this.getMaxVisibleAreaHeight()) {
79
+ estimatedRowsCount = this.getEstimatedRowsCountForHeight(this.getMaxVisibleAreaHeight() + this.getPrerenderMargin())
80
+ }
61
81
  return Math.min(
62
- firstShownItemIndex + (estimatedRowsCount * this.getColumnsCount() - 1),
82
+ firstShownItemIndex + (estimatedRowsCount * columnsCount - 1),
63
83
  itemsCount - 1
64
84
  )
65
85
  }
66
86
 
67
87
  getEstimatedRowsCountForHeight(height) {
68
88
  const estimatedItemHeight = this.getEstimatedItemHeight()
89
+ const verticalSpacing = this.getVerticalSpacing()
69
90
  if (estimatedItemHeight) {
70
- return Math.ceil((height + this.getVerticalSpacing()) / (estimatedItemHeight + this.getVerticalSpacing()))
91
+ return Math.ceil((height + verticalSpacing) / (estimatedItemHeight + verticalSpacing))
71
92
  } else {
72
93
  // If no items have been rendered yet, and no `estimatedItemHeight` option
73
94
  // has been passed, then default to `1` estimated rows count in any `height`.
@@ -84,315 +105,669 @@ export default class Layout {
84
105
  return this.getAverageItemHeight() || this.estimatedItemHeight || 0
85
106
  }
86
107
 
87
- updateLayoutForItemsDiff(layout, {
108
+ getLayoutUpdateForItemsDiff({
109
+ firstShownItemIndex,
110
+ lastShownItemIndex,
111
+ beforeItemsHeight,
112
+ afterItemsHeight
113
+ }, {
88
114
  prependedItemsCount,
89
115
  appendedItemsCount
90
116
  }, {
91
- itemsCount
117
+ itemsCount,
118
+ columnsCount,
119
+ shouldRestoreScrollPosition,
120
+ onResetGridLayout
92
121
  }) {
93
- layout.firstShownItemIndex += prependedItemsCount
94
- layout.lastShownItemIndex += prependedItemsCount
95
- const columnsCount = this.getColumnsCount()
96
- if (prependedItemsCount % columnsCount === 0) {
97
- // If the layout stays the same, then simply increase
98
- // the top and bottom margins proportionally to the amount
99
- // of the items added.
100
- const prependedRowsCount = prependedItemsCount / columnsCount
122
+ // const layoutUpdate = {}
123
+
124
+ // If the layout stays the same, then simply increase
125
+ // the top and bottom margins proportionally to the amount
126
+ // of the items added.
127
+ const averageItemHeight = this.getAverageItemHeight()
128
+ const verticalSpacing = this.getVerticalSpacing()
129
+
130
+ if (appendedItemsCount > 0) {
101
131
  const appendedRowsCount = Math.ceil(appendedItemsCount / columnsCount)
102
- const averageItemHeight = this.getAverageItemHeight()
103
- const verticalSpacing = this.getVerticalSpacing()
104
- layout.beforeItemsHeight += prependedRowsCount * (averageItemHeight + verticalSpacing)
105
- layout.afterItemsHeight += appendedRowsCount * (verticalSpacing + averageItemHeight)
106
- } else {
107
- // Rows will be rebalanced as a result of prepending the items,
108
- // and the row heights can change as a result, so recalculate
109
- // `beforeItemsHeight` and `afterItemsHeight` from scratch.
110
- // `this.itemHeights[]` and `firstShownItemIndex`/`lastShownItemIndex`
111
- // have already been updated at this point.
112
- layout.beforeItemsHeight = this.getBeforeItemsHeight(
113
- layout.firstShownItemIndex,
114
- layout.lastShownItemIndex
115
- )
116
- layout.afterItemsHeight = this.getAfterItemsHeight(
117
- layout.firstShownItemIndex,
118
- layout.lastShownItemIndex,
119
- itemsCount
120
- )
132
+ const addedHeightAfter = appendedRowsCount * (verticalSpacing + averageItemHeight)
133
+
134
+ afterItemsHeight += addedHeightAfter
135
+
136
+ // layoutUpdate = {
137
+ // ...layoutUpdate,
138
+ // afterItemsHeight
139
+ // }
140
+ }
141
+
142
+ if (prependedItemsCount > 0) {
143
+ const prependedRowsCount = Math.ceil(prependedItemsCount / columnsCount)
144
+ const addedHeightBefore = prependedRowsCount * (averageItemHeight + verticalSpacing)
145
+
146
+ firstShownItemIndex += prependedItemsCount
147
+ lastShownItemIndex += prependedItemsCount
148
+ beforeItemsHeight += addedHeightBefore
149
+
150
+ // If the currently shown items position on screen should be preserved
151
+ // when prepending new items, then it means that:
152
+ // * The current scroll position should be snapshotted.
153
+ // * The current list height should be snapshotted.
154
+ // * All prepended items should be shown so that their height could be
155
+ // measured after they're rendered. Based on the prepended items' height,
156
+ // the scroll position will be restored so that there's no "jump of content".
157
+ if (shouldRestoreScrollPosition) {
158
+ firstShownItemIndex = 0
159
+ beforeItemsHeight = 0
160
+ }
161
+
162
+ if (prependedItemsCount % columnsCount > 0) {
163
+ // Rows will be rebalanced as a result of prepending new items,
164
+ // and row heights can change as a result, so re-layout items
165
+ // after they've been measured (after the upcoming re-render).
166
+ //
167
+ // For example, consider a web page where item rows are `display: flex`.
168
+ // Suppose there're 3 columns and it shows items from 4 to 6.
169
+ //
170
+ // ------------------------------------------
171
+ // | Apples are | Bananas | Cranberries |
172
+ // | green | | |
173
+ // ------------------------------------------
174
+ // | Dates | Elderberry | Figs are |
175
+ // | | | tasty |
176
+ // ------------------------------------------
177
+ //
178
+ // Now, 1 item gets prepended. As a result, all existing rows will have
179
+ // a different set of items, which means that the row heights will change.
180
+ //
181
+ // ------------------------------------------
182
+ // | Zucchini | Apples are | Bananas |
183
+ // | | green | |
184
+ // ------------------------------------------
185
+ // | Cranberries | Dates | Elderberry |
186
+ // ------------------------------------------
187
+ // | Figs |
188
+ // | are tasty |
189
+ // ---------------
190
+ //
191
+ // As it can be seen above, the second row's height has changed from 2 to 1.
192
+ // Not only that, but `itemHeights` have changed as well, so if you thought
193
+ // that the library could easily recalculate row heights using `Math.max()` — 
194
+ // turns out it's not always the case.
195
+ //
196
+ // There could be an explicit opt-in option for automatically recalculating
197
+ // row heights, but I don't want to write code for such an extremely rare
198
+ // use case. Instead, use the `getColumnsCount()` parameter function when
199
+ // fetching previous items.
200
+
201
+ onResetGridLayout()
202
+
203
+ warn('~ Prepended items count', prependedItemsCount, 'is not divisible by Columns Count', columnsCount, '~')
204
+ warn('Layout reset required')
205
+
206
+ const shownItemsCountBeforeItemsUpdate = lastShownItemIndex - firstShownItemIndex + 1
207
+
208
+ firstShownItemIndex = 0
209
+ beforeItemsHeight = 0
210
+
211
+ if (!shouldRestoreScrollPosition) {
212
+ // Limit shown items count if too many items have been prepended.
213
+ if (prependedItemsCount > shownItemsCountBeforeItemsUpdate) {
214
+ lastShownItemIndex = this.getInitialLastShownItemIndex({
215
+ itemsCount,
216
+ columnsCount,
217
+ firstShownItemIndex
218
+ })
219
+
220
+ // Approximate `afterItemsHeight` calculation.
221
+ const afterItemsCount = itemsCount - (lastShownItemIndex + 1)
222
+ afterItemsHeight = Math.ceil(afterItemsCount / columnsCount) * (verticalSpacing + averageItemHeight)
223
+
224
+ // layoutUpdate = {
225
+ // ...layoutUpdate,
226
+ // afterItemsHeight
227
+ // }
228
+ }
229
+ }
230
+ }
231
+
232
+ // layoutUpdate = {
233
+ // ...layoutUpdate,
234
+ // beforeItemsHeight,
235
+ // firstShownItemIndex,
236
+ // lastShownItemIndex
237
+ // }
238
+ }
239
+
240
+ // return layoutUpdate
241
+
242
+ // Overwrite all four props in all scenarios.
243
+ // The reason is that only this way subsequent `setItems()` calls
244
+ // will be truly "stateless" when a chain of `setItems()` calls
245
+ // could be replaced with just the last one in a scenario when
246
+ // `updateState()` calls are "asynchronous" (delayed execution).
247
+ //
248
+ // So, for example, the user calls `setItems()` with one set of items.
249
+ // A `updateState()` call has been dispatched but the `state` hasn't been updated yet.
250
+ // Then the user calls `setItems()` with another set of items.
251
+ // If this function only returned a minimal set of properties that actually change,
252
+ // the other layout properties of the second `setItems()` call wouldn't overwrite the ones
253
+ // scheduled for update during the first `setItems()` call, resulting in an inconsistent `state`.
254
+ //
255
+ // For example, the first `setItems()` call does a `updateState()` call where it updates
256
+ // `afterItemsHeight`, and then the second `setItems()` call only updates `beforeItemsHeight`
257
+ // and `firstShownItemIndex` and `lastShownItemIndex`. If the second `setItems()` call was to
258
+ // overwrite any effects of the pending-but-not-yet-applied first `setItems()` call, it would
259
+ // have to call `updateState()` with an `afterItemsHeight` property too, even though it hasn't change.
260
+ // That would be just to revert the change to `afterItemsHeight` state property already scheduled
261
+ // by the first `setItems()` call.
262
+ //
263
+ return {
264
+ beforeItemsHeight,
265
+ afterItemsHeight,
266
+ firstShownItemIndex,
267
+ lastShownItemIndex
121
268
  }
122
269
  }
123
270
 
124
- _getVisibleItemIndexes(
125
- visibleAreaTop,
126
- visibleAreaBottom,
127
- listTopOffset,
128
- itemsCount
129
- ) {
271
+ // If an item that hasn't been shown (and measured) yet is encountered
272
+ // then show such item and then retry after it has been measured.
273
+ getItemNotMeasuredIndexes(i, {
274
+ itemsCount,
275
+ firstShownItemIndex,
276
+ nonMeasuredAreaHeight,
277
+ indexOfTheFirstItemInTheRow
278
+ }) {
279
+ log('Item index', i, 'height is required for calculations but hasn\'t been measured yet. Mark the item as "shown", rerender the list, measure the item\'s height and redo the layout.')
280
+
130
281
  const columnsCount = this.getColumnsCount()
282
+
283
+ const itemsCountToRenderForMeasurement = Math.min(
284
+ this.getEstimatedRowsCountForHeight(nonMeasuredAreaHeight) * columnsCount,
285
+ this.measureItemsBatchSize || Infinity,
286
+ )
287
+
288
+ if (firstShownItemIndex === undefined) {
289
+ firstShownItemIndex = indexOfTheFirstItemInTheRow
290
+ }
291
+
292
+ const lastShownItemIndex = Math.min(
293
+ indexOfTheFirstItemInTheRow + itemsCountToRenderForMeasurement - 1,
294
+ // Guard against index overflow.
295
+ itemsCount - 1
296
+ )
297
+
298
+ return {
299
+ firstNonMeasuredItemIndex: i,
300
+ firstShownItemIndex,
301
+ lastShownItemIndex
302
+ }
303
+ }
304
+
305
+ /**
306
+ * Finds the indexes of the currently visible items.
307
+ * @return {object} `{ firstShownItemIndex: number, lastShownItemIndex: number, firstNonMeasuredItemIndex: number? }`
308
+ */
309
+ getShownItemIndexes({
310
+ itemsCount,
311
+ visibleAreaTop,
312
+ visibleAreaBottom
313
+ }) {
314
+ let indexes = this._getShownItemIndex({
315
+ itemsCount,
316
+ fromIndex: 0,
317
+ visibleAreaTop,
318
+ visibleAreaBottom,
319
+ findFirstShownItemIndex: true
320
+ })
321
+
322
+ if (indexes === null) {
323
+ return this.getNonVisibleListShownItemIndexes()
324
+ }
325
+
326
+ if (indexes.firstNonMeasuredItemIndex !== undefined) {
327
+ return indexes
328
+ }
329
+
330
+ const { firstShownItemIndex, beforeItemsHeight } = indexes
331
+
332
+ indexes = this._getShownItemIndex({
333
+ itemsCount,
334
+ fromIndex: firstShownItemIndex,
335
+ beforeItemsHeight,
336
+ visibleAreaTop,
337
+ visibleAreaBottom,
338
+ findLastShownItemIndex: true
339
+ })
340
+
341
+ if (indexes === null) {
342
+ return this.getNonVisibleListShownItemIndexes()
343
+ }
344
+
345
+ if (indexes.firstNonMeasuredItemIndex !== undefined) {
346
+ return indexes
347
+ }
348
+
349
+ const { lastShownItemIndex } = indexes
350
+
351
+ return {
352
+ firstShownItemIndex,
353
+ lastShownItemIndex
354
+ }
355
+ }
356
+
357
+ _getShownItemIndex(parameters) {
358
+ const {
359
+ beforeResize,
360
+ itemsCount,
361
+ visibleAreaTop,
362
+ visibleAreaBottom,
363
+ findFirstShownItemIndex,
364
+ findLastShownItemIndex,
365
+ // backwards
366
+ } = parameters
367
+
368
+ let {
369
+ fromIndex,
370
+ beforeItemsHeight
371
+ } = parameters
372
+
373
+ // This function could potentially also use `this.getPreviouslyCalculatedLayout()`
374
+ // in order to skip calculating visible item indexes from scratch
375
+ // and instead just calculate the difference from a "previously calculated layout".
376
+ //
377
+ // I did a simple test in a web browser and found out that running the following
378
+ // piece of code is less than 10 milliseconds:
379
+ //
380
+ // var startedAt = Date.now()
381
+ // var i = 0
382
+ // while (i < 1000000) {
383
+ // i++
384
+ // }
385
+ // console.log(Date.now() - startedAt)
386
+ //
387
+ // Which becomes negligible in my project's use case (a couple thousands items max).
388
+ //
389
+ // If someone would attempt to use a "previously calculated layout" here
390
+ // then `shownItemsHeight` would also have to be returned from this function:
391
+ // the total height of all shown items including vertical spacing between them.
392
+ //
393
+ // If "previously calculated layout" would be used then it would first find
394
+ // `firstShownItemIndex` and then find `lastShownItemIndex` as part of two
395
+ // separate calls of this function, each with or without `backwards` flag,
396
+ // depending on whether `visibleAreaTop` and `visibleAreBottom` have shifted up or down.
397
+
131
398
  let firstShownItemIndex
132
399
  let lastShownItemIndex
133
- let previousRowsHeight = 0
134
- const rowsCount = Math.ceil(itemsCount / columnsCount)
135
- let rowIndex = 0
136
- while (rowIndex < rowsCount) {
137
- const hasMoreRows = itemsCount > (rowIndex + 1) * columnsCount
138
- const verticalSpaceAfterCurrentRow = hasMoreRows ? this.getVerticalSpacing() : 0
400
+
401
+ // It's not always required to pass `beforeItemsHeight` parameter:
402
+ // when `fromIndex` is `0`, it's also assumed to be `0`.
403
+ if (fromIndex === 0) {
404
+ beforeItemsHeight = 0
405
+ }
406
+
407
+ if (beforeItemsHeight === undefined) {
408
+ throw new Error('[virtual-scroller] `beforeItemsHeight` not passed to `Layout.getShownItemIndexes()` when starting from index ' + fromIndex);
409
+ }
410
+
411
+ // const backwards = false
412
+ // while (backwards ? i >= 0 : i < itemsCount) {}
413
+
414
+ if (!beforeResize) {
415
+ const beforeResizeItemsCount = this.getBeforeResizeItemsCount()
416
+ if (beforeResizeItemsCount > fromIndex) {
417
+ // First search for the item in "before resize" items.
418
+ const {
419
+ notFound,
420
+ beforeItemsHeight: beforeResizeItemsHeight,
421
+ firstShownItemIndex,
422
+ lastShownItemIndex
423
+ } = this._getShownItemIndex({
424
+ ...parameters,
425
+ beforeResize: true,
426
+ itemsCount: beforeResizeItemsCount
427
+ })
428
+
429
+ // If the item was not found in "before resize" items
430
+ // then search in regular items skipping "before resize" ones.
431
+ if (notFound) {
432
+ beforeItemsHeight = beforeResizeItemsHeight
433
+ fromIndex += beforeResizeItemsCount
434
+ } else {
435
+ // If the item was found in "before resize" items
436
+ // then return the result.
437
+ // Rebalance first / last shown item indexes based on
438
+ // the current columns count, if required.
439
+ const columnsCount = this.getColumnsCount()
440
+ return {
441
+ firstShownItemIndex: firstShownItemIndex === undefined
442
+ ? undefined
443
+ : Math.floor(firstShownItemIndex / columnsCount) * columnsCount,
444
+ lastShownItemIndex: lastShownItemIndex === undefined
445
+ ? undefined
446
+ : Math.floor(lastShownItemIndex / columnsCount) * columnsCount,
447
+ beforeItemsHeight: beforeResizeItemsHeight
448
+ }
449
+ }
450
+ }
451
+ }
452
+
453
+ const columnsCount = beforeResize ? this.getColumnsCountBeforeResize() : this.getColumnsCount()
454
+ const verticalSpacing = beforeResize ? this.getVerticalSpacingBeforeResize() : this.getVerticalSpacing()
455
+
456
+ let i = fromIndex
457
+ while (i < itemsCount) {
458
+ const currentRowFirstItemIndex = i
459
+
460
+ const hasMoreRows = itemsCount > currentRowFirstItemIndex + columnsCount
461
+ const verticalSpacingAfterCurrentRow = hasMoreRows ? verticalSpacing : 0
462
+
139
463
  let currentRowHeight = 0
464
+
465
+ // Calculate current row height.
140
466
  let columnIndex = 0
141
- let i
142
- while (columnIndex < columnsCount
143
- && (i = rowIndex * columnsCount + columnIndex) < itemsCount) {
144
- const itemHeight = this.getItemHeight(i)
145
- // If an item that hasn't been shown (and measured) yet is encountered
146
- // then show such item and then retry after it has been measured.
467
+ while (columnIndex < columnsCount && i < itemsCount) {
468
+ const itemHeight = beforeResize ? this.getItemHeightBeforeResize(i) : this.getItemHeight(i)
469
+
470
+ // If this item hasn't been measured yet (or re-measured after a resize)
471
+ // then mark it as the first non-measured one.
472
+ //
473
+ // Can't happen by definition when `beforeResize` parameter is `true`.
474
+ //
147
475
  if (itemHeight === undefined) {
148
- log(`Item index ${i} lies within the visible area or its "margins", but its height hasn't been measured yet. Mark the item as "shown", render the list, measure the item's height and redo the layout.`)
149
- if (firstShownItemIndex === undefined) {
150
- firstShownItemIndex = rowIndex * columnsCount
151
- }
152
- const heightLeft = visibleAreaBottom - (listTopOffset + previousRowsHeight)
153
- lastShownItemIndex = Math.min(
154
- (rowIndex + this.getEstimatedRowsCountForHeight(heightLeft)) * columnsCount - 1,
155
- // Guard against index overflow.
156
- itemsCount - 1
157
- )
158
- return {
159
- firstNonMeasuredItemIndex: i,
160
- firstShownItemIndex,
161
- lastShownItemIndex
162
- }
476
+ return this.getItemNotMeasuredIndexes(i, {
477
+ itemsCount,
478
+ firstShownItemIndex: findLastShownItemIndex ? fromIndex : undefined,
479
+ indexOfTheFirstItemInTheRow: currentRowFirstItemIndex,
480
+ nonMeasuredAreaHeight: (visibleAreaBottom + this.getPrerenderMargin()) - beforeItemsHeight
481
+ })
163
482
  }
483
+
164
484
  currentRowHeight = Math.max(currentRowHeight, itemHeight)
165
- // If this is the first item visible
166
- // then start showing items from this row.
167
- if (firstShownItemIndex === undefined) {
168
- if (listTopOffset + previousRowsHeight + currentRowHeight > visibleAreaTop) {
169
- log('First shown row index', rowIndex)
170
- firstShownItemIndex = rowIndex * columnsCount
485
+
486
+ columnIndex++
487
+ i++
488
+ }
489
+
490
+ const itemsHeightFromFirstRowToThisRow = beforeItemsHeight + currentRowHeight
491
+
492
+ const rowStepsIntoVisibleAreaTop = itemsHeightFromFirstRowToThisRow > visibleAreaTop - this.getPrerenderMargin()
493
+ const rowStepsOutOfVisibleAreaBottomOrIsAtTheBorder = itemsHeightFromFirstRowToThisRow + verticalSpacingAfterCurrentRow >= visibleAreaBottom + this.getPrerenderMargin()
494
+
495
+ // if (backwards) {
496
+ // if (findFirstShownItemIndex) {
497
+ // if (rowStepsOutOfVisibleAreaTop) {
498
+ // return {
499
+ // firstShownItemIndex: currentRowFirstItemIndex + columnsCount
500
+ // }
501
+ // }
502
+ // } else if (findLastShownItemIndex) {
503
+ // if (rowStepsIntoVisibleAreaBottom) {
504
+ // return {
505
+ // lastShownItemIndex: currentRowFirstItemIndex + columnsCount - 1
506
+ // }
507
+ // }
508
+ // }
509
+ // }
510
+
511
+ if (findFirstShownItemIndex) {
512
+ if (rowStepsIntoVisibleAreaTop) {
513
+ // If item is the first one visible in the viewport
514
+ // then start showing items from this row.
515
+ return {
516
+ firstShownItemIndex: currentRowFirstItemIndex,
517
+ beforeItemsHeight
171
518
  }
172
519
  }
173
- // If this item is the last one visible in the viewport then exit.
174
- if (listTopOffset + previousRowsHeight + currentRowHeight + verticalSpaceAfterCurrentRow > visibleAreaBottom) {
175
- log('Last shown row index', rowIndex)
176
- // The list height is estimated until all items have been seen,
177
- // so it's possible that even when the list DOM element happens
178
- // to be in the viewport in reality the list isn't visible
179
- // in which case `firstShownItemIndex` will be `undefined`.
180
- if (firstShownItemIndex !== undefined) {
181
- lastShownItemIndex = Math.min(
520
+ } else if (findLastShownItemIndex) {
521
+ if (rowStepsOutOfVisibleAreaBottomOrIsAtTheBorder) {
522
+ return {
523
+ lastShownItemIndex: Math.min(
182
524
  // The index of the last item in the current row.
183
- (rowIndex + 1) * columnsCount - 1,
525
+ currentRowFirstItemIndex + columnsCount - 1,
184
526
  // Guards against index overflow.
185
527
  itemsCount - 1
186
528
  )
187
529
  }
188
- return {
189
- firstShownItemIndex,
190
- lastShownItemIndex
191
- }
192
530
  }
193
- columnIndex++
194
531
  }
195
- previousRowsHeight += currentRowHeight
196
- // If there're more rows below the current row, then add vertical spacing.
197
- previousRowsHeight += verticalSpaceAfterCurrentRow
198
- rowIndex++
199
- }
200
- // If there're no more items then the last item is the last one to show.
201
- if (firstShownItemIndex !== undefined && lastShownItemIndex === undefined) {
202
- lastShownItemIndex = itemsCount - 1
203
- log('Last item index (is fully visible)', lastShownItemIndex)
204
- }
205
- return {
206
- firstShownItemIndex,
207
- lastShownItemIndex
532
+
533
+ beforeItemsHeight += currentRowHeight + verticalSpacingAfterCurrentRow
534
+
535
+ // if (backwards) {
536
+ // // Set `i` to be the first item of the current row.
537
+ // i -= columnsCount
538
+ // const prevoiusRowIsBeforeResize = i - 1 < this.getBeforeResizeItemsCount()
539
+ // const previousRowColumnsCount = prevoiusRowIsBeforeResize ? this.getColumnsCountBeforeResize() : this.getColumnsCount()
540
+ // // Set `i` to be the first item of the previous row.
541
+ // i -= previousRowColumnsCount
542
+ // }
208
543
  }
209
- }
210
544
 
211
- // Finds the items which are displayed in the viewport.
212
- getVisibleItemIndexes(
213
- visibleAreaTop,
214
- visibleAreaBottom,
215
- listTopOffset,
216
- itemsCount
217
- ) {
218
- let {
219
- firstNonMeasuredItemIndex,
220
- firstShownItemIndex,
221
- lastShownItemIndex
222
- } = this._getVisibleItemIndexes(
223
- visibleAreaTop,
224
- visibleAreaBottom,
225
- listTopOffset,
226
- itemsCount
227
- )
228
- const redoLayoutAfterMeasuringItemHeights = firstNonMeasuredItemIndex !== undefined
229
- // If some items will be rendered in order to measure their height,
230
- // and it's not a `preserveScrollPositionOnPrependItems` case,
231
- // then limit the amount of such items being measured in a single pass.
232
- if (redoLayoutAfterMeasuringItemHeights && this.measureItemsBatchSize) {
233
- const maxAllowedLastShownItemIndex = firstNonMeasuredItemIndex + this.measureItemsBatchSize - 1
234
- const columnsCount = this.getColumnsCount()
235
- lastShownItemIndex = Math.min(
236
- // Also guards against index overflow.
237
- lastShownItemIndex,
238
- // The index of the last item in the row.
239
- Math.ceil(maxAllowedLastShownItemIndex / columnsCount) * columnsCount - 1
240
- )
545
+ // if (backwards) {
546
+ // if (findFirstShownItemIndex) {
547
+ // warn('The list is supposed to be visible but no visible item has been found (while traversing backwards)')
548
+ // return null
549
+ // } else if (findLastShownItemIndex) {
550
+ // return {
551
+ // firstShownItemIndex: 0
552
+ // }
553
+ // }
554
+ // }
555
+
556
+ if (beforeResize) {
557
+ return {
558
+ notFound: true,
559
+ beforeItemsHeight
560
+ }
241
561
  }
242
- return {
243
- firstShownItemIndex,
244
- lastShownItemIndex,
245
- redoLayoutAfterMeasuringItemHeights
562
+
563
+ // This case isn't supposed to happen but it could hypothetically happen
564
+ // because the list height is measured from the user's screen and
565
+ // not necessarily can be trusted.
566
+ if (findFirstShownItemIndex) {
567
+ warn('The list is supposed to be visible but no visible item has been found')
568
+ return null
569
+ } else if (findLastShownItemIndex) {
570
+ return {
571
+ lastShownItemIndex: itemsCount - 1
572
+ }
246
573
  }
247
574
  }
248
575
 
249
576
  getNonVisibleListShownItemIndexes() {
250
- return {
577
+ const layout = {
251
578
  firstShownItemIndex: 0,
252
- lastShownItemIndex: 0,
253
- redoLayoutAfterMeasuringItemHeights: this.getItemHeight(0) === undefined
579
+ lastShownItemIndex: 0
254
580
  }
255
- }
256
-
257
- getItemIndexes(
258
- visibleAreaTop,
259
- visibleAreaBottom,
260
- listTopOffset,
261
- listHeight,
262
- itemsCount
263
- ) {
264
- const isVisible = listTopOffset + listHeight > visibleAreaTop && listTopOffset < visibleAreaBottom
265
- if (!isVisible) {
266
- log('The entire list is off-screen. No items are visible.')
267
- return
268
- }
269
- // Find the items which are displayed in the viewport.
270
- const indexes = this.getVisibleItemIndexes(
271
- visibleAreaTop,
272
- visibleAreaBottom,
273
- listTopOffset,
274
- itemsCount
275
- )
276
- // The list height is estimated until all items have been seen,
277
- // so it's possible that even when the list DOM element happens
278
- // to be in the viewport, in reality the list isn't visible
279
- // in which case `firstShownItemIndex` will be `undefined`.
280
- if (indexes.firstShownItemIndex === undefined) {
281
- log('The entire list is off-screen. No items are visible.')
282
- return
581
+ if (this.getItemHeight(0) === undefined) {
582
+ layout.firstNonMeasuredItemIndex = 0
283
583
  }
284
- return indexes
584
+ return layout
285
585
  }
286
586
 
287
587
  /**
288
588
  * Measures "before" items height.
289
- * @param {number} firstShownItemIndexNew first shown item index.
290
- * @param {number} lastShownItemIndex — New last shown item index.
589
+ * @param {number} beforeItemsCountBasically, first shown item index.
291
590
  * @return {number}
292
591
  */
293
592
  getBeforeItemsHeight(
294
- firstShownItemIndex,
295
- lastShownItemIndex
593
+ beforeItemsCount,
594
+ { beforeResize } = {}
296
595
  ) {
297
- const columnsCount = this.getColumnsCount()
298
- const firstShownRowIndex = Math.floor(firstShownItemIndex / columnsCount)
596
+ // This function could potentially also use `this.getPreviouslyCalculatedLayout()`
597
+ // in order to skip calculating visible item indexes from scratch
598
+ // and instead just calculate the difference from a "previously calculated layout".
599
+ //
600
+ // I did a simple test in a web browser and found out that running the following
601
+ // piece of code is less than 10 milliseconds:
602
+ //
603
+ // var startedAt = Date.now()
604
+ // var i = 0
605
+ // while (i < 1000000) {
606
+ // i++
607
+ // }
608
+ // console.log(Date.now() - startedAt)
609
+ //
610
+ // Which becomes negligible in my project's use case (a couple thousands items max).
611
+
299
612
  let beforeItemsHeight = 0
300
- // Add all "before" items height.
301
- let rowIndex = 0
302
- while (rowIndex < firstShownRowIndex) {
613
+ let i = 0
614
+
615
+ if (!beforeResize) {
616
+ const beforeResizeItemsCount = this.getBeforeResizeItemsCount()
617
+
618
+ if (beforeResizeItemsCount > 0) {
619
+ // First add all "before resize" item heights.
620
+ beforeItemsHeight = this.getBeforeItemsHeight(
621
+ // `firstShownItemIndex` (called `beforeItemsCount`) could be greater than
622
+ // `beforeResizeItemsCount` when the user scrolls down.
623
+ // `firstShownItemIndex` (called `beforeItemsCount`) could be less than
624
+ // `beforeResizeItemsCount` when the user scrolls up.
625
+ Math.min(beforeItemsCount, beforeResizeItemsCount),
626
+ { beforeResize: true }
627
+ )
628
+ i = beforeResizeItemsCount
629
+ }
630
+ }
631
+
632
+ const columnsCount = beforeResize ? this.getColumnsCountBeforeResize() : this.getColumnsCount()
633
+ const verticalSpacing = beforeResize ? this.getVerticalSpacingBeforeResize() : this.getVerticalSpacing()
634
+
635
+ while (i < beforeItemsCount) {
636
+ const currentRowFirstItemIndex = i
637
+
303
638
  let rowHeight = 0
304
639
  let columnIndex = 0
640
+ // Not checking for `itemsCount` overflow here because `i = beforeItemsCount`
641
+ // can only start at the start of a row, meaning that when calculating
642
+ // "before items height" it's not supposed to add item heights from the
643
+ // last row of items because in that case it would have to iterate from
644
+ // `i === beforeItemsCount` and that condition is already checked above.
645
+ // while (i < itemsCount) {
305
646
  while (columnIndex < columnsCount) {
306
- rowHeight = Math.max(
307
- rowHeight,
308
- this.getItemHeight(rowIndex * columnsCount + columnIndex)
309
- || this.getAverageItemHeight()
310
- )
647
+ let itemHeight = beforeResize ? this.getItemHeightBeforeResize(i) : this.getItemHeight(i)
648
+ if (itemHeight === undefined) {
649
+ // `itemHeight` can only be `undefined` when not `beforeResize`.
650
+ // Use the current "average item height" as a substitute.
651
+ itemHeight = this.getAverageItemHeight()
652
+ }
653
+ rowHeight = Math.max(rowHeight, itemHeight)
654
+ i++
311
655
  columnIndex++
312
656
  }
657
+
313
658
  beforeItemsHeight += rowHeight
314
- beforeItemsHeight += this.getVerticalSpacing()
315
- rowIndex++
659
+ beforeItemsHeight += verticalSpacing
316
660
  }
661
+
317
662
  return beforeItemsHeight
318
663
  }
319
664
 
320
665
  /**
321
666
  * Measures "after" items height.
322
- * @param {number} firstShownItemIndexNew first shown item index.
323
- * @param {number} lastShownItemIndex — New last shown item index.
324
- * @param {number} averageItemHeight — Average item height.
325
- * @param {number} verticalSpacing — Item vertical spacing.
667
+ * @param {number} lastShownItemIndexLast shown item index.
326
668
  * @param {number} itemsCount — Items count.
327
669
  * @return {number}
328
670
  */
329
671
  getAfterItemsHeight(
330
- firstShownItemIndex,
331
672
  lastShownItemIndex,
332
673
  itemsCount
333
674
  ) {
675
+ // This function could potentially also use `this.getPreviouslyCalculatedLayout()`
676
+ // in order to skip calculating visible item indexes from scratch
677
+ // and instead just calculate the difference from a "previously calculated layout".
678
+ //
679
+ // I did a simple test in a web browser and found out that running the following
680
+ // piece of code is less than 10 milliseconds:
681
+ //
682
+ // var startedAt = Date.now()
683
+ // var i = 0
684
+ // while (i < 1000000) {
685
+ // i++
686
+ // }
687
+ // console.log(Date.now() - startedAt)
688
+ //
689
+ // Which becomes negligible in my project's use case (a couple thousands items max).
690
+
334
691
  const columnsCount = this.getColumnsCount()
335
- const rowsCount = Math.ceil(itemsCount / columnsCount)
336
692
  const lastShownRowIndex = Math.floor(lastShownItemIndex / columnsCount)
693
+
337
694
  let afterItemsHeight = 0
338
- let rowIndex = lastShownRowIndex + 1
339
- while (rowIndex < rowsCount) {
695
+
696
+ let i = lastShownItemIndex + 1
697
+ while (i < itemsCount) {
340
698
  let rowHeight = 0
341
699
  let columnIndex = 0
342
- let i
343
- while (columnIndex < columnsCount
344
- && (i = rowIndex * columnsCount + columnIndex) < itemsCount) {
345
- rowHeight = Math.max(
346
- rowHeight,
347
- this.getItemHeight(i) || this.getAverageItemHeight()
348
- )
700
+ while (columnIndex < columnsCount && i < itemsCount) {
701
+ let itemHeight = this.getItemHeight(i)
702
+ if (itemHeight === undefined) {
703
+ itemHeight = this.getAverageItemHeight()
704
+ }
705
+ rowHeight = Math.max(rowHeight, itemHeight)
706
+ i++
349
707
  columnIndex++
350
708
  }
709
+
351
710
  // Add all "after" items height.
352
711
  afterItemsHeight += this.getVerticalSpacing()
353
712
  afterItemsHeight += rowHeight
354
- rowIndex++
355
713
  }
714
+
356
715
  return afterItemsHeight
357
716
  }
358
717
 
359
718
  /**
360
- * Finds the indexes of the currently visible items.
361
- * @return {object} `{ firstShownItemIndex: number, lastShownItemIndex: number, redoLayoutAfterMeasuringItemHeights: boolean }`
719
+ * Returns the items's top offset relative to the top edge of the first item.
720
+ * @param {number} i Item index
721
+ * @return {[number]} Returns `undefined` if any of the previous items haven't been rendered yet.
362
722
  */
363
- getShownItemIndexes({
364
- listHeight,
365
- itemsCount,
366
- visibleAreaIncludingMargins,
367
- listTopOffsetInsideScrollableContainer
368
- }) {
369
- if (this.bypass) {
370
- return {
371
- firstShownItemIndex: 0,
372
- lastShownItemIndex: itemsCount - 1
723
+ getItemTopOffset(i) {
724
+ let topOffsetInsideScrollableContainer = 0
725
+
726
+ const beforeResizeItemsCount = this.getBeforeResizeItemsCount()
727
+ const beforeResizeRowsCount = beforeResizeItemsCount === 0
728
+ ? 0
729
+ : Math.ceil(beforeResizeItemsCount / this.getColumnsCountBeforeResize())
730
+
731
+ const maxBeforeResizeRowsCount = i < beforeResizeItemsCount
732
+ ? Math.floor(i / this.getColumnsCountBeforeResize())
733
+ : beforeResizeRowsCount
734
+
735
+ let beforeResizeRowIndex = 0
736
+ while (beforeResizeRowIndex < maxBeforeResizeRowsCount) {
737
+ const rowHeight = this.getItemHeightBeforeResize(
738
+ beforeResizeRowIndex * this.getColumnsCountBeforeResize()
739
+ )
740
+
741
+ topOffsetInsideScrollableContainer += rowHeight
742
+ topOffsetInsideScrollableContainer += this.getVerticalSpacingBeforeResize()
743
+
744
+ beforeResizeRowIndex++
745
+ }
746
+
747
+ const itemRowIndex = Math.floor((i - beforeResizeItemsCount) / this.getColumnsCount())
748
+
749
+ let rowIndex = 0
750
+ while (rowIndex < itemRowIndex) {
751
+ let rowHeight = 0
752
+ let columnIndex = 0
753
+ while (columnIndex < this.getColumnsCount()) {
754
+ const itemHeight = this.getItemHeight(
755
+ beforeResizeItemsCount + rowIndex * this.getColumnsCount() + columnIndex
756
+ )
757
+ if (itemHeight === undefined) {
758
+ return
759
+ }
760
+ rowHeight = Math.max(rowHeight, itemHeight)
761
+ columnIndex++
373
762
  }
763
+
764
+ topOffsetInsideScrollableContainer += rowHeight
765
+ topOffsetInsideScrollableContainer += this.getVerticalSpacing()
766
+
767
+ rowIndex++
374
768
  }
375
- // Finds the indexes of the items that are currently visible
376
- // (or close to being visible) in the scrollable container.
377
- // For scrollable containers other than the main screen, it could also
378
- // check the visibility of such scrollable container itself, because it
379
- // might be not visible.
380
- // If such kind of an optimization would hypothetically be implemented,
381
- // then it would also require listening for "scroll" events on the screen.
382
- // Overall, I suppose that such "actual visibility" feature would be
383
- // a very minor optimization and not something I'd deal with.
384
- return this.getItemIndexes(
385
- visibleAreaIncludingMargins.top,
386
- visibleAreaIncludingMargins.bottom,
387
- listTopOffsetInsideScrollableContainer,
388
- listHeight,
389
- itemsCount
390
- ) || this.getNonVisibleListShownItemIndexes()
391
- }
392
769
 
393
- showItemsFromTheStart(layout) {
394
- layout.firstShownItemIndex = 0
395
- layout.beforeItemsHeight = 0
770
+ return topOffsetInsideScrollableContainer
396
771
  }
397
772
  }
398
773
 
@@ -400,9 +775,11 @@ export const LAYOUT_REASON = {
400
775
  SCROLL: 'scroll',
401
776
  STOPPED_SCROLLING: 'stopped scrolling',
402
777
  MANUAL: 'manual',
403
- MOUNT: 'mount',
404
- ITEM_HEIGHT_NOT_MEASURED: 'some item height wasn\'t measured',
405
- RESIZE: 'resize',
778
+ STARTED: 'started',
779
+ NON_MEASURED_ITEMS_HAVE_BEEN_MEASURED: 'non-measured item heights have been measured',
780
+ VIEWPORT_WIDTH_CHANGED: 'viewport width changed',
781
+ VIEWPORT_HEIGHT_CHANGED: 'viewport height changed',
782
+ VIEWPORT_SIZE_UNCHANGED: 'viewport size unchanged',
406
783
  ITEM_HEIGHT_CHANGED: 'item height changed',
407
784
  ITEMS_CHANGED: 'items changed',
408
785
  TOP_OFFSET_CHANGED: 'list top offset changed'