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
@@ -1,907 +1,227 @@
1
- // For some weird reason, in Chrome, `setTimeout()` would lag up to a second (or more) behind.
2
- // Turns out, Chrome developers have deprecated `setTimeout()` API entirely without asking anyone.
3
- // Replacing `setTimeout()` with `requestAnimationFrame()` can work around that Chrome bug.
4
- // https://github.com/bvaughn/react-virtualized/issues/722
5
- import { setTimeout, clearTimeout } from 'request-animation-frame-timeout'
6
-
7
- import {
8
- supportsTbody,
9
- BROWSER_NOT_SUPPORTED_ERROR,
10
- addTbodyStyles,
11
- setTbodyPadding
12
- } from './DOM/tbody'
13
-
14
- import DOMRenderingEngine from './DOM/RenderingEngine'
15
- import WaitForStylesToLoad from './DOM/WaitForStylesToLoad'
16
-
17
- import Layout, { LAYOUT_REASON } from './Layout'
18
- import Resize from './Resize'
19
- import Scroll from './Scroll'
20
- import RestoreScroll from './RestoreScroll'
21
- import ItemHeights from './ItemHeights'
22
- import getItemsDiff from './getItemsDiff'
23
- import getVerticalSpacing from './getVerticalSpacing'
24
- // import getItemCoordinates from './getItemCoordinates'
25
-
26
- import log, { warn, isDebug, reportError } from './utility/debug'
27
- import shallowEqual from './utility/shallowEqual'
1
+ import VirtualScrollerConstructor from './VirtualScroller.constructor.js'
2
+ import { hasTbodyStyles, addTbodyStyles } from './DOM/tbody.js'
3
+ import { LAYOUT_REASON } from './Layout.js'
4
+ import log from './utility/debug.js'
28
5
 
29
6
  export default class VirtualScroller {
30
7
  /**
31
- * @param {function} getContainerElement — Returns the container DOM `Element`.
8
+ * @param {function} getItemsContainerElement — Returns the container DOM `Element`.
32
9
  * @param {any[]} items — The list of items.
33
10
  * @param {Object} [options] — See README.md.
34
11
  * @return {VirtualScroller}
35
12
  */
36
13
  constructor(
37
- getContainerElement,
14
+ getItemsContainerElement,
38
15
  items,
39
16
  options = {}
40
17
  ) {
41
- const {
42
- getState,
43
- setState,
44
- onStateChange,
45
- customState,
46
- // `preserveScrollPositionAtBottomOnMount` option name is deprecated,
47
- // use `preserveScrollPositionOfTheBottomOfTheListOnMount` option instead.
48
- preserveScrollPositionAtBottomOnMount,
49
- preserveScrollPositionOfTheBottomOfTheListOnMount,
50
- initialScrollPosition,
51
- onScrollPositionChange,
52
- measureItemsBatchSize,
53
- // `getScrollableContainer` option is deprecated.
54
- // Use `scrollableContainer` instead.
55
- getScrollableContainer,
56
- getColumnsCount,
57
- getItemId,
58
- tbody,
59
- _useTimeoutInRenderLoop,
60
- // bypassBatchSize
61
- } = options
62
-
63
- let {
64
- bypass,
65
- // margin,
66
- estimatedItemHeight,
67
- // getItemState,
68
- onItemInitialRender,
69
- // `onItemFirstRender(i)` is deprecated, use `onItemInitialRender(item)` instead.
70
- onItemFirstRender,
71
- scrollableContainer,
72
- state,
73
- renderingEngine
74
- } = options
75
-
76
- log('~ Initialize ~')
77
-
78
- // If `state` is passed then use `items` from `state`
79
- // instead of the `items` argument.
80
- if (state) {
81
- items = state.items
82
- }
83
-
84
- // `getScrollableContainer` option is deprecated.
85
- // Use `scrollableContainer` instead.
86
- if (!scrollableContainer && getScrollableContainer) {
87
- scrollableContainer = getScrollableContainer()
88
- }
89
-
90
- // Could support non-DOM rendering engines.
91
- // For example, React Native, `<canvas/>`, etc.
92
- if (!renderingEngine) {
93
- renderingEngine = DOMRenderingEngine
94
- }
95
-
96
- this.screen = renderingEngine.createScreen()
97
- this.scrollableContainer = renderingEngine.createScrollableContainer(scrollableContainer)
98
-
99
- // if (margin === undefined) {
100
- // // Renders items which are outside of the screen by this "margin".
101
- // // Is the screen height by default: seems to be the optimal value
102
- // // for "Page Up" / "Page Down" navigation and optimized mouse wheel scrolling.
103
- // margin = this.scrollableContainer ? this.scrollableContainer.getHeight() : 0
104
- // }
105
-
106
- // Work around `<tbody/>` not being able to have `padding`.
107
- // https://gitlab.com/catamphetamine/virtual-scroller/-/issues/1
108
- if (tbody) {
109
- if (renderingEngine.name !== 'DOM') {
110
- throw new Error('`tbody` option is only supported for DOM rendering engine')
111
- }
112
- log('~ <tbody/> detected ~')
113
- this.tbody = true
114
- if (!supportsTbody()) {
115
- log('~ <tbody/> not supported ~')
116
- reportError(BROWSER_NOT_SUPPORTED_ERROR)
117
- bypass = true
118
- }
119
- }
120
-
121
- if (bypass) {
122
- log('~ "bypass" mode ~')
123
- }
124
-
125
- // In `bypass` mode, `VirtualScroller` doesn't wait
126
- // for the user to scroll down to render all items:
127
- // instead, it renders all items right away, as if
128
- // the list is rendered without using `VirtualScroller`.
129
- // It was added just to measure how much is the
130
- // performance difference between using a `VirtualScroller`
131
- // and not using a `VirtualScroller`.
132
- // It turned out that unmounting large React component trees
133
- // is a very long process, so `VirtualScroller` does seem to
134
- // make sense when used in a React application.
135
- this.bypass = bypass
136
- // this.bypassBatchSize = bypassBatchSize || 10
137
-
138
- // Using `setTimeout()` in render loop is a workaround
139
- // for avoiding a React error message:
140
- // "Maximum update depth exceeded.
141
- // This can happen when a component repeatedly calls
142
- // `.setState()` inside `componentWillUpdate()` or `componentDidUpdate()`.
143
- // React limits the number of nested updates to prevent infinite loops."
144
- this._useTimeoutInRenderLoop = _useTimeoutInRenderLoop
145
-
146
- if (getItemId) {
147
- this.isItemEqual = (a, b) => getItemId(a) === getItemId(b)
148
- } else {
149
- this.isItemEqual = (a, b) => a === b
150
- }
151
-
152
- this.initialItems = items
153
- // this.margin = margin
154
-
155
- this.onStateChange = onStateChange
156
-
157
- this._getColumnsCount = getColumnsCount
158
-
159
- if (onItemInitialRender) {
160
- this.onItemInitialRender = onItemInitialRender
161
- }
162
- // `onItemFirstRender(i)` is deprecated, use `onItemInitialRender(item)` instead.
163
- else if (onItemFirstRender) {
164
- this.onItemInitialRender = (item) => {
165
- warn('`onItemFirstRender(i)` is deprecated, use `onItemInitialRender(item)` instead.')
166
- const { items } = this.getState()
167
- const i = items.indexOf(item)
168
- // The `item` could also be non-found due to the inconsistency bug:
169
- // The reason is that `i` can be non-consistent with the `items`
170
- // passed to `<VirtualScroller/>` in React due to `setState()` not being
171
- // instanteneous: when new `items` are passed to `<VirtualScroller/>`,
172
- // `VirtualScroller.setState({ items })` is called, and if `onItemFirstRender(i)`
173
- // is called after the aforementioned `setState()` is called but before it finishes,
174
- // `i` would point to an index in "previous" `items` while the application
175
- // would assume that `i` points to an index in the "new" `items`,
176
- // resulting in an incorrect item being assumed by the application
177
- // or even in an "array index out of bounds" error.
178
- if (i >= 0) {
179
- onItemFirstRender(i)
180
- }
181
- }
182
- }
183
-
184
- log('Items count', items.length)
185
- if (estimatedItemHeight) {
186
- log('Estimated item height', estimatedItemHeight)
187
- }
188
-
189
- if (setState) {
190
- this.getState = getState
191
- this.setState = (state) => {
192
- log('Set state', state)
193
- setState(state, {
194
- willUpdateState: this.willUpdateState,
195
- didUpdateState: this.didUpdateState
196
- })
197
- }
198
- } else {
199
- this.getState = () => this.state
200
- this.setState = (state) => {
201
- log('Set state', state)
202
- const prevState = this.getState()
203
- // Because this variant of `.setState()` is "synchronous" (immediate),
204
- // it can be written like `...prevState`, and no state updates would be lost.
205
- // But if it was "asynchronous" (not immediate), then `...prevState`
206
- // wouldn't work in all cases, because it could be stale in cases
207
- // when more than a single `setState()` call is made before
208
- // the state actually updates, making `prevState` stale.
209
- const newState = {
210
- ...prevState,
211
- ...state
212
- }
213
- this.willUpdateState(newState, prevState)
214
- this.state = newState
215
- this.didUpdateState(prevState)
216
- }
217
- }
218
-
219
- if (state) {
220
- log('Initial state (passed)', state)
221
- }
222
-
223
- // Sometimes, when `new VirtualScroller()` instance is created,
224
- // `getContainerElement()` might not be ready to return the "container" DOM Element yet
225
- // (for example, because it's not rendered yet). That's the reason why it's a getter function.
226
- // For example, in React `<VirtualScroller/>` component, a `VirtualScroller`
227
- // instance is created in the React component's `constructor()`, and at that time
228
- // the container Element is not yet available. The container Element is available
229
- // in `componentDidMount()`, but `componentDidMount()` is not executed on server,
230
- // which would mean that React `<VirtualScroller/>` wouldn't render at all
231
- // on server side, while with the `getContainerElement()` approach, on server side,
232
- // it still "renders" a list with a predefined amount of items in it by default.
233
- // (`initiallyRenderedItemsCount`, or `1`).
234
- this.getContainerElement = getContainerElement
235
- // Remove any accidental text nodes from container (like whitespace).
236
- // Also guards against cases when someone accidentally tries
237
- // using `VirtualScroller` on a non-empty element.
238
- if (getContainerElement()) {
239
- this.screen.clearElement(getContainerElement())
240
- }
241
-
242
- this.itemHeights = new ItemHeights(
243
- this.screen,
244
- this.getContainerElement,
245
- (i) => this.getState().itemHeights[i],
246
- (i, height) => this.getState().itemHeights[i] = height
18
+ VirtualScrollerConstructor.call(
19
+ this,
20
+ getItemsContainerElement,
21
+ items,
22
+ options
247
23
  )
248
-
249
- // Initialize `ItemHeights` from the initially passed `state`.
250
- if (state) {
251
- this.itemHeights.initialize(state.itemHeights)
252
- }
253
-
254
- this.layout = new Layout({
255
- bypass,
256
- estimatedItemHeight,
257
- measureItemsBatchSize: measureItemsBatchSize === undefined ? 50 : measureItemsBatchSize,
258
- getVerticalSpacing: () => this.getVerticalSpacing(),
259
- getColumnsCount: () => this.getColumnsCount(),
260
- getItemHeight: (i) => this.getState().itemHeights[i],
261
- getAverageItemHeight: () => this.itemHeights.getAverage()
262
- })
263
-
264
- this.resize = new Resize({
265
- bypass,
266
- scrollableContainer: this.scrollableContainer,
267
- getContainerElement: this.getContainerElement,
268
- updateLayout: ({ reason }) => this.onUpdateShownItemIndexes({ reason }),
269
- resetStateAndLayout: () => {
270
- // Reset item heights, because if scrollable container's width (or height)
271
- // has changed, then the list width (or height) most likely also has changed,
272
- // and also some CSS `@media()` rules might have been added or removed.
273
- // So re-render the list entirely.
274
- log('~ Scrollable container size changed, re-measure item heights. ~')
275
- this.redoLayoutReason = LAYOUT_REASON.RESIZE
276
- // `this.layoutResetPending` flag will be cleared in `didUpdateState()`.
277
- this.layoutResetPending = true
278
- log('Reset state')
279
- // Calling `this.setState(state)` will trigger `didUpdateState()`.
280
- // `didUpdateState()` will detect `this.redoLayoutReason`.
281
- this.setState(this.getInitialLayoutState(this.newItemsPending || this.getState().items))
282
- }
283
- })
284
-
285
- if (preserveScrollPositionAtBottomOnMount) {
286
- warn('`preserveScrollPositionAtBottomOnMount` option/property has been renamed to `preserveScrollPositionOfTheBottomOfTheListOnMount`')
287
- }
288
-
289
- this.preserveScrollPositionOfTheBottomOfTheListOnMount = preserveScrollPositionOfTheBottomOfTheListOnMount || preserveScrollPositionAtBottomOnMount
290
-
291
- this.scroll = new Scroll({
292
- bypass: this.bypass,
293
- scrollableContainer: this.scrollableContainer,
294
- updateLayout: ({ reason }) => this.onUpdateShownItemIndexes({ reason }),
295
- initialScrollPosition,
296
- onScrollPositionChange,
297
- isImmediateLayoutScheduled: () => this.layoutTimer,
298
- hasNonRenderedItemsAtTheTop: () => this.getState().firstShownItemIndex > 0,
299
- hasNonRenderedItemsAtTheBottom: () => this.getState().lastShownItemIndex < this.getItemsCount() - 1,
300
- getLatestLayoutVisibleAreaIncludingMargins: () => this.latestLayoutVisibleAreaIncludingMargins,
301
- preserveScrollPositionOfTheBottomOfTheListOnMount: this.preserveScrollPositionOfTheBottomOfTheListOnMount
302
- })
303
-
304
- this.restoreScroll = new RestoreScroll({
305
- screen: this.screen,
306
- getContainerElement: this.getContainerElement
307
- })
308
-
309
- this.waitForStylesToLoad = new WaitForStylesToLoad({
310
- updateLayout: ({ reason }) => this.onUpdateShownItemIndexes({ reason }),
311
- getListTopOffsetInsideScrollableContainer: this.getListTopOffsetInsideScrollableContainer
312
- })
313
-
314
- this.setState(state || this.getInitialState(customState))
315
24
  }
316
25
 
317
26
  /**
318
- * Returns the initial state of the `VirtualScroller`.
319
- * @param {object} [customState] — Any additional "custom" state may be stored in `VirtualScroller`'s state. For example, React implementation stores item "refs" as "custom" state.
320
- * @return {object}
27
+ * Should be invoked after a "container" DOM Element is mounted (inserted into the DOM tree).
321
28
  */
322
- getInitialState(customState) {
323
- const items = this.initialItems
324
- const state = {
325
- ...customState,
326
- ...this.getInitialLayoutState(items),
327
- items,
328
- itemStates: new Array(items.length)
29
+ start() {
30
+ if (this._isActive) {
31
+ throw new Error('[virtual-scroller] `VirtualScroller` has already been started')
329
32
  }
330
- log('Initial state (autogenerated)', state)
331
- log('First shown item index', state.firstShownItemIndex)
332
- log('Last shown item index', state.lastShownItemIndex)
333
- return state
334
- }
335
33
 
336
- getInitialLayoutValues({ itemsCount, bypass }) {
337
- return this.layout.getInitialLayoutValues({
338
- bypass,
339
- itemsCount,
340
- visibleAreaHeightIncludingMargins: this.scrollableContainer && (2 * this.getMargin() + this.scrollableContainer.getHeight())
341
- })
342
- }
34
+ // If has been stopped previously.
35
+ const isRestart = this._isActive === false
343
36
 
344
- getInitialLayoutState(items) {
345
- const itemsCount = items.length
346
- const {
347
- firstShownItemIndex,
348
- lastShownItemIndex,
349
- beforeItemsHeight,
350
- afterItemsHeight
351
- } = this.getInitialLayoutValues({
352
- itemsCount,
353
- bypass: this.preserveScrollPositionOfTheBottomOfTheListOnMount
354
- })
355
- const itemHeights = new Array(itemsCount)
356
- // Optionally preload items to be rendered.
357
- this.onBeforeShowItems(
358
- items,
359
- itemHeights,
360
- firstShownItemIndex,
361
- lastShownItemIndex
362
- )
363
- // This "initial" state object must include all possible state properties
364
- // because `this.setState()` gets called with this state on window resize,
365
- // when `VirtualScroller` gets reset.
366
- // Item states aren't included here because the state of all items should be
367
- // preserved on window resize.
368
- return {
369
- itemHeights,
370
- columnsCount: this._getColumnsCount ? this._getColumnsCount(this.scrollableContainer) : undefined,
371
- verticalSpacing: undefined,
372
- firstShownItemIndex,
373
- lastShownItemIndex,
374
- beforeItemsHeight,
375
- afterItemsHeight
37
+ if (!isRestart) {
38
+ // If no custom one has been configured, uses the default one.
39
+ // Also sets the initial state.
40
+ if (!this._usesCustomStateStorage) {
41
+ this.useDefaultStateStorage()
42
+ }
43
+ // If `render()` function parameter was passed,
44
+ // perform an initial render.
45
+ if (this._render) {
46
+ this._render(this.getState())
47
+ }
376
48
  }
377
- }
378
-
379
- getVerticalSpacing() {
380
- return this.getState() && this.getState().verticalSpacing || 0
381
- }
382
-
383
- getColumnsCount() {
384
- return this.getState() && this.getState().columnsCount || 1
385
- }
386
49
 
387
- getItemsCount() {
388
- return this.getState().items.length
389
- }
50
+ log('~ Start ~')
390
51
 
391
- getMargin() {
392
- // `VirtualScroller` also items that are outside of the screen
393
- // by the amount of this "render ahead margin" (both on top and bottom).
394
- // The default "render ahead margin" is equal to the screen height:
395
- // this seems to be the optimal value for "Page Up" / "Page Down" navigation
396
- // and optimized mouse wheel scrolling (a user is unlikely to continuously
397
- // scroll past the height of a screen, and when they stop scrolling,
398
- // the list is re-rendered).
399
- const renderAheadMarginRatio = 1 // in scrollable container heights.
400
- return this.scrollableContainer.getHeight() * renderAheadMarginRatio
401
- }
52
+ // `this._isActive = true` should be placed somewhere at the start of this function.
53
+ this._isActive = true
402
54
 
403
- /**
404
- * Calls `onItemFirstRender()` for items that haven't been
405
- * "seen" previously.
406
- * @param {any[]} items
407
- * @param {number[]} itemHeights
408
- * @param {number} firstShownItemIndex
409
- * @param {number} lastShownItemIndex
410
- */
411
- onBeforeShowItems(
412
- items,
413
- itemHeights,
414
- firstShownItemIndex,
415
- lastShownItemIndex
416
- ) {
417
- if (this.onItemInitialRender) {
418
- let i = firstShownItemIndex
419
- while (i <= lastShownItemIndex) {
420
- if (itemHeights[i] === undefined) {
421
- this.onItemInitialRender(items[i])
422
- }
423
- i++
424
- }
425
- }
426
- }
55
+ // Reset `ListHeightMeasurement` just in case it has some "leftover" state.
56
+ this.listHeightMeasurement.reset()
427
57
 
428
- onMount() {
429
- warn('`.onMount()` instance method name is deprecated, use `.listen()` instance method name instead.')
430
- this.listen()
431
- }
58
+ // Reset `_isResizing` flag just in case it has some "leftover" value.
59
+ this._isResizing = undefined
432
60
 
433
- render() {
434
- warn('`.render()` instance method name is deprecated, use `.listen()` instance method name instead.')
435
- this.listen()
436
- }
61
+ // Reset `_isSettingNewItems` flag just in case it has some "leftover" value.
62
+ this._isSettingNewItems = undefined
437
63
 
438
- /**
439
- * Should be invoked after a "container" DOM Element is mounted (inserted into the DOM tree).
440
- */
441
- listen() {
442
- if (this.isRendered === false) {
443
- throw new Error('[virtual-scroller] Can\'t restart a `VirtualScroller` after it has been stopped')
444
- }
445
- log('~ Rendered (initial) ~')
446
- // `this.isRendered = true` should be the first statement in this function,
447
- // otherwise `DOMVirtualScroller` would enter an infinite re-render loop.
448
- this.isRendered = true
449
- this.onRenderedNewLayout()
450
- this.resize.listen()
451
- this.scroll.listen()
452
64
  // Work around `<tbody/>` not being able to have `padding`.
453
65
  // https://gitlab.com/catamphetamine/virtual-scroller/-/issues/1
454
66
  if (this.tbody) {
455
- addTbodyStyles(this.getContainerElement())
456
- }
457
- if (this.preserveScrollPositionOfTheBottomOfTheListOnMount) {
458
- // In this case, all items are shown, so there's no need to call
459
- // `this.onUpdateShownItemIndexes()` after the initial render.
67
+ if (!hasTbodyStyles(this.getItemsContainerElement())) {
68
+ addTbodyStyles(this.getItemsContainerElement())
69
+ }
70
+ }
71
+
72
+ // If there was a pending state update that didn't get applied
73
+ // because of stopping the `VirtualScroller`, apply that state update now.
74
+ //
75
+ // The pending state update won't get applied if the scrollable container width
76
+ // has changed but that's ok because that state update currently could only contain:
77
+ // * `scrollableContainerWidth`
78
+ // * `verticalSpacing`
79
+ // * `beforeResize`
80
+ // All of those get rewritten in `onResize()` anyway.
81
+ //
82
+ let stateUpdate = this._stoppedStateUpdate
83
+ this._stoppedStateUpdate = undefined
84
+
85
+ // Reset `this.verticalSpacing` so that it re-measures it in cases when
86
+ // the `VirtualScroller` was previously stopped and is now being restarted.
87
+ // The rationale is that a previously captured inter-item vertical spacing
88
+ // can't be "trusted" in a sense that the user might have resized the window
89
+ // after the previous `state` has been snapshotted.
90
+ // If the user has resized the window, then changing window width might have
91
+ // activated different CSS `@media()` "queries" resulting in a potentially different
92
+ // vertical spacing after the restart.
93
+ // If it's not a restart then `this.verticalSpacing` is `undefined` anyway.
94
+ this.verticalSpacing = undefined
95
+
96
+ const verticalSpacingStateUpdate = this.measureItemHeightsAndSpacing()
97
+ if (verticalSpacingStateUpdate) {
98
+ stateUpdate = {
99
+ ...stateUpdate,
100
+ ...verticalSpacingStateUpdate
101
+ }
102
+ }
103
+
104
+ this.resize.start()
105
+ this.scroll.start()
106
+
107
+ // If `scrollableContainerWidth` hasn't been measured yet,
108
+ // measure it and write it to state.
109
+ if (this.getState().scrollableContainerWidth === undefined) {
110
+ const scrollableContainerWidth = this.scrollableContainer.getWidth()
111
+ stateUpdate = {
112
+ ...stateUpdate,
113
+ scrollableContainerWidth
114
+ }
460
115
  } else {
461
- this.onUpdateShownItemIndexes({ reason: LAYOUT_REASON.MOUNT })
116
+ // Reset layout:
117
+ // * If the scrollable container width has changed while stopped.
118
+ // * If the restored state was calculated for another scrollable container width.
119
+ const newWidth = this.scrollableContainer.getWidth()
120
+ const prevWidth = this.getState().scrollableContainerWidth
121
+ if (newWidth !== prevWidth) {
122
+ log('~ Scrollable container width changed from', prevWidth, 'to', newWidth, '~')
123
+ // `stateUpdate` doesn't get passed to `this.onResize()`, and, therefore,
124
+ // won't be applied. But that's ok because currently it could only contain:
125
+ // * `scrollableContainerWidth`
126
+ // * `verticalSpacing`
127
+ // * `beforeResize`
128
+ // All of those get rewritten in `onResize()` anyway.
129
+ return this.onResize()
130
+ }
462
131
  }
463
- }
464
132
 
465
- onRenderedNewLayout() {
466
- // Update item vertical spacing.
467
- this.measureVerticalSpacing()
468
- // Measure "newly shown" item heights.
469
- // Also re-validate already measured items' heights.
470
- this.itemHeights.measureItemHeights(
471
- this.getState().firstShownItemIndex,
472
- this.getState().lastShownItemIndex
473
- )
474
- // Update `<tbody/>` `padding`.
475
- // (`<tbody/>` is different in a way that it can't have `margin`, only `padding`).
476
- // https://gitlab.com/catamphetamine/virtual-scroller/-/issues/1
477
- if (this.tbody) {
478
- setTbodyPadding(
479
- this.getContainerElement(),
480
- this.getState().beforeItemsHeight,
481
- this.getState().afterItemsHeight
482
- )
133
+ // If the `VirtualScroller` uses custom (external) state storage, then
134
+ // check if the columns count has changed between calling `.getInitialState()`
135
+ // and `.start()`. If it has, perform a re-layout "from scratch".
136
+ if (this._usesCustomStateStorage) {
137
+ const columnsCount = this.getActualColumnsCount()
138
+ const columnsCountFromState = this.getState().columnsCount || 1
139
+ if (columnsCount !== columnsCountFromState) {
140
+ return this.onResize()
141
+ }
483
142
  }
484
- }
485
143
 
486
- getVisibleAreaBoundsIncludingMargins() {
487
- const visibleArea = this.scroll.getVisibleAreaBounds()
488
- visibleArea.top -= this.getMargin()
489
- visibleArea.bottom += this.getMargin()
490
- return visibleArea
144
+ // Re-calculate layout and re-render the list.
145
+ // Do that even if when an initial `state` parameter, containing layout values,
146
+ // has been passed. The reason is that the `state` parameter can't be "trusted"
147
+ // in a way that it could have been snapshotted for another window width and
148
+ // the user might have resized their window since then.
149
+ this.onUpdateShownItemIndexes({ reason: LAYOUT_REASON.STARTED, stateUpdate })
491
150
  }
492
151
 
493
- /**
494
- * Returns the list's top offset relative to the scrollable container's top edge.
495
- * @return {number}
496
- */
497
- getListTopOffsetInsideScrollableContainer = () => {
498
- const listTopOffset = this.scrollableContainer.getTopOffset(this.getContainerElement())
499
- this.waitForStylesToLoad.onGotListTopOffset(listTopOffset)
500
- return listTopOffset
501
- }
152
+ // Could be passed as a "callback" parameter, so bind it to `this`.
153
+ stop = () => {
154
+ if (!this._isActive) {
155
+ throw new Error('[virtual-scroller] Can\'t stop a `VirtualScroller` that hasn\'t been started')
156
+ }
502
157
 
503
- onUnmount() {
504
- warn('`.onUnmount()` instance method name is deprecated, use `.stop()` instance method name instead.')
505
- this.stop()
506
- }
158
+ this._isActive = false
507
159
 
508
- destroy() {
509
- warn('`.destroy()` instance method name is deprecated, use `.stop()` instance method name instead.')
510
- this.stop()
511
- }
160
+ log('~ Stop ~')
512
161
 
513
- stop() {
514
- this.isRendered = false
515
162
  this.resize.stop()
516
163
  this.scroll.stop()
517
- this.waitForStylesToLoad.stop()
518
- if (this.layoutTimer) {
519
- clearTimeout(this.layoutTimer)
520
- this.layoutTimer = undefined
521
- }
522
- }
523
164
 
524
- /**
525
- * Should be called right before `state` is updated.
526
- * @param {object} prevState
527
- * @param {object} newState
528
- */
529
- willUpdateState = (newState, prevState) => {
530
- // Ignore setting initial state.
531
- if (!prevState) {
532
- return
165
+ // Stop `ListTopOffsetWatcher` if it has been started.
166
+ // There seems to be no need to restart `ListTopOffsetWatcher`.
167
+ // It's mainly a hacky workaround for development mode anyway.
168
+ if (this.listTopOffsetWatcher && this.listTopOffsetWatcher.isStarted()) {
169
+ this.listTopOffsetWatcher.stop()
533
170
  }
534
- // This function isn't currently used.
535
- // Was previously used to capture scroll position in order to
536
- // restore it later after the new state is rendered.
537
- }
538
171
 
539
- /**
540
- * Should be called right after `state` is updated.
541
- * @param {object} prevState
542
- */
543
- didUpdateState = (prevState) => {
544
- const newState = this.getState()
545
- if (this.onStateChange) {
546
- if (!shallowEqual(newState, prevState)) {
547
- this.onStateChange(newState, prevState)
548
- }
549
- }
550
- // Ignore setting initial state.
551
- if (!prevState) {
552
- return
553
- }
554
- if (!this.isRendered) {
555
- return
556
- }
557
- log('~ Rendered ~')
558
- this.newItemsPending = undefined
559
- this.layoutResetPending = undefined
560
- let redoLayoutReason = this.redoLayoutReason
561
- this.redoLayoutReason = undefined
562
- const { items: previousItems } = prevState
563
- const { items: newItems } = newState
564
- if (newItems !== previousItems) {
565
- let layoutNeedsReCalculating = true
566
- const itemsDiff = this.getItemsDiff(previousItems, newItems)
567
- // If it's an "incremental" update.
568
- if (itemsDiff) {
569
- const {
570
- prependedItemsCount,
571
- appendedItemsCount
572
- } = itemsDiff
573
- if (prependedItemsCount > 0) {
574
- // The call to `.onPrepend()` must precede
575
- // the call to `.measureItemHeights()`
576
- // which is called in `.onRendered()`.
577
- this.itemHeights.onPrepend(prependedItemsCount)
578
- if (this.restoreScroll.shouldRestoreScrollAfterRender()) {
579
- layoutNeedsReCalculating = false
580
- log('~ Restore Scroll Position ~')
581
- const scrollByY = this.restoreScroll.getScrollDifference()
582
- if (scrollByY) {
583
- log('Scroll down by', scrollByY)
584
- this.scroll.scrollByY(scrollByY)
585
- } else {
586
- log('Scroll position hasn\'t changed')
587
- }
588
- }
589
- }
590
- } else {
591
- this.itemHeights.reset()
592
- this.itemHeights.initialize(this.getState().itemHeights)
593
- }
594
- if (layoutNeedsReCalculating) {
595
- redoLayoutReason = LAYOUT_REASON.ITEMS_CHANGED
596
- }
597
- }
598
- // Call `.onRendered()` if shown items configuration changed.
599
- if (newState.firstShownItemIndex !== prevState.firstShownItemIndex ||
600
- newState.lastShownItemIndex !== prevState.lastShownItemIndex ||
601
- newState.items !== prevState.items) {
602
- this.onRenderedNewLayout()
603
- }
604
- if (redoLayoutReason) {
605
- return this.redoLayoutRightAfterRender({
606
- reason: redoLayoutReason
607
- })
608
- }
172
+ // Cancel any scheduled layout.
173
+ this.cancelLayoutTimer({})
609
174
  }
610
175
 
611
- redoLayoutRightAfterRender({ reason }) {
612
- // In React, `setTimeout()` is used to prevent a React error:
613
- // "Maximum update depth exceeded.
614
- // This can happen when a component repeatedly calls
615
- // `.setState()` inside `componentWillUpdate()` or `componentDidUpdate()`.
616
- // React limits the number of nested updates to prevent infinite loops."
617
- if (this._useTimeoutInRenderLoop) {
618
- // Cancel a previously scheduled re-layout.
619
- if (this.layoutTimer) {
620
- clearTimeout(this.layoutTimer)
621
- }
622
- // Schedule a new re-layout.
623
- this.layoutTimer = setTimeout(() => {
624
- this.layoutTimer = undefined
625
- this.onUpdateShownItemIndexes({ reason })
626
- }, 0)
627
- } else {
628
- this.onUpdateShownItemIndexes({ reason })
629
- }
630
- }
631
-
632
- measureVerticalSpacing() {
633
- if (this.getState().verticalSpacing === undefined) {
634
- log('~ Measure item vertical spacing ~')
635
- const verticalSpacing = getVerticalSpacing({
636
- container: this.getContainerElement(),
637
- screen: this.screen
638
- })
639
- if (verticalSpacing === undefined) {
640
- log('Not enough items rendered to measure vertical spacing')
641
- } else {
642
- log('Item vertical spacing', verticalSpacing)
643
- this.setState({ verticalSpacing })
644
- }
176
+ hasToBeStarted() {
177
+ if (!this._isActive) {
178
+ throw new Error('[virtual-scroller] `VirtualScroller` hasn\'t been started')
645
179
  }
646
180
  }
647
181
 
648
- remeasureItemHeight(i) {
649
- const { firstShownItemIndex } = this.getState()
650
- return this.itemHeights.remeasureItemHeight(i, firstShownItemIndex)
182
+ // Bind it to `this` because this function could hypothetically be passed
183
+ // as a "callback" parameter.
184
+ updateLayout = () => {
185
+ this.hasToBeStarted()
186
+ this.onUpdateShownItemIndexes({ reason: LAYOUT_REASON.MANUAL })
651
187
  }
652
188
 
653
- onItemStateChange(i, itemState) {
654
- if (isDebug()) {
655
- log('~ Item state changed ~')
656
- log('Item', i)
657
- log('Previous state' + '\n' + JSON.stringify(this.getState().itemStates[i], null, 2))
658
- log('New state' + '\n' + JSON.stringify(itemState, null, 2))
659
- }
660
- this.getState().itemStates[i] = itemState
661
- }
662
-
663
- onItemHeightChange(i) {
664
- log('~ Re-measure item height ~')
665
- log('Item', i)
666
- const { itemHeights } = this.getState()
667
- const previousHeight = itemHeights[i]
668
- if (previousHeight === undefined) {
669
- return reportError(`"onItemHeightChange()" has been called for item ${i}, but that item hasn't been rendered before.`)
670
- }
671
- const newHeight = this.remeasureItemHeight(i)
672
- // Check if the item is still rendered.
673
- if (newHeight === undefined) {
674
- // There could be valid cases when an item is no longer rendered
675
- // by the time `.onItemHeightChange(i)` gets called.
676
- // For example, suppose there's a list of several items on a page,
677
- // and those items are in "minimized" state (having height 100px).
678
- // Then, a user clicks an "Expand all items" button, and all items
679
- // in the list are expanded (expanded item height is gonna be 700px).
680
- // `VirtualScroller` demands that `.onItemHeightChange(i)` is called
681
- // in such cases, and the developer has properly added the code to do that.
682
- // So, if there were 10 "minimized" items visible on a page, then there
683
- // will be 10 individual `.onItemHeightChange(i)` calls. No issues so far.
684
- // But, as the first `.onItemHeightChange(i)` call executes, it immediately
685
- // ("synchronously") triggers a re-layout, and that re-layout finds out
686
- // that now, because the first item is big, it occupies most of the screen
687
- // space, and only the first 3 items are visible on screen instead of 10,
688
- // and so it leaves the first 3 items mounted and unmounts the rest 7.
689
- // Then, after `VirtualScroller` has rerendered, the code returns to
690
- // where it was executing, and calls `.onItemHeightChange(i)` for the
691
- // second item. It also triggers an immediate re-layout that finds out
692
- // that only the first 2 items are visible on screen, and it unmounts
693
- // the third one too. After that, it calls `.onItemHeightChange(i)`
694
- // for the third item, but that item is no longer rendered, so its height
695
- // can't be measured, and the same's for all the rest of the original 10 items.
696
- // So, even though the developer has written their code properly, there're
697
- // still situations when the item could be no longer rendered by the time
698
- // `.onItemHeightChange(i)` gets called.
699
- return warn('The item is no longer rendered. This is not necessarily a bug, and could happen, for example, when there\'re several `onItemHeightChange(i)` calls issued at the same time.')
700
- }
701
- log('Previous height', previousHeight)
702
- log('New height', newHeight)
703
- if (previousHeight !== newHeight) {
704
- log('~ Item height has changed ~')
705
- // log('Item', i)
706
- this.onUpdateShownItemIndexes({ reason: LAYOUT_REASON.ITEM_HEIGHT_CHANGED })
707
- }
189
+ // Bind the function to `this` so that it could be passed as a callback
190
+ // in a random application's code.
191
+ onRender = () => {
192
+ this._onRender(this.getState(), this.previousState)
708
193
  }
709
194
 
710
195
  /**
711
- * Validates the heights of items to be hidden on next render.
712
- * For example, a user could click a "Show more" button,
713
- * or an "Expand YouTube video" button, which would result
714
- * in the actual height of the list item being different
715
- * from what has been initially measured in `this.itemHeights[i]`,
716
- * if the developer didn't call `.onItemStateChange()` and `.onItemHeightChange(i)`.
196
+ * Returns the items's top offset relative to the scrollable container's top edge.
197
+ * @param {number} i Item index
198
+ * @return {[number]} Returns the item's scroll Y position. Returns `undefined` if any of the previous items haven't been rendered yet.
717
199
  */
718
- validateWillBeHiddenItemHeightsAreAccurate(firstShownItemIndex, lastShownItemIndex) {
719
- let isValid = true
720
- let i = this.getState().firstShownItemIndex
721
- while (i <= this.getState().lastShownItemIndex) {
722
- if (i >= firstShownItemIndex && i <= lastShownItemIndex) {
723
- // The item's still visible.
724
- } else {
725
- // The item will be hidden. Re-measure its height.
726
- // The rationale is that there could be a situation when an item's
727
- // height has changed, and the developer has properly added an
728
- // `.onItemHeightChange(i)` call to notify `VirtualScroller`
729
- // about that change, but at the same time that wouldn't work.
730
- // For example, suppose there's a list of several items on a page,
731
- // and those items are in "minimized" state (having height 100px).
732
- // Then, a user clicks an "Expand all items" button, and all items
733
- // in the list are expanded (expanded item height is gonna be 700px).
734
- // `VirtualScroller` demands that `.onItemHeightChange(i)` is called
735
- // in such cases, and the developer has properly added the code to do that.
736
- // So, if there were 10 "minimized" items visible on a page, then there
737
- // will be 10 individual `.onItemHeightChange(i)` calls. No issues so far.
738
- // But, as the first `.onItemHeightChange(i)` call executes, it immediately
739
- // ("synchronously") triggers a re-layout, and that re-layout finds out
740
- // that now, because the first item is big, it occupies most of the screen
741
- // space, and only the first 3 items are visible on screen instead of 10,
742
- // and so it leaves the first 3 items mounted and unmounts the rest 7.
743
- // Then, after `VirtualScroller` has rerendered, the code returns to
744
- // where it was executing, and calls `.onItemHeightChange(i)` for the
745
- // second item. It also triggers an immediate re-layout that finds out
746
- // that only the first 2 items are visible on screen, and it unmounts
747
- // the third one too. After that, it calls `.onItemHeightChange(i)`
748
- // for the third item, but that item is no longer rendered, so its height
749
- // can't be measured, and the same's for all the rest of the original 10 items.
750
- // So, even though the developer has written their code properly, the
751
- // `VirtualScroller` still ends up having incorrect `itemHeights[]`:
752
- // `[700px, 700px, 100px, 100px, 100px, 100px, 100px, 100px, 100px, 100px]`
753
- // while it should have been `700px` for all of them.
754
- // To work around such issues, every item's height is re-measured before it
755
- // gets hidden.
756
- const previouslyMeasuredItemHeight = this.getState().itemHeights[i]
757
- const actualItemHeight = this.remeasureItemHeight(i)
758
- if (actualItemHeight !== previouslyMeasuredItemHeight) {
759
- isValid = false
760
- warn('Item', i, 'will be unmounted at next render because it\'s no longer visible. Its height has changed from', previouslyMeasuredItemHeight, 'to', actualItemHeight, 'since it was last measured. This is not necessarily a bug, and could happen, for example, when there\'re several `onItemHeightChange(i)` calls issued at the same time, and the first one triggers a re-layout before the rest of them have had a chance to be executed.')
761
- }
762
- }
763
- i++
200
+ getItemScrollPosition(i) {
201
+ const itemTopOffsetInList = this.layout.getItemTopOffset(i)
202
+ if (itemTopOffsetInList === undefined) {
203
+ return
764
204
  }
765
- return isValid
205
+ return this.getListTopOffsetInsideScrollableContainer() + itemTopOffsetInList
766
206
  }
767
207
 
768
208
  /**
769
- * Updates the "from" and "to" shown item indexes.
770
- * If the list is visible and some of the items being shown are new
771
- * and are required to be measured first, then
772
- * `redoLayoutAfterMeasuringItemHeights` is `true`.
773
- * If the list is visible and all items being shown have been encountered
774
- * (and measured) before, then `redoLayoutAfterMeasuringItemHeights` is `false`.
209
+ * Forces a re-measure of an item's height.
210
+ * @param {number} i Item index
775
211
  */
776
- updateShownItemIndexes = () => {
777
- log('~ Layout results ' + (this.bypass ? '(bypass) ' : '') + '~')
778
- const visibleAreaIncludingMargins = this.getVisibleAreaBoundsIncludingMargins()
779
- this.latestLayoutVisibleAreaIncludingMargins = visibleAreaIncludingMargins
780
- const listTopOffsetInsideScrollableContainer = this.getListTopOffsetInsideScrollableContainer()
781
- // Get shown item indexes.
782
- let {
783
- firstShownItemIndex,
784
- lastShownItemIndex,
785
- redoLayoutAfterMeasuringItemHeights
786
- } = this.layout.getShownItemIndexes({
787
- listHeight: this.screen.getElementHeight(this.getContainerElement()),
788
- itemsCount: this.getItemsCount(),
789
- visibleAreaIncludingMargins,
790
- listTopOffsetInsideScrollableContainer
791
- })
792
- // If scroll position is scheduled to be restored after render,
793
- // then the "anchor" item must be rendered, and all of the prepended
794
- // items before it, all in a single pass. This way, all of the
795
- // prepended items' heights could be measured right after the render
796
- // has finished, and the scroll position can then be immediately restored.
797
- if (this.restoreScroll.shouldRestoreScrollAfterRender()) {
798
- if (lastShownItemIndex < this.restoreScroll.getAnchorItemIndex()) {
799
- lastShownItemIndex = this.restoreScroll.getAnchorItemIndex()
800
- }
801
- // `firstShownItemIndex` is always `0` when prepending items.
802
- // And `lastShownItemIndex` always covers all prepended items in this case.
803
- // None of the prepended items have been rendered before,
804
- // so their heights are unknown. The code at the start of this function
805
- // did therefore set `redoLayoutAfterMeasuringItemHeights` to `true`
806
- // in order to render just the first prepended item in order to
807
- // measure it, and only then make a decision on how many other
808
- // prepended items to render. But since we've instructed the code
809
- // to show all of the prepended items at once, there's no need to
810
- // "redo layout after render". Additionally, if layout was re-done
811
- // after render, then there would be a short interval of visual
812
- // "jitter" due to the scroll position not being restored because it'd
813
- // wait for the second layout to finish instead of being restored
814
- // right after the first one.
815
- redoLayoutAfterMeasuringItemHeights = false
816
- }
817
- // Validate the heights of items to be hidden on next render.
818
- // For example, a user could click a "Show more" button,
819
- // or an "Expand YouTube video" button, which would result
820
- // in the actual height of the list item being different
821
- // from what has been initially measured in `this.itemHeights[i]`,
822
- // if the developer didn't call `.onItemStateChange()` and `.onItemHeightChange(i)`.
823
- if (!this.validateWillBeHiddenItemHeightsAreAccurate(firstShownItemIndex, lastShownItemIndex)) {
824
- // Redo layout, now with the correct item heights.
825
- log('~ Some of the will-be-hidden item heights have changed since they\'ve last been measured. Redo layout. ~')
826
- return this.updateShownItemIndexes();
827
- }
828
- // Measure "before" items height.
829
- const beforeItemsHeight = this.layout.getBeforeItemsHeight(
830
- firstShownItemIndex,
831
- lastShownItemIndex
832
- )
833
- // Measure "after" items height.
834
- const afterItemsHeight = this.layout.getAfterItemsHeight(
835
- firstShownItemIndex,
836
- lastShownItemIndex,
837
- this.getItemsCount()
838
- )
839
- // Debugging.
840
- if (this._getColumnsCount) {
841
- log('Columns count', this.getColumnsCount())
842
- }
843
- log('First shown item index', firstShownItemIndex)
844
- log('Last shown item index', lastShownItemIndex)
845
- log('Before items height', beforeItemsHeight)
846
- log('After items height (actual or estimated)', afterItemsHeight)
847
- log('Average item height (calculated on previous render)', this.itemHeights.getAverage())
848
- if (isDebug()) {
849
- log('Item heights', this.getState().itemHeights.slice())
850
- log('Item states', this.getState().itemStates.slice())
851
- }
852
- if (redoLayoutAfterMeasuringItemHeights) {
853
- // `this.redoLayoutReason` will be detected in `didUpdateState()`.
854
- // `didUpdateState()` is triggered by `this.setState()` below.
855
- this.redoLayoutReason = LAYOUT_REASON.ITEM_HEIGHT_NOT_MEASURED
856
- }
857
- // Optionally preload items to be rendered.
858
- this.onBeforeShowItems(
859
- this.getState().items,
860
- this.getState().itemHeights,
861
- firstShownItemIndex,
862
- lastShownItemIndex
863
- )
864
- // Render.
865
- this.setState({
866
- firstShownItemIndex,
867
- lastShownItemIndex,
868
- beforeItemsHeight,
869
- afterItemsHeight,
870
- // // Average item height is stored in state to differentiate between
871
- // // the initial state and "anything has been measured already" state.
872
- // averageItemHeight: this.itemHeights.getAverage()
873
- })
874
- }
875
-
876
- onUpdateShownItemIndexes = ({ reason }) => {
877
- // If there're no items then there's no need to re-layout anything.
878
- if (this.getItemsCount() === 0) {
879
- return
880
- }
881
- // Cancel a "re-layout when user stops scrolling" timer.
882
- this.scroll.onLayout()
883
- // Cancel a re-layout that is scheduled to run at the next "frame",
884
- // because a re-layout will be performed right now.
885
- if (this.layoutTimer) {
886
- clearTimeout(this.layoutTimer)
887
- this.layoutTimer = undefined
888
- }
889
- // Perform a re-layout.
890
- log(`~ Calculate Layout (on ${reason}) ~`)
891
- this.updateShownItemIndexes()
212
+ onItemHeightChange(i) {
213
+ this.hasToBeStarted()
214
+ this._onItemHeightChange(i)
892
215
  }
893
216
 
894
- updateLayout = () => this.onUpdateShownItemIndexes({ reason: LAYOUT_REASON.MANUAL })
895
-
896
- // `.layout()` method name is deprecated, use `.updateLayout()` instead.
897
- layout = () => this.updateLayout()
898
-
899
217
  /**
900
- * @deprecated
901
- * `.updateItems()` has been renamed to `.setItems()`.
218
+ * Updates an item's state in `state.itemStates[]`.
219
+ * @param {number} i Item index
220
+ * @param {any} i — Item's new state
902
221
  */
903
- updateItems(newItems, options) {
904
- return this.setItems(newItems, options)
222
+ onItemStateChange(i, newItemState) {
223
+ this.hasToBeStarted()
224
+ this._onItemStateChange(i, newItemState)
905
225
  }
906
226
 
907
227
  /**
@@ -910,101 +230,7 @@ export default class VirtualScroller {
910
230
  * @param {boolean} [options.preserveScrollPositionOnPrependItems] — Set to `true` to enable "restore scroll position after prepending items" feature (could be useful when implementing "Show previous items" button).
911
231
  */
912
232
  setItems(newItems, options = {}) {
913
- // * @param {object} [newCustomState] — If `customState` was passed to `getInitialState()`, this `newCustomState` updates it.
914
- const {
915
- items: previousItems
916
- } = this.getState()
917
- let {
918
- itemStates,
919
- itemHeights
920
- } = this.getState()
921
- log('~ Update items ~')
922
- let layout
923
- const itemsDiff = this.getItemsDiff(previousItems, newItems)
924
- // If it's an "incremental" update.
925
- if (itemsDiff && !this.layoutResetPending) {
926
- const {
927
- firstShownItemIndex,
928
- lastShownItemIndex,
929
- beforeItemsHeight,
930
- afterItemsHeight
931
- } = this.getState()
932
- layout = {
933
- firstShownItemIndex,
934
- lastShownItemIndex,
935
- beforeItemsHeight,
936
- afterItemsHeight
937
- }
938
- const {
939
- prependedItemsCount,
940
- appendedItemsCount
941
- } = itemsDiff
942
- if (prependedItemsCount > 0) {
943
- log('Prepend', prependedItemsCount, 'items')
944
- itemHeights = new Array(prependedItemsCount).concat(itemHeights)
945
- if (itemStates) {
946
- itemStates = new Array(prependedItemsCount).concat(itemStates)
947
- }
948
- }
949
- if (appendedItemsCount > 0) {
950
- log('Append', appendedItemsCount, 'items')
951
- itemHeights = itemHeights.concat(new Array(appendedItemsCount))
952
- if (itemStates) {
953
- itemStates = itemStates.concat(new Array(appendedItemsCount))
954
- }
955
- }
956
- this.layout.updateLayoutForItemsDiff(layout, itemsDiff, {
957
- itemsCount: newItems.length
958
- })
959
- if (prependedItemsCount > 0) {
960
- // `preserveScrollPosition` option name is deprecated,
961
- // use `preserveScrollPositionOnPrependItems` instead.
962
- if (options.preserveScrollPositionOnPrependItems || options.preserveScrollPosition) {
963
- if (this.getState().firstShownItemIndex === 0) {
964
- this.restoreScroll.captureScroll({
965
- previousItems,
966
- newItems,
967
- prependedItemsCount
968
- })
969
- this.layout.showItemsFromTheStart(layout)
970
- }
971
- }
972
- }
973
- } else {
974
- log('Items have changed, and', (itemsDiff ? 'a re-layout from scratch has been requested.' : 'it\'s not a simple append and/or prepend.'), 'Rerender the entire list from scratch.')
975
- log('Previous items', previousItems)
976
- log('New items', newItems)
977
- itemHeights = new Array(newItems.length)
978
- itemStates = new Array(newItems.length)
979
- layout = this.getInitialLayoutValues({
980
- itemsCount: newItems.length
981
- })
982
- }
983
- log('~ Update state ~')
984
- log('First shown item index', layout.firstShownItemIndex)
985
- log('Last shown item index', layout.lastShownItemIndex)
986
- log('Before items height', layout.beforeItemsHeight)
987
- log('After items height (actual or estimated)', layout.afterItemsHeight)
988
- // Optionally preload items to be rendered.
989
- this.onBeforeShowItems(
990
- newItems,
991
- itemHeights,
992
- layout.firstShownItemIndex,
993
- layout.lastShownItemIndex
994
- )
995
- // `this.newItemsPending` will be cleared in `didUpdateState()`.
996
- this.newItemsPending = newItems
997
- // Update state.
998
- this.setState({
999
- // ...customState,
1000
- ...layout,
1001
- items: newItems,
1002
- itemStates,
1003
- itemHeights
1004
- })
1005
- }
1006
-
1007
- getItemsDiff(previousItems, newItems) {
1008
- return getItemsDiff(previousItems, newItems, this.isItemEqual)
233
+ this.hasToBeStarted()
234
+ return this._setItems(newItems, options)
1009
235
  }
1010
236
  }