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/Scroll.js CHANGED
@@ -4,72 +4,67 @@
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'
8
- import log from './utility/debug'
7
+ import log from './utility/debug.js'
9
8
 
10
9
  export default class Scroll {
11
10
  constructor({
12
11
  bypass,
13
12
  scrollableContainer,
14
- updateLayout,
13
+ itemsContainer,
14
+ onScroll,
15
15
  initialScrollPosition,
16
16
  onScrollPositionChange,
17
17
  isImmediateLayoutScheduled,
18
18
  hasNonRenderedItemsAtTheTop,
19
19
  hasNonRenderedItemsAtTheBottom,
20
- getLatestLayoutVisibleAreaIncludingMargins,
21
- preserveScrollPositionOfTheBottomOfTheListOnMount
20
+ getLatestLayoutVisibleArea,
21
+ getListTopOffset,
22
+ getPrerenderMargin,
23
+ onScrolledToTop,
24
+ waitForScrollingToStop
22
25
  }) {
23
26
  this.bypass = bypass
24
27
  this.scrollableContainer = scrollableContainer
25
- this.updateLayout = updateLayout
28
+ this.itemsContainer = itemsContainer
29
+ this.onScroll = onScroll
26
30
  this.initialScrollPosition = initialScrollPosition
27
31
  this.onScrollPositionChange = onScrollPositionChange
28
32
  this.isImmediateLayoutScheduled = isImmediateLayoutScheduled
29
33
  this.hasNonRenderedItemsAtTheTop = hasNonRenderedItemsAtTheTop
30
34
  this.hasNonRenderedItemsAtTheBottom = hasNonRenderedItemsAtTheBottom
31
- this.getLatestLayoutVisibleAreaIncludingMargins = getLatestLayoutVisibleAreaIncludingMargins
32
-
33
- if (preserveScrollPositionOfTheBottomOfTheListOnMount) {
34
- if (scrollableContainer) {
35
- this.preserveScrollPositionOfTheBottomOfTheListOnMount = {
36
- scrollableContainerContentHeight: scrollableContainer.getContentHeight()
37
- }
38
- }
39
- }
35
+ this.getLatestLayoutVisibleArea = getLatestLayoutVisibleArea
36
+ this.getListTopOffset = getListTopOffset
37
+ this.getPrerenderMargin = getPrerenderMargin
38
+ this.onScrolledToTop = onScrolledToTop
39
+ this.waitForScrollingToStop = waitForScrollingToStop
40
40
  }
41
41
 
42
- listen() {
42
+ start() {
43
43
  if (this.initialScrollPosition !== undefined) {
44
44
  this.scrollToY(this.initialScrollPosition)
45
+ this.initialScrollPosition = undefined
45
46
  }
46
47
  if (this.onScrollPositionChange) {
47
- this.updateScrollPosition()
48
- this.removeScrollPositionListener = this.scrollableContainer.addScrollListener(this.updateScrollPosition)
49
- }
50
- if (!this.bypass) {
51
- this.removeScrollListener = this.scrollableContainer.addScrollListener(this.onScroll)
52
- }
53
- if (this.preserveScrollPositionOfTheBottomOfTheListOnMount) {
54
- this.scrollToY(this.getScrollY() + (this.scrollableContainer.getContentHeight() - this.preserveScrollPositionOfTheBottomOfTheListOnMount.scrollableContainerContentHeight))
48
+ this.onScrollPositionChange(this.getScrollY())
55
49
  }
50
+ this.stopListeningToScroll = this.scrollableContainer.onScroll(this.onScrollListener)
56
51
  }
57
52
 
58
53
  stop() {
59
- if (this.removeScrollPositionListener) {
60
- this.removeScrollPositionListener()
61
- }
62
- if (this.removeScrollListener) {
63
- this.removeScrollListener()
64
- }
65
- this.cancelOnUserStopsScrollingTimer()
54
+ this.stopListeningToScroll()
55
+ this.stopListeningToScroll = undefined
56
+ // this.onStopScrollingListener = undefined
57
+ this.shouldCallOnScrollListenerWhenStopsScrolling = undefined
58
+ this.cancelOnStopScrollingTimer()
66
59
  }
67
60
 
68
61
  scrollToY(scrollY) {
62
+ this.ignoreScrollEvents = true
69
63
  this.scrollableContainer.scrollToY(scrollY)
64
+ this.ignoreScrollEvents = undefined
70
65
  }
71
66
 
72
- scrollByY(scrollByY) {
67
+ scrollByY = (scrollByY) => {
73
68
  this.scrollToY(this.getScrollY() + scrollByY)
74
69
  }
75
70
 
@@ -77,88 +72,146 @@ export default class Scroll {
77
72
  return this.scrollableContainer.getScrollY()
78
73
  }
79
74
 
80
- /**
81
- * Updates the current scroll Y position in state.
82
- */
83
- updateScrollPosition = () => {
84
- this.onScrollPositionChange(this.getScrollY())
85
- }
86
-
87
- cancelOnUserStopsScrollingTimer() {
88
- if (this.onUserStopsScrollingTimer) {
89
- clearTimeout(this.onUserStopsScrollingTimer)
90
- this.onUserStopsScrollingTimer = undefined
75
+ cancelOnStopScrollingTimer() {
76
+ if (this.onStopScrollingTimer) {
77
+ clearTimeout(this.onStopScrollingTimer)
78
+ this.onStopScrollingTimer = undefined
91
79
  }
92
80
  }
93
81
 
94
- onLayout() {
82
+ cancelScheduledLayout() {
95
83
  // Cancel a "re-layout when user stops scrolling" timer.
96
- this.cancelOnUserStopsScrollingTimer()
84
+ this.cancelOnStopScrollingTimer()
97
85
  }
98
86
 
99
- onScroll = () => {
87
+ onScrollListener = () => {
88
+ if (this.onScrollPositionChange) {
89
+ this.onScrollPositionChange(this.getScrollY())
90
+ }
91
+
92
+ // If the user has scrolled up to the top of the items container.
93
+ // (this option isn't currently used)
94
+ if (this.onScrolledToTop) {
95
+ if (this.getScrollY() < this.getListTopOffset()) {
96
+ this.onScrolledToTop()
97
+ }
98
+ }
99
+
100
+ if (this.bypass) {
101
+ return
102
+ }
103
+
104
+ if (this.ignoreScrollEvents) {
105
+ return
106
+ }
107
+
100
108
  // Prefer not performing a re-layout while the user is scrolling (if possible).
101
109
  // If the user doesn't scroll too far and then stops for a moment,
102
110
  // then a mid-scroll re-layout could be delayed until such a brief stop:
103
111
  // presumably, this results in better (smoother) scrolling performance,
104
112
  // delaying the work to when it doesn't introduce any stutter or "jank".
105
113
 
106
- // Reset `this.onUserStopsScrollingTimer` (will be re-created below).
107
- this.cancelOnUserStopsScrollingTimer()
114
+ // Reset `this.onStopScrollingTimer` (will be re-created below).
115
+ this.cancelOnStopScrollingTimer()
108
116
 
109
- // See whether rendering "new" previous/next items is required
110
- // right now, or it can wait until the user stops scrolling.
117
+ // See if the latest "layout" (the currently rendered set of items)
118
+ // is still sufficient in order to show all the items that're
119
+ // currently inside the viewport. If there're some non-rendered items
120
+ // that're visible in the current viewport, then those items
121
+ // should be rendered "immediately" rather than waiting until
122
+ // the user stops scrolling.
111
123
  const forceUpdate =
112
124
  // If the items have been rendered at least once
113
- this.getLatestLayoutVisibleAreaIncludingMargins() && (
125
+ this.getLatestLayoutVisibleArea() && (
114
126
  (
115
- // If the user has scrolled up past the extra "margin"
116
- (this.getScrollY() < this.getLatestLayoutVisibleAreaIncludingMargins().top) &&
117
- // and if there're any previous non-rendered items to render.
127
+ // If the user has scrolled up past the "prerender margin"
128
+ // and there're some non-rendered items at the top,
129
+ // then force a re-layout.
130
+ //
131
+ // (during these calculations we assume that the list's top coordinate
132
+ // hasn't changed since previous layout; even if that's not exactly true,
133
+ // the items will be re-layout when the user stops scrolling anyway)
134
+ //
135
+ (this.getScrollY() < this.getLatestLayoutVisibleArea().top - this.getPrerenderMargin()) &&
118
136
  this.hasNonRenderedItemsAtTheTop()
119
137
  )
120
138
  ||
121
139
  (
122
- // If the user has scrolled down past the extra "margin"
123
- (this.getScrollY() + this.scrollableContainer.getHeight() > this.getLatestLayoutVisibleAreaIncludingMargins().bottom) &&
124
- // and if there're any next non-rendered items to render.
140
+ // If the user has scrolled down past the "prerender margin"
141
+ // and there're any non-rendered items left at the end,
142
+ // then force a re-layout.
143
+ //
144
+ // (during these calculations we assume that the list's top coordinate
145
+ // hasn't changed since previous layout; even if that's not exactly true,
146
+ // the items will be re-layout when the user stops scrolling anyway)
147
+ //
148
+ (this.getScrollY() + this.scrollableContainer.getHeight() > this.getLatestLayoutVisibleArea().bottom + this.getPrerenderMargin()) &&
125
149
  this.hasNonRenderedItemsAtTheBottom()
126
150
  )
127
151
  )
128
152
 
129
153
  if (forceUpdate) {
130
- log('The user has scrolled far enough: force re-layout')
154
+ log('The user has scrolled far enough: perform a re-layout')
131
155
  } else {
132
- log('The user hasn\'t scrolled too much: delay re-layout')
156
+ log('The user is scrolling: perform a re-layout when they stop scrolling')
133
157
  }
134
158
 
135
- if (!forceUpdate) {
136
- // If a re-layout is already scheduled at the next "frame",
137
- // don't schedule a "re-layout when user stops scrolling" timer.
138
- if (this.isImmediateLayoutScheduled()) {
139
- return
140
- }
141
- this.onUserStopsScrollingTimer = setTimeout(
142
- () => {
143
- this.onUserStopsScrollingTimer = undefined
144
- this.updateLayout({ reason: LAYOUT_REASON.STOPPED_SCROLLING })
145
- },
146
- // "scroll" events are usually dispatched every 16 milliseconds
147
- // for 60fps refresh rate, so waiting for 100 milliseconds feels
148
- // reasonable: that would be about 6 frames of inactivity period,
149
- // which could mean that either the user has stopped scrolling
150
- // (for a moment) or the browser is lagging and stuttering
151
- // (skipping frames due to high load).
152
- // If the user continues scrolling then this timeout is constantly
153
- // refreshed (cancelled and then re-created).
154
- WAIT_FOR_USER_TO_STOP_SCROLLING_TIMEOUT
155
- )
159
+ if (forceUpdate || this.waitForScrollingToStop === false) {
160
+ return this.onScroll()
161
+ }
162
+
163
+ // If a re-layout is already scheduled at the next "frame",
164
+ // don't schedule a "re-layout when user stops scrolling" timer.
165
+ if (this.isImmediateLayoutScheduled()) {
156
166
  return
157
167
  }
158
168
 
159
- this.updateLayout({ reason: LAYOUT_REASON.SCROLL })
169
+ this.shouldCallOnScrollListenerWhenStopsScrolling = true
170
+ this.watchOnStopScrolling()
171
+ }
172
+
173
+ watchOnStopScrolling() {
174
+ this.onStopScrollingTimer = setTimeout(
175
+ () => {
176
+ this.onStopScrollingTimer = undefined
177
+
178
+ if (this.shouldCallOnScrollListenerWhenStopsScrolling) {
179
+ this.shouldCallOnScrollListenerWhenStopsScrolling = undefined
180
+ this.onScroll({ delayed: true })
181
+ }
182
+
183
+ // `onStopScrolling()` feature is not currently used.
184
+ // if (this.onStopScrollingListener) {
185
+ // const onStopScrollingListener = this.onStopScrollingListener
186
+ // this.onStopScrollingListener = undefined
187
+ // // `onStopScrollingListener()` may hypothetically schedule
188
+ // // another `onStopScrolling()` listener, so set
189
+ // // `this.onStopScrollingListener` to `undefined` before
190
+ // // calling it rather than after.
191
+ // log('~ The user has stopped scrolling ~')
192
+ // onStopScrollingListener()
193
+ // }
194
+ },
195
+ // "scroll" events are usually dispatched every 16 milliseconds
196
+ // for 60fps refresh rate, so waiting for 100 milliseconds feels
197
+ // reasonable: that would be about 6 frames of inactivity period,
198
+ // which could mean that either the user has stopped scrolling
199
+ // (for a moment) or the browser is lagging and stuttering
200
+ // (skipping frames due to high load).
201
+ // If the user continues scrolling then this timeout is constantly
202
+ // refreshed (cancelled and then re-created).
203
+ ON_STOP_SCROLLING_INACTIVE_PERIOD
204
+ )
160
205
  }
161
206
 
207
+ // (this function isn't currently used)
208
+ // onStopScrolling(onStopScrollingListener) {
209
+ // this.onStopScrollingListener = onStopScrollingListener
210
+ // if (!this.onStopScrollingTimer) {
211
+ // this.watchOnStopScrolling()
212
+ // }
213
+ // }
214
+
162
215
  /**
163
216
  * Returns visible area coordinates relative to the scrollable container.
164
217
  * @return {object} `{ top: number, bottom: number }`
@@ -174,4 +227,4 @@ export default class Scroll {
174
227
  }
175
228
  }
176
229
 
177
- const WAIT_FOR_USER_TO_STOP_SCROLLING_TIMEOUT = 100
230
+ const ON_STOP_SCROLLING_INACTIVE_PERIOD = 100
@@ -0,0 +1,26 @@
1
+ export default function createColumnsHelpers({ getColumnsCount }) {
2
+ if (getColumnsCount) {
3
+ const scrollableContainerArgument = {
4
+ getWidth: () => this.scrollableContainer.getWidth()
5
+ }
6
+ this.getActualColumnsCountForState = () => {
7
+ const columnsCount = getColumnsCount(scrollableContainerArgument)
8
+ // `columnsCount: 1` is effectively same as `columnsCount: undefined`
9
+ // from the code's point of view. This makes one less property in `state`
10
+ // which makes `state` a bit less cluttered (easier for inspection).
11
+ if (columnsCount !== 1) {
12
+ return columnsCount
13
+ }
14
+ }
15
+ } else {
16
+ this.getActualColumnsCountForState = () => undefined
17
+ }
18
+
19
+ this.getActualColumnsCount = () => {
20
+ return this.getActualColumnsCountForState() || 1
21
+ }
22
+
23
+ this.getColumnsCount = () => {
24
+ return this.getState() && this.getState().columnsCount || 1
25
+ }
26
+ }
@@ -0,0 +1,336 @@
1
+ import {
2
+ supportsTbody,
3
+ BROWSER_NOT_SUPPORTED_ERROR
4
+ } from './DOM/tbody.js'
5
+
6
+ import DOMEngine from './DOM/Engine.js'
7
+
8
+ import Layout, { LAYOUT_REASON } from './Layout.js'
9
+ import Resize from './Resize.js'
10
+ import BeforeResize from './BeforeResize.js'
11
+ import Scroll from './Scroll.js'
12
+ import ListHeightMeasurement from './ListHeightMeasurement.js'
13
+ import ItemHeights from './ItemHeights.js'
14
+
15
+ import log, { warn, isDebug, reportError } from './utility/debug.js'
16
+
17
+ import createStateHelpers from './VirtualScroller.state.js'
18
+ import createVerticalSpacingHelpers from './VirtualScroller.verticalSpacing.js'
19
+ import createColumnsHelpers from './VirtualScroller.columns.js'
20
+ import createLayoutHelpers from './VirtualScroller.layout.js'
21
+ import createOnRenderHelpers from './VirtualScroller.onRender.js'
22
+ import createResizeHelpers from './VirtualScroller.resize.js'
23
+ import createItemsHelpers from './VirtualScroller.items.js'
24
+
25
+ /**
26
+ * @param {function} getItemsContainerElement — Returns the container DOM `Element`.
27
+ * @param {any[]} items — The list of items.
28
+ * @param {Object} [options] — See README.md.
29
+ * @return {VirtualScroller}
30
+ */
31
+ export default function VirtualScrollerConstructor(
32
+ getItemsContainerElement,
33
+ items,
34
+ options = {}
35
+ ) {
36
+ const {
37
+ render,
38
+ state,
39
+ onStateChange,
40
+ initialScrollPosition,
41
+ onScrollPositionChange,
42
+ // `scrollableContainer` option is deprecated.
43
+ // Use `getScrollableContainer()` option instead.
44
+ scrollableContainer,
45
+ measureItemsBatchSize = 50,
46
+ getColumnsCount,
47
+ getItemId,
48
+ tbody,
49
+ estimatedItemHeight,
50
+ onItemInitialRender,
51
+ // `onItemFirstRender(i)` is deprecated, use `onItemInitialRender(item)` instead.
52
+ onItemFirstRender,
53
+ _useTimeoutInRenderLoop,
54
+ _waitForScrollingToStop,
55
+ engine
56
+ } = options
57
+
58
+ let {
59
+ bypass,
60
+ getScrollableContainer
61
+ } = options
62
+
63
+ log('~ Initialize ~')
64
+
65
+ // Could support non-DOM rendering engines.
66
+ // For example, React Native, `<canvas/>`, etc.
67
+ this.engine = engine || DOMEngine
68
+
69
+ // `scrollableContainer` option is deprecated.
70
+ // Use `getScrollableContainer()` option instead.
71
+ if (!getScrollableContainer && scrollableContainer) {
72
+ getScrollableContainer = () => scrollableContainer
73
+ }
74
+
75
+ // Sometimes, when `new VirtualScroller()` instance is created,
76
+ // `getItemsContainerElement()` might not be ready to return the "container" DOM Element yet
77
+ // (for example, because it's not rendered yet). That's the reason why it's a getter function.
78
+ // For example, in React `<VirtualScroller/>` component, a `VirtualScroller`
79
+ // instance is created in the React component's `constructor()`, and at that time
80
+ // the container Element is not yet available. The container Element is available
81
+ // in `componentDidMount()`, but `componentDidMount()` is not executed on server,
82
+ // which would mean that React `<VirtualScroller/>` wouldn't render at all
83
+ // on server side, while with the `getItemsContainerElement()` approach, on server side,
84
+ // it still "renders" a list with a predefined amount of items in it by default.
85
+ // (`initiallyRenderedItemsCount`, or `1`).
86
+ this.getItemsContainerElement = getItemsContainerElement
87
+
88
+ // if (prerenderMargin === undefined) {
89
+ // // Renders items which are outside of the screen by this "prerender margin".
90
+ // // Is the screen height by default: seems to be the optimal value
91
+ // // for "Page Up" / "Page Down" navigation and optimized mouse wheel scrolling.
92
+ // prerenderMargin = this.scrollableContainer ? this.scrollableContainer.getHeight() : 0
93
+ // }
94
+
95
+ if (options.getState || options.setState) {
96
+ throw new Error('[virtual-scroller] `getState`/`setState` options usage has changed in the new version. See the readme for more details.')
97
+ }
98
+
99
+ // Work around `<tbody/>` not being able to have `padding`.
100
+ // https://gitlab.com/catamphetamine/virtual-scroller/-/issues/1
101
+ if (tbody) {
102
+ if (this.engine !== DOMEngine) {
103
+ throw new Error('[virtual-scroller] `tbody` option is only supported for DOM rendering engine')
104
+ }
105
+ log('~ <tbody/> detected ~')
106
+ this.tbody = true
107
+ if (!supportsTbody()) {
108
+ log('~ <tbody/> not supported ~')
109
+ reportError(BROWSER_NOT_SUPPORTED_ERROR)
110
+ bypass = true
111
+ }
112
+ }
113
+
114
+ if (bypass) {
115
+ log('~ "bypass" mode ~')
116
+ }
117
+
118
+ // In `bypass` mode, `VirtualScroller` doesn't wait
119
+ // for the user to scroll down to render all items:
120
+ // instead, it renders all items right away, as if
121
+ // the list is rendered without using `VirtualScroller`.
122
+ // It was added just to measure how much is the
123
+ // performance difference between using a `VirtualScroller`
124
+ // and not using a `VirtualScroller`.
125
+ // It turned out that unmounting large React component trees
126
+ // is a very long process, so `VirtualScroller` does seem to
127
+ // make sense when used in a React application.
128
+ this.bypass = bypass
129
+ // this.bypassBatchSize = bypassBatchSize || 10
130
+
131
+ // Using `setTimeout()` in render loop is a workaround
132
+ // for avoiding a React error message:
133
+ // "Maximum update depth exceeded.
134
+ // This can happen when a component repeatedly calls
135
+ // `.setState()` inside `componentWillUpdate()` or `componentDidUpdate()`.
136
+ // React limits the number of nested updates to prevent infinite loops."
137
+ this._useTimeoutInRenderLoop = _useTimeoutInRenderLoop
138
+
139
+ if (getItemId) {
140
+ this.isItemEqual = (a, b) => getItemId(a) === getItemId(b)
141
+ } else {
142
+ this.isItemEqual = (a, b) => a === b
143
+ }
144
+
145
+ if (onItemInitialRender) {
146
+ this.onItemInitialRender = onItemInitialRender
147
+ }
148
+ // `onItemFirstRender(i)` is deprecated, use `onItemInitialRender(item)` instead.
149
+ else if (onItemFirstRender) {
150
+ this.onItemInitialRender = (item) => {
151
+ warn('`onItemFirstRender(i)` is deprecated, use `onItemInitialRender(item)` instead.')
152
+ const { items } = this.getState()
153
+ const i = items.indexOf(item)
154
+ // The `item` could also be non-found due to the inconsistency bug:
155
+ // The reason is that `i` can be non-consistent with the `items`
156
+ // passed to `<VirtualScroller/>` in React due to `updateState()` not being
157
+ // instanteneous: when new `items` are passed to `<VirtualScroller/>`,
158
+ // `VirtualScroller.updateState({ items })` is called, and if `onItemFirstRender(i)`
159
+ // is called after the aforementioned `updateState()` is called but before it finishes,
160
+ // `i` would point to an index in "previous" `items` while the application
161
+ // would assume that `i` points to an index in the "new" `items`,
162
+ // resulting in an incorrect item being assumed by the application
163
+ // or even in an "array index out of bounds" error.
164
+ if (i >= 0) {
165
+ onItemFirstRender(i)
166
+ }
167
+ }
168
+ }
169
+
170
+ // If initial `state` is passed then use `items` from `state`
171
+ // instead of the `items` argument.
172
+ if (state) {
173
+ items = state.items
174
+ }
175
+
176
+ log('Items count', items.length)
177
+ if (estimatedItemHeight) {
178
+ log('Estimated item height', estimatedItemHeight)
179
+ }
180
+
181
+ createStateHelpers.call(this, { state, onStateChange, render, items })
182
+
183
+ createVerticalSpacingHelpers.call(this)
184
+ createColumnsHelpers.call(this, { getColumnsCount })
185
+
186
+ createLayoutHelpers.call(this)
187
+ createOnRenderHelpers.call(this)
188
+ createResizeHelpers.call(this)
189
+ createItemsHelpers.call(this)
190
+
191
+ createHelpers.call(this, {
192
+ getScrollableContainer,
193
+ estimatedItemHeight,
194
+ measureItemsBatchSize,
195
+ initialScrollPosition,
196
+ onScrollPositionChange,
197
+ waitForScrollingToStop: _waitForScrollingToStop
198
+ })
199
+
200
+ if (state) {
201
+ // Initialize `ItemHeights` from previously measured `state.itemHeights`.
202
+ this.itemHeights.readItemHeightsFromState(state)
203
+
204
+ // Initialize some `BeforeResize` internal flags from a previously saved state.
205
+ this.beforeResize.initializeFromState(state)
206
+ }
207
+ }
208
+
209
+ function createHelpers({
210
+ getScrollableContainer,
211
+ estimatedItemHeight,
212
+ measureItemsBatchSize,
213
+ initialScrollPosition,
214
+ onScrollPositionChange,
215
+ waitForScrollingToStop
216
+ }) {
217
+ this.itemsContainer = this.engine.createItemsContainer(
218
+ this.getItemsContainerElement
219
+ )
220
+
221
+ // If the items "container" element is mounted at this stage,
222
+ // remove any accidental text nodes from it (like whitespace).
223
+ //
224
+ // Also, this guards against cases when someone accidentally tries
225
+ // using `VirtualScroller` on a non-empty element.
226
+ //
227
+ if (this.getItemsContainerElement()) {
228
+ this.itemsContainer.clear()
229
+ }
230
+
231
+ this.scrollableContainer = this.engine.createScrollableContainer(
232
+ getScrollableContainer,
233
+ this.getItemsContainerElement
234
+ )
235
+
236
+ // Create `ItemHeights` instance.
237
+ this.itemHeights = new ItemHeights({
238
+ container: this.itemsContainer,
239
+ getItemHeight: (i) => this.getState().itemHeights[i],
240
+ setItemHeight: (i, height) => this.getState().itemHeights[i] = height
241
+ })
242
+
243
+ this.layout = new Layout({
244
+ bypass: this.bypass,
245
+ estimatedItemHeight,
246
+ measureItemsBatchSize,
247
+ getPrerenderMargin: () => this.getPrerenderMargin(),
248
+ getVerticalSpacing: () => this.getVerticalSpacing(),
249
+ getVerticalSpacingBeforeResize: () => this.getVerticalSpacingBeforeResize(),
250
+ getColumnsCount: () => this.getColumnsCount(),
251
+ getColumnsCountBeforeResize: () => this.getState().beforeResize && this.getState().beforeResize.columnsCount,
252
+ getItemHeight: (i) => this.getState().itemHeights[i],
253
+ getItemHeightBeforeResize: (i) => this.getState().beforeResize && this.getState().beforeResize.itemHeights[i],
254
+ getBeforeResizeItemsCount: () => this.getState().beforeResize ? this.getState().beforeResize.itemHeights.length : 0,
255
+ getAverageItemHeight: () => this.itemHeights.getAverage(),
256
+ getMaxVisibleAreaHeight: () => this.scrollableContainer && this.scrollableContainer.getHeight(),
257
+ //
258
+ // The "previously calculated layout" feature is not currently used.
259
+ //
260
+ // The current layout snapshot could be stored as a "previously calculated layout" variable
261
+ // so that it could theoretically be used when calculating new layout incrementally
262
+ // rather than from scratch, which would be an optimization.
263
+ //
264
+ getPreviouslyCalculatedLayout: () => this.previouslyCalculatedLayout
265
+ })
266
+
267
+ this.resize = new Resize({
268
+ bypass: this.bypass,
269
+ getWidth: () => this.scrollableContainer.getWidth(),
270
+ getHeight: () => this.scrollableContainer.getHeight(),
271
+ listenForResize: (listener) => this.scrollableContainer.onResize(listener),
272
+ onResizeStart: () => {
273
+ log('~ Scrollable container resize started ~')
274
+ this._isResizing = true
275
+ },
276
+ onResizeStop: () => {
277
+ log('~ Scrollable container resize finished ~')
278
+ this._isResizing = undefined
279
+ },
280
+ onNoChange: () => {
281
+ // There might have been some missed `this.onUpdateShownItemIndexes()` calls
282
+ // due to setting `this._isResizing` flag to `true` during the resize.
283
+ // So, update shown item indexes just in case.
284
+ this.onUpdateShownItemIndexes({
285
+ reason: LAYOUT_REASON.VIEWPORT_SIZE_UNCHANGED
286
+ })
287
+ },
288
+ onHeightChange: () => this.onUpdateShownItemIndexes({
289
+ reason: LAYOUT_REASON.VIEWPORT_HEIGHT_CHANGED
290
+ }),
291
+ onWidthChange: (prevWidth, newWidth) => {
292
+ log('~ Scrollable container width changed from', prevWidth, 'to', newWidth, '~')
293
+ this.onResize()
294
+ }
295
+ })
296
+
297
+ this.scroll = new Scroll({
298
+ bypass: this.bypass,
299
+ scrollableContainer: this.scrollableContainer,
300
+ itemsContainer: this.itemsContainer,
301
+ waitForScrollingToStop,
302
+ onScroll: ({ delayed } = {}) => {
303
+ this.onUpdateShownItemIndexes({
304
+ reason: delayed ? LAYOUT_REASON.STOPPED_SCROLLING : LAYOUT_REASON.SCROLL
305
+ })
306
+ },
307
+ initialScrollPosition,
308
+ onScrollPositionChange,
309
+ isImmediateLayoutScheduled: () => Boolean(this.layoutTimer),
310
+ hasNonRenderedItemsAtTheTop: () => this.getState().firstShownItemIndex > 0,
311
+ hasNonRenderedItemsAtTheBottom: () => this.getState().lastShownItemIndex < this.getItemsCount() - 1,
312
+ getLatestLayoutVisibleArea: () => this.latestLayoutVisibleArea,
313
+ getListTopOffset: this.getListTopOffsetInsideScrollableContainer,
314
+ getPrerenderMargin: () => this.getPrerenderMargin()
315
+ })
316
+
317
+ this.listHeightMeasurement = new ListHeightMeasurement({
318
+ itemsContainer: this.itemsContainer,
319
+ getListTopOffset: this.getListTopOffsetInsideScrollableContainer
320
+ })
321
+
322
+ if (this.engine.watchListTopOffset) {
323
+ this.listTopOffsetWatcher = this.engine.watchListTopOffset({
324
+ getListTopOffset: this.getListTopOffsetInsideScrollableContainer,
325
+ onListTopOffsetChange: ({ reason }) => this.onUpdateShownItemIndexes({
326
+ reason: LAYOUT_REASON.TOP_OFFSET_CHANGED
327
+ })
328
+ })
329
+ }
330
+
331
+ this.beforeResize = new BeforeResize({
332
+ getState: this.getState,
333
+ getVerticalSpacing: this.getVerticalSpacing,
334
+ getColumnsCount: this.getColumnsCount
335
+ })
336
+ }