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,10 +1,12 @@
1
1
  export default class ScrollableContainer {
2
2
  /**
3
3
  * Constructs a new "scrollable container" from an element.
4
- * @param {Element} scrollableContainer
4
+ * @param {func} getElement — Returns the scrollable container element.
5
+ * @param {func} getItemsContainerElement — Returns items "container" element.
5
6
  */
6
- constructor(element) {
7
- this.element = element
7
+ constructor(getElement, getItemsContainerElement) {
8
+ this.getElement = getElement
9
+ this.getItemsContainerElement = getItemsContainerElement
8
10
  }
9
11
 
10
12
  /**
@@ -12,7 +14,7 @@ export default class ScrollableContainer {
12
14
  * @return {number}
13
15
  */
14
16
  getScrollY() {
15
- return this.element.scrollTop
17
+ return this.getElement().scrollTop
16
18
  }
17
19
 
18
20
  /**
@@ -23,10 +25,10 @@ export default class ScrollableContainer {
23
25
  // IE 11 doesn't seem to have a `.scrollTo()` method.
24
26
  // https://gitlab.com/catamphetamine/virtual-scroller/-/issues/10
25
27
  // https://stackoverflow.com/questions/39908825/window-scrollto-is-not-working-in-internet-explorer-11
26
- if (this.element.scrollTo) {
27
- this.element.scrollTo(0, scrollY)
28
+ if (this.getElement().scrollTo) {
29
+ this.getElement().scrollTo(0, scrollY)
28
30
  } else {
29
- this.element.scrollTop = scrollY
31
+ this.getElement().scrollTop = scrollY
30
32
  }
31
33
  }
32
34
 
@@ -36,7 +38,7 @@ export default class ScrollableContainer {
36
38
  * @return {number}
37
39
  */
38
40
  getWidth() {
39
- return this.element.offsetWidth
41
+ return this.getElement().offsetWidth
40
42
  }
41
43
 
42
44
  /**
@@ -45,64 +47,47 @@ export default class ScrollableContainer {
45
47
  * @return {number}
46
48
  */
47
49
  getHeight() {
48
- // if (!this.element && !precise) {
50
+ // if (!this.getElement() && !precise) {
49
51
  // return getScreenHeight()
50
52
  // }
51
- return this.element.offsetHeight
53
+ return this.getElement().offsetHeight
52
54
  }
53
55
 
54
56
  /**
55
- * Returns the height of the content in a scrollable container.
56
- * For example, a scrollable container can have a height of 500px,
57
- * but the content in it could have a height of 5000px,
58
- * in which case a vertical scrollbar is rendered, and only
59
- * one-tenth of all the items are shown at any given moment.
60
- * This function is currently only used when using the
61
- * `preserveScrollPositionOfTheBottomOfTheListOnMount` feature.
62
- * @return {number}
63
- */
64
- getContentHeight() {
65
- return this.element.scrollHeight
66
- }
67
-
68
- /**
69
- * Returns a "top offset" of an element
57
+ * Returns a "top offset" of an items container element
70
58
  * relative to the "scrollable container"'s top edge.
71
- * @param {Element} element
72
59
  * @return {number}
73
60
  */
74
- getTopOffset(element) {
75
- const scrollableContainerTop = this.element.getBoundingClientRect().top
76
- const scrollableContainerBorderTopWidth = this.element.clientTop
77
- const top = element.getBoundingClientRect().top
78
- return (top - scrollableContainerTop) + this.getScrollY() - scrollableContainerBorderTopWidth
61
+ getItemsContainerTopOffset() {
62
+ const scrollableContainerTop = this.getElement().getBoundingClientRect().top
63
+ const scrollableContainerBorderTopWidth = this.getElement().clientTop
64
+ const itemsContainerTop = this.getItemsContainerElement().getBoundingClientRect().top
65
+ return (itemsContainerTop - scrollableContainerTop) + this.getScrollY() - scrollableContainerBorderTopWidth
79
66
  }
80
67
 
81
68
  // isVisible() {
82
- // const { top, bottom } = this.element.getBoundingClientRect()
69
+ // const { top, bottom } = this.getElement().getBoundingClientRect()
83
70
  // return bottom > 0 && top < getScreenHeight()
84
71
  // }
85
72
 
86
73
  /**
87
74
  * Adds a "scroll" event listener to the "scrollable container".
88
- * @param {onScroll} Should be called whenever the scroll position inside the "scrollable container" (potentially) changes.
75
+ * @param {onScrollListener} Should be called whenever the scroll position inside the "scrollable container" (potentially) changes.
89
76
  * @return {function} Returns a function that stops listening.
90
77
  */
91
- addScrollListener(onScroll) {
92
- this.element.addEventListener('scroll', onScroll)
93
- return () => this.element.removeEventListener('scroll', onScroll)
78
+ onScroll(onScrollListener) {
79
+ const element = this.getElement()
80
+ element.addEventListener('scroll', onScrollListener)
81
+ return () => element.removeEventListener('scroll', onScrollListener)
94
82
  }
95
83
 
96
84
  /**
97
85
  * Adds a "resize" event listener to the "scrollable container".
98
86
  * @param {onResize} Should be called whenever the "scrollable container"'s width or height (potentially) changes.
99
- * @param {Element} options.container The result of the `getContainerElement()` function that was passed in `VirtualScroller` constructor. For example, DOM renderer uses it to filter-out unrelated "resize" events.
100
- * @return {function} Returns a function that stops listening.
87
+ * @return {function} Returns a function that stops listening.
101
88
  */
102
- onResize(onResize, { container }) {
103
- // Could somehow track DOM Element size.
104
- // For now, `scrollableContainer` is supposed to have constant width and height.
105
- // (unless window is resized).
89
+ onResize(onResize) {
90
+ // Watches "scrollable container"'s dimensions via a `ResizeObserver`.
106
91
  // https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver
107
92
  // https://web.dev/resize-observer/
108
93
  let unobserve
@@ -110,7 +95,7 @@ export default class ScrollableContainer {
110
95
  const resizeObserver = new ResizeObserver((entries) => {
111
96
  // "one entry per observed element".
112
97
  // https://web.dev/resize-observer/
113
- // `entry.target === this.element`.
98
+ // `entry.target === this.getElement()`.
114
99
  const entry = entries[0]
115
100
  // // If `entry.contentBoxSize` property is supported by the web browser.
116
101
  // if (entry.contentBoxSize) {
@@ -120,8 +105,9 @@ export default class ScrollableContainer {
120
105
  // }
121
106
  onResize()
122
107
  })
123
- resizeObserver.observe(this.element)
124
- unobserve = () => resizeObserver.unobserve(this.element)
108
+ const element = this.getElement()
109
+ resizeObserver.observe(element)
110
+ unobserve = () => resizeObserver.unobserve(element)
125
111
  }
126
112
  // I guess, if window is resized, `onResize()` will be triggered twice:
127
113
  // once for window resize, and once for the scrollable container resize.
@@ -129,7 +115,9 @@ export default class ScrollableContainer {
129
115
  // hasn't changed since the previous time `onResize()` has been called,
130
116
  // then `onResize()` doesn't do anything, so, I guess, there shouldn't be
131
117
  // any "performance implications" of running the listener twice in such case.
132
- const unlistenGlobalResize = addGlobalResizeListener(onResize, { container })
118
+ const unlistenGlobalResize = addGlobalResizeListener(onResize, {
119
+ itemsContainerElement: this.getItemsContainerElement()
120
+ })
133
121
  return () => {
134
122
  if (unobserve) {
135
123
  unobserve()
@@ -140,8 +128,12 @@ export default class ScrollableContainer {
140
128
  }
141
129
 
142
130
  export class ScrollableWindowContainer extends ScrollableContainer {
143
- constructor() {
144
- super(window)
131
+ /**
132
+ * Constructs a new window "scrollable container".
133
+ * @param {func} getItemsContainerElement — Returns items "container" element.
134
+ */
135
+ constructor(getItemsContainerElement) {
136
+ super(() => window, getItemsContainerElement)
145
137
  }
146
138
 
147
139
  /**
@@ -189,38 +181,24 @@ export class ScrollableWindowContainer extends ScrollableContainer {
189
181
  }
190
182
 
191
183
  /**
192
- * Returns the height of the content in a scrollable container.
193
- * For example, a scrollable container can have a height of 500px,
194
- * but the content in it could have a height of 5000px,
195
- * in which case a vertical scrollbar is rendered, and only
196
- * one-tenth of all the items are shown at any given moment.
197
- * This function is currently only used when using the
198
- * `preserveScrollPositionOfTheBottomOfTheListOnMount` feature.
199
- * @return {number}
200
- */
201
- getContentHeight() {
202
- return document.documentElement.scrollHeight
203
- }
204
-
205
- /**
206
- * Returns a "top offset" of an element
184
+ * Returns a "top offset" of an items container element
207
185
  * relative to the "scrollable container"'s top edge.
208
- * @param {Element} element
209
186
  * @return {number}
210
187
  */
211
- getTopOffset(element) {
188
+ getItemsContainerTopOffset() {
212
189
  const borderTopWidth = document.clientTop || document.body.clientTop || 0
213
- return element.getBoundingClientRect().top + this.getScrollY() - borderTopWidth
190
+ return this.getItemsContainerElement().getBoundingClientRect().top + this.getScrollY() - borderTopWidth
214
191
  }
215
192
 
216
193
  /**
217
194
  * Adds a "resize" event listener to the "scrollable container".
218
195
  * @param {onScroll} Should be called whenever the "scrollable container"'s width or height (potentially) changes.
219
- * @param {Element} options.container — The result of the `getContainerElement()` function that was passed in `VirtualScroller` constructor. For example, DOM renderer uses it to filter-out unrelated "resize" events.
220
196
  * @return {function} Returns a function that stops listening.
221
197
  */
222
- onResize(onResize, { container }) {
223
- return addGlobalResizeListener(onResize, { container })
198
+ onResize(onResize) {
199
+ return addGlobalResizeListener(onResize, {
200
+ itemsContainerElement: this.getItemsContainerElement()
201
+ })
224
202
  }
225
203
 
226
204
  // isVisible() {
@@ -230,11 +208,11 @@ export class ScrollableWindowContainer extends ScrollableContainer {
230
208
 
231
209
  /**
232
210
  * Adds a "resize" event listener to the `window`.
233
- * @param {onResize} Should be called whenever the "container"'s width or height (potentially) changes.
234
- * @param {Element} options.container — The "container".
211
+ * @param {onResize} Should be called whenever the "scrollable container"'s width or height (potentially) changes.
212
+ * @param {Element} options.itemsContainerElement — The items "container" element, which is not the same as the "scrollable container" element. For example, "scrollable container" could be resized while the list element retaining its size. One such example is a user entering fullscreen mode on an HTML5 `<video/>` element: in that case, a "resize" event is triggered on a window, and window dimensions change to the user's screen size, but such "resize" event can be ignored because the list isn't visible until the user exits fullscreen mode.
235
213
  * @return {function} Returns a function that stops listening.
236
214
  */
237
- function addGlobalResizeListener(onResize, { container }) {
215
+ function addGlobalResizeListener(onResize, { itemsContainerElement }) {
238
216
  const onResizeListener = () => {
239
217
  // By default, `VirtualScroller` always performs a re-layout
240
218
  // on window `resize` event. But browsers (Chrome, Firefox)
@@ -265,7 +243,7 @@ function addGlobalResizeListener(onResize, { container }) {
265
243
  // the layout wouldn't be affected too, so such `resize` event should also be
266
244
  // ignored: when `document.fullscreenElement` is inside the `container`.
267
245
  //
268
- if (document.fullscreenElement.contains(container)) {
246
+ if (document.fullscreenElement.contains(itemsContainerElement)) {
269
247
  // The element is either the `container`'s ancestor,
270
248
  // Or is the `container` itself.
271
249
  // (`a.contains(b)` includes the `a === b` case).
@@ -1,37 +1,42 @@
1
- import VirtualScrollerCore from '../VirtualScroller'
1
+ import VirtualScrollerCore from '../VirtualScroller.js'
2
2
 
3
- import log, { warn } from '../utility/debug'
4
- import px from '../utility/px'
3
+ import log, { warn } from '../utility/debug.js'
4
+ import px from '../utility/px.js'
5
5
 
6
6
  export default class VirtualScroller {
7
- constructor(element, items, renderItem, options = {}) {
8
- this.container = element
7
+ constructor(itemsContainerElement, items, renderItem, options = {}) {
8
+ this.container = itemsContainerElement
9
9
  this.renderItem = renderItem
10
+
10
11
  const {
11
12
  onMount,
12
13
  onItemUnmount,
13
14
  ...restOptions
14
15
  } = options
16
+
15
17
  this.onItemUnmount = onItemUnmount
16
18
  this.tbody = this.container.tagName === 'TBODY'
19
+
17
20
  this.virtualScroller = new VirtualScrollerCore(
18
21
  () => this.container,
19
22
  items,
20
23
  {
21
24
  ...restOptions,
22
- tbody: this.tbody,
23
- onStateChange: this.onStateChange
25
+ render: this.render,
26
+ tbody: this.tbody
24
27
  }
25
28
  )
29
+
30
+ this.start()
31
+
26
32
  // `onMount()` option is deprecated due to no longer being used.
27
33
  // If someone thinks there's a valid use case for it, create an issue.
28
34
  if (onMount) {
29
35
  onMount()
30
36
  }
31
- this.virtualScroller.listen()
32
37
  }
33
38
 
34
- onStateChange = (state, prevState) => {
39
+ render = (state, prevState) => {
35
40
  const {
36
41
  items,
37
42
  firstShownItemIndex,
@@ -39,9 +44,11 @@ export default class VirtualScroller {
39
44
  beforeItemsHeight,
40
45
  afterItemsHeight
41
46
  } = state
42
- log('~ On state change ~')
43
- log('Previous state', prevState)
44
- log('New state', state)
47
+
48
+ // log('~ On state change ~')
49
+ // log('Previous state', prevState)
50
+ // log('New state', state)
51
+
45
52
  // Set container padding top and bottom.
46
53
  // Work around `<tbody/>` not being able to have `padding`.
47
54
  // https://gitlab.com/catamphetamine/virtual-scroller/-/issues/1
@@ -51,11 +58,11 @@ export default class VirtualScroller {
51
58
  this.container.style.paddingTop = px(beforeItemsHeight)
52
59
  this.container.style.paddingBottom = px(afterItemsHeight)
53
60
  }
61
+
54
62
  // Perform an intelligent "diff" re-render if the `items` are the same.
55
63
  const diffRender = prevState && items === prevState.items && items.length > 0
56
64
  // Remove no longer visible items from the DOM.
57
65
  if (diffRender) {
58
- log('Incremental rerender')
59
66
  // Decrement instead of increment here because
60
67
  // `this.container.removeChild()` changes indexes.
61
68
  let i = prevState.lastShownItemIndex
@@ -63,18 +70,19 @@ export default class VirtualScroller {
63
70
  if (i >= firstShownItemIndex && i <= lastShownItemIndex) {
64
71
  // The item is still visible.
65
72
  } else {
66
- log('Remove item index', i)
73
+ log('DOM: Remove element for item index', i)
67
74
  // The item is no longer visible. Remove it.
68
75
  this.unmountItem(this.container.childNodes[i - prevState.firstShownItemIndex])
69
76
  }
70
77
  i--
71
78
  }
72
79
  } else {
73
- log('Rerender from scratch')
80
+ log('DOM: Rerender the list from scratch')
74
81
  while (this.container.firstChild) {
75
82
  this.unmountItem(this.container.firstChild)
76
83
  }
77
84
  }
85
+
78
86
  // Add newly visible items to the DOM.
79
87
  let shouldPrependItems = diffRender
80
88
  const prependBeforeItemElement = shouldPrependItems && this.container.firstChild
@@ -89,11 +97,11 @@ export default class VirtualScroller {
89
97
  } else {
90
98
  const item = this.renderItem(items[i])
91
99
  if (shouldPrependItems) {
92
- log('Prepend item index', i)
100
+ log('DOM: Prepend element for item index', i)
93
101
  // Append `item` to `this.container` before the retained items.
94
102
  this.container.insertBefore(item, prependBeforeItemElement)
95
103
  } else {
96
- log('Append item index', i)
104
+ log('DOM: Append element for item index', i)
97
105
  // Append `item` to `this.container`.
98
106
  this.container.appendChild(item)
99
107
  }
@@ -114,11 +122,18 @@ export default class VirtualScroller {
114
122
  this.stop()
115
123
  }
116
124
 
117
- // Public API. Should be "bound" to `this`.
125
+ // Public API.
126
+ // Should be "bound" to `this`.
118
127
  stop = () => {
119
128
  this.virtualScroller.stop()
120
129
  }
121
130
 
131
+ // Potentially public API in some hypothetical scenario.
132
+ // Should be "bound" to `this`.
133
+ start = () => {
134
+ this.virtualScroller.start()
135
+ }
136
+
122
137
  unmountItem(itemElement) {
123
138
  this.container.removeChild(itemElement)
124
139
  if (this.onItemUnmount) {
@@ -1,7 +1,7 @@
1
1
  // A workaround for `<tbody/>` not being able to have `padding`.
2
2
  // https://gitlab.com/catamphetamine/virtual-scroller/-/issues/1
3
3
 
4
- import px from '../utility/px'
4
+ import px from '../utility/px.js'
5
5
 
6
6
  export const BROWSER_NOT_SUPPORTED_ERROR = 'It looks like you\'re using Internet Explorer which doesn\'t support CSS variables required for a <tbody/> container. VirtualScroller has been switched into "bypass" mode (render all items). See: https://gitlab.com/catamphetamine/virtual-scroller/-/issues/1'
7
7
 
@@ -18,28 +18,37 @@ export function supportsTbody() {
18
18
  return true
19
19
  }
20
20
 
21
+ export const TBODY_CLASS_NAME = 'VirtualScroller'
22
+ const STYLE_ELEMENT_ID = 'VirtualScrollerStyle'
23
+
24
+ export function hasTbodyStyles(tbody) {
25
+ return tbody.classList.contains(TBODY_CLASS_NAME) &&
26
+ Boolean(document.getElementById(STYLE_ELEMENT_ID))
27
+ }
28
+
21
29
  export function addTbodyStyles(tbody) {
22
30
  // `classList.add` is supported in Internet Explorer 10+.
23
- tbody.classList.add('VirtualScroller')
24
- let style = document.getElementById('VirtualScrollerStyle')
25
- if (!style) {
26
- style = document.createElement('style')
27
- style.id = 'VirtualScrollerStyle'
28
- // CSS variables aren't supported in Internet Explorer.
29
- style.innerText = `
30
- tbody.VirtualScroller:before {
31
- content: '';
32
- display: table-row;
33
- height: var(--VirtualScroller-paddingTop);
34
- }
35
- tbody.VirtualScroller:after {
36
- content: '';
37
- display: table-row;
38
- height: var(--VirtualScroller-paddingBottom);
39
- }
40
- `.replace(/[\n\t]/g, '')
41
- document.head.appendChild(style)
42
- }
31
+ tbody.classList.add(TBODY_CLASS_NAME)
32
+
33
+ // Create a `<style/>` element.
34
+ const style = document.createElement('style')
35
+ style.id = STYLE_ELEMENT_ID
36
+
37
+ // CSS variables aren't supported in Internet Explorer.
38
+ style.innerText = `
39
+ tbody.${TBODY_CLASS_NAME}:before {
40
+ content: '';
41
+ display: table-row;
42
+ height: var(--VirtualScroller-paddingTop);
43
+ }
44
+ tbody.${TBODY_CLASS_NAME}:after {
45
+ content: '';
46
+ display: table-row;
47
+ height: var(--VirtualScroller-paddingBottom);
48
+ }
49
+ `.replace(/[\n\t]/g, '')
50
+
51
+ document.head.appendChild(style)
43
52
  }
44
53
 
45
54
  export function setTbodyPadding(tbody, beforeItemsHeight, afterItemsHeight) {
@@ -1,9 +1,13 @@
1
- import log, { warn, isDebug, reportError } from './utility/debug'
1
+ import log, { warn, isDebug, reportError } from './utility/debug.js'
2
2
 
3
3
  export default class ItemHeights {
4
- constructor(screen, getContainerElement, getItemHeight, setItemHeight) {
5
- this.screen = screen
6
- this.getContainerElement = getContainerElement
4
+ constructor({
5
+ container,
6
+ itemHeights,
7
+ getItemHeight,
8
+ setItemHeight
9
+ }) {
10
+ this.container = container
7
11
  this._get = getItemHeight
8
12
  this._set = setItemHeight
9
13
  this.reset()
@@ -25,14 +29,14 @@ export default class ItemHeights {
25
29
  }
26
30
 
27
31
  /**
28
- * Is called after `.reset()`.
32
+ * Can only be called after a `.reset()` (including new instance creation).
29
33
  * Initializes `this.measuredItemsHeight`, `this.firstMeasuredItemIndex`
30
34
  * and `this.lastMeasuredItemIndex` instance variables from `VirtualScroller` `state`.
31
35
  * These instance variables are used when calculating "average" item height:
32
36
  * the "average" item height is simply `this.measuredItemsHeight` divided by
33
37
  * `this.lastMeasuredItemIndex` minus `this.firstMeasuredItemIndex` plus 1.
34
38
  */
35
- initialize(itemHeights) {
39
+ readItemHeightsFromState({ itemHeights }) {
36
40
  let i = 0
37
41
  while (i < itemHeights.length) {
38
42
  if (itemHeights[i] === undefined) {
@@ -64,13 +68,7 @@ export default class ItemHeights {
64
68
  // }
65
69
 
66
70
  _measureItemHeight(i, firstShownItemIndex) {
67
- const container = this.getContainerElement()
68
- if (container) {
69
- const elementIndex = i - firstShownItemIndex
70
- if (elementIndex >= 0 && elementIndex < this.screen.getChildElementsCount(container)) {
71
- return this.screen.getChildElementHeight(container, elementIndex)
72
- }
73
- }
71
+ return this.container.getNthRenderedItemHeight(i - firstShownItemIndex)
74
72
  }
75
73
 
76
74
  /**
@@ -92,6 +90,7 @@ export default class ItemHeights {
92
90
  * @return {number[]} The indexes of the items that have not previously been measured and have been measured now.
93
91
  */
94
92
  measureItemHeights(firstShownItemIndex, lastShownItemIndex) {
93
+ log('~ Measure item heights ~')
95
94
  // If no items are rendered, don't measure anything.
96
95
  if (firstShownItemIndex === undefined) {
97
96
  return
@@ -102,8 +101,10 @@ export default class ItemHeights {
102
101
  // then reset `this.measuredItemsHeight` and "first measured"/"last measured" item indexes.
103
102
  // For example, this could happen when `.setItems()` prepends a lot of new items.
104
103
  if (this.firstMeasuredItemIndex !== undefined) {
105
- if (firstShownItemIndex > this.lastMeasuredItemIndex + 1 ||
106
- lastShownItemIndex < this.firstMeasuredItemIndex - 1) {
104
+ if (
105
+ firstShownItemIndex > this.lastMeasuredItemIndex + 1 ||
106
+ lastShownItemIndex < this.firstMeasuredItemIndex - 1
107
+ ) {
107
108
  // Reset.
108
109
  log('Non-measured items gap detected. Reset first and last measured item indexes.')
109
110
  this.reset()
@@ -115,20 +116,16 @@ export default class ItemHeights {
115
116
  let firstMeasuredItemIndexHasBeenUpdated = false
116
117
  let i = firstShownItemIndex
117
118
  while (i <= lastShownItemIndex) {
119
+ // Measure item heights that haven't been measured previously.
118
120
  // Don't re-measure item heights that have been measured previously.
119
121
  // The rationale is that developers are supposed to manually call
120
122
  // `.onItemHeightChange()` every time an item's height changes.
121
- // If developers aren't neglecting that rule, item heights won't
123
+ // If developers don't neglect that rule, item heights won't
122
124
  // change unexpectedly.
123
- // // Re-measure all shown items' heights, because an item's height
124
- // // might have changed since it has been measured initially.
125
- // // For example, if an item is a long comment with a "Show more" button,
126
- // // then the user might have clicked that "Show more" button.
127
125
  if (this._get(i) === undefined) {
128
126
  nonPreviouslyMeasuredItemIndexes.push(i)
129
- log('Item', i, 'hasn\'t been previously measured')
130
127
  const height = this._measureItemHeight(i, firstShownItemIndex)
131
- log('Height', height)
128
+ log('Item index', i, 'height', height)
132
129
  this._set(i, height)
133
130
  // Update average item height calculation variables
134
131
  // related to the previously measured items
@@ -164,14 +161,17 @@ export default class ItemHeights {
164
161
  this.lastMeasuredItemIndex = i
165
162
  }
166
163
  } else {
167
- // Validate the item's height right after showing it after being hidden,
168
- // because, if the stored item's state isn't applied properly, the item's
169
- // height might be incorrect when it's rendered with that state not applied,
170
- // and so a developer could know that there's a bug in their code.
164
+ // Validate that the item's height didn't change since it was last measured.
165
+ // If it did, then display a warning and update the item's height
166
+ // as an attempt to fix things.
167
+ // If an item's height changes unexpectedly then it means that there'll
168
+ // likely be "content jumping".
171
169
  const previousHeight = this._get(i)
172
170
  const height = this._measureItemHeight(i, firstShownItemIndex)
173
171
  if (previousHeight !== height) {
174
- warn('Item', i, 'height was', previousHeight, 'before it was hidden, but, after showing it again, its height is', height, '. Perhaps you forgot to persist the item\'s state by calling `onItemStateChange(i, newState)` when it changed, and that state got lost when the item element was unmounted, which resulted in a different height when the item was shown again, but with the missing state.')
172
+ warn('Item index', i, 'height changed unexpectedly: it was', previousHeight, 'before, but now it is', height, '. An item\'s height is allowed to change only in two cases: when the item\'s "state" changes and the developer calls `onItemStateChange(i, newState)`, or when the item\'s height changes for some other reason and the developer calls `onItemHeightChange(i)`. Perhaps you forgot to persist the item\'s "state" by calling `onItemStateChange(i, newState)` when it changed, and that "state" got lost when the item element was unmounted, which resulted in a different height when the item was shown again having its "state" reset.')
173
+ // Update the item's height as an attempt to fix things.
174
+ this._set(i, height)
175
175
  }
176
176
  }
177
177
  i++