virtual-scroller 1.7.7 → 1.8.1

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 (154) hide show
  1. package/.gitlab-ci.yml +1 -1
  2. package/CHANGELOG.md +24 -1
  3. package/README.md +139 -33
  4. package/babel.config.js +25 -0
  5. package/babel.js +5 -0
  6. package/bundle/index-bypass.html +1 -1
  7. package/bundle/index-dom.html +1 -1
  8. package/bundle/index-grid.html +1 -2
  9. package/bundle/index-scrollableContainer.html +1 -1
  10. package/bundle/index-tbody-scrollableContainer.html +2 -0
  11. package/bundle/index-tbody.html +2 -0
  12. package/bundle/virtual-scroller-dom.js +1 -1
  13. package/bundle/virtual-scroller-dom.js.map +1 -1
  14. package/bundle/virtual-scroller-react.js +1 -1
  15. package/bundle/virtual-scroller-react.js.map +1 -1
  16. package/bundle/virtual-scroller.js +1 -1
  17. package/bundle/virtual-scroller.js.map +1 -1
  18. package/commonjs/BeforeResize.js +319 -0
  19. package/commonjs/BeforeResize.js.map +1 -0
  20. package/commonjs/DOM/Engine.js +46 -0
  21. package/commonjs/DOM/Engine.js.map +1 -0
  22. package/commonjs/DOM/ItemsContainer.js +78 -0
  23. package/commonjs/DOM/ItemsContainer.js.map +1 -0
  24. package/commonjs/DOM/{WaitForStylesToLoad.js → ListTopOffsetWatcher.js} +56 -35
  25. package/commonjs/DOM/ListTopOffsetWatcher.js.map +1 -0
  26. package/commonjs/DOM/ScrollableContainer.js +56 -81
  27. package/commonjs/DOM/ScrollableContainer.js.map +1 -1
  28. package/commonjs/DOM/VirtualScroller.js +20 -15
  29. package/commonjs/DOM/VirtualScroller.js.map +1 -1
  30. package/commonjs/DOM/tbody.js +2 -2
  31. package/commonjs/ItemHeights.js +22 -29
  32. package/commonjs/ItemHeights.js.map +1 -1
  33. package/commonjs/Layout.js +588 -215
  34. package/commonjs/Layout.js.map +1 -1
  35. package/commonjs/Layout.test.js +191 -0
  36. package/commonjs/Layout.test.js.map +1 -0
  37. package/commonjs/ListHeightChangeWatcher.js +126 -0
  38. package/commonjs/ListHeightChangeWatcher.js.map +1 -0
  39. package/commonjs/Resize.js +22 -21
  40. package/commonjs/Resize.js.map +1 -1
  41. package/commonjs/Scroll.js +148 -88
  42. package/commonjs/Scroll.js.map +1 -1
  43. package/commonjs/VirtualScroller.js +1269 -390
  44. package/commonjs/VirtualScroller.js.map +1 -1
  45. package/commonjs/getItemCoordinates.js.map +1 -1
  46. package/commonjs/getItemsDiff.js.map +1 -1
  47. package/commonjs/getVerticalSpacing.js +8 -8
  48. package/commonjs/getVerticalSpacing.js.map +1 -1
  49. package/commonjs/react/VirtualScroller.js +31 -37
  50. package/commonjs/react/VirtualScroller.js.map +1 -1
  51. package/commonjs/utility/debounce.js +26 -4
  52. package/commonjs/utility/debounce.js.map +1 -1
  53. package/commonjs/utility/debug.js +51 -12
  54. package/commonjs/utility/debug.js.map +1 -1
  55. package/commonjs/utility/getStateSnapshot.js +50 -0
  56. package/commonjs/utility/getStateSnapshot.js.map +1 -0
  57. package/commonjs/utility/px.js +1 -1
  58. package/commonjs/utility/px.js.map +1 -1
  59. package/commonjs/utility/px.test.js +14 -0
  60. package/commonjs/utility/px.test.js.map +1 -0
  61. package/commonjs/utility/shallowEqual.js +1 -1
  62. package/commonjs/utility/shallowEqual.js.map +1 -1
  63. package/commonjs/utility/throttle.js.map +1 -1
  64. package/dom/index.d.ts +23 -0
  65. package/index.d.ts +84 -0
  66. package/modules/BeforeResize.js +310 -0
  67. package/modules/BeforeResize.js.map +1 -0
  68. package/modules/DOM/Engine.js +27 -0
  69. package/modules/DOM/Engine.js.map +1 -0
  70. package/modules/DOM/ItemsContainer.js +71 -0
  71. package/modules/DOM/ItemsContainer.js.map +1 -0
  72. package/modules/DOM/{WaitForStylesToLoad.js → ListTopOffsetWatcher.js} +57 -35
  73. package/modules/DOM/ListTopOffsetWatcher.js.map +1 -0
  74. package/modules/DOM/ScrollableContainer.js +55 -80
  75. package/modules/DOM/ScrollableContainer.js.map +1 -1
  76. package/modules/DOM/VirtualScroller.js +15 -14
  77. package/modules/DOM/VirtualScroller.js.map +1 -1
  78. package/modules/ItemHeights.js +17 -28
  79. package/modules/ItemHeights.js.map +1 -1
  80. package/modules/Layout.js +582 -213
  81. package/modules/Layout.js.map +1 -1
  82. package/modules/Layout.test.js +185 -0
  83. package/modules/Layout.test.js.map +1 -0
  84. package/modules/ListHeightChangeWatcher.js +119 -0
  85. package/modules/ListHeightChangeWatcher.js.map +1 -0
  86. package/modules/Resize.js +21 -20
  87. package/modules/Resize.js.map +1 -1
  88. package/modules/Scroll.js +148 -87
  89. package/modules/Scroll.js.map +1 -1
  90. package/modules/VirtualScroller.js +1263 -390
  91. package/modules/VirtualScroller.js.map +1 -1
  92. package/modules/getItemCoordinates.js.map +1 -1
  93. package/modules/getItemsDiff.js.map +1 -1
  94. package/modules/getVerticalSpacing.js +8 -8
  95. package/modules/getVerticalSpacing.js.map +1 -1
  96. package/modules/react/VirtualScroller.js +31 -37
  97. package/modules/react/VirtualScroller.js.map +1 -1
  98. package/modules/utility/debounce.js +26 -4
  99. package/modules/utility/debounce.js.map +1 -1
  100. package/modules/utility/debug.js +47 -10
  101. package/modules/utility/debug.js.map +1 -1
  102. package/modules/utility/getStateSnapshot.js +43 -0
  103. package/modules/utility/getStateSnapshot.js.map +1 -0
  104. package/modules/utility/px.js +1 -1
  105. package/modules/utility/px.js.map +1 -1
  106. package/modules/utility/px.test.js +9 -0
  107. package/modules/utility/px.test.js.map +1 -0
  108. package/modules/utility/shallowEqual.js +1 -1
  109. package/modules/utility/shallowEqual.js.map +1 -1
  110. package/modules/utility/throttle.js.map +1 -1
  111. package/package.json +24 -22
  112. package/react/index.d.ts +27 -0
  113. package/source/BeforeResize.js +317 -0
  114. package/source/DOM/Engine.js +32 -0
  115. package/source/DOM/ItemsContainer.js +48 -0
  116. package/source/DOM/{WaitForStylesToLoad.js → ListTopOffsetWatcher.js} +48 -22
  117. package/source/DOM/ScrollableContainer.js +39 -56
  118. package/source/DOM/VirtualScroller.js +6 -7
  119. package/source/ItemHeights.js +19 -24
  120. package/source/Layout.js +626 -252
  121. package/source/Layout.test.js +171 -0
  122. package/source/ListHeightChangeWatcher.js +94 -0
  123. package/source/Resize.js +23 -15
  124. package/source/Scroll.js +139 -78
  125. package/source/VirtualScroller.js +1243 -286
  126. package/source/getVerticalSpacing.js +7 -7
  127. package/source/react/VirtualScroller.js +2 -18
  128. package/source/utility/debounce.js +20 -3
  129. package/source/utility/debug.js +34 -3
  130. package/source/utility/getStateSnapshot.js +36 -0
  131. package/source/utility/px.js +1 -1
  132. package/source/utility/px.test.js +9 -0
  133. package/website/index-bypass.html +195 -0
  134. package/website/index-grid.html +0 -1
  135. package/website/index-scrollableContainer.html +208 -0
  136. package/website/index-tbody-scrollableContainer.html +68 -0
  137. package/website/index-tbody.html +55 -0
  138. package/commonjs/DOM/RenderingEngine.js +0 -33
  139. package/commonjs/DOM/RenderingEngine.js.map +0 -1
  140. package/commonjs/DOM/Screen.js +0 -87
  141. package/commonjs/DOM/Screen.js.map +0 -1
  142. package/commonjs/DOM/WaitForStylesToLoad.js.map +0 -1
  143. package/commonjs/RestoreScroll.js +0 -118
  144. package/commonjs/RestoreScroll.js.map +0 -1
  145. package/modules/DOM/RenderingEngine.js +0 -19
  146. package/modules/DOM/RenderingEngine.js.map +0 -1
  147. package/modules/DOM/Screen.js +0 -80
  148. package/modules/DOM/Screen.js.map +0 -1
  149. package/modules/DOM/WaitForStylesToLoad.js.map +0 -1
  150. package/modules/RestoreScroll.js +0 -111
  151. package/modules/RestoreScroll.js.map +0 -1
  152. package/source/DOM/RenderingEngine.js +0 -22
  153. package/source/DOM/Screen.js +0 -51
  154. package/source/RestoreScroll.js +0 -86
@@ -4,47 +4,72 @@
4
4
  // https://github.com/bvaughn/react-virtualized/issues/722
5
5
  import { setTimeout, clearTimeout } from 'request-animation-frame-timeout'
6
6
 
7
- import { LAYOUT_REASON } from '../Layout'
7
+ // Refreshing two times every seconds seems reasonable.
8
+ const WATCH_LIST_TOP_OFFSET_INTERVAL = 500
9
+
10
+ // Refreshing for 3 seconds after the initial page load seems reasonable.
11
+ const WATCH_LIST_TOP_OFFSET_MAX_DURATION = 3000
8
12
 
9
13
  // `VirtualScroller` calls `this.layout.layOut()` on mount,
10
14
  // but if the page styles are applied after `VirtualScroller` mounts
11
15
  // (for example, if styles are applied via javascript, like Webpack does)
12
- // then the list might not render correctly and will only show the first item.
13
- // The reason for that would be that calling `.getListTopOffsetInsideScrollableContainer()`
14
- // on mount returns "incorrect" `top` position because the styles haven't been applied yet.
16
+ // then the list might not render correctly and it will only show the first item.
17
+ // The reason is that in that case calling `.getListTopOffset()` on mount
18
+ // returns "incorrect" `top` position because the styles haven't been applied yet.
19
+ //
15
20
  // For example, consider a page:
16
21
  // <div class="page">
17
22
  // <nav class="sidebar">...</nav>
18
23
  // <main>...</main>
19
24
  // </div>
25
+ //
20
26
  // The sidebar is styled as `position: fixed`, but until
21
27
  // the page styles have been applied it's gonna be a regular `<div/>`
22
28
  // meaning that `<main/>` will be rendered below the sidebar
23
29
  // and will appear offscreen and so it will only render the first item.
30
+ //
24
31
  // Then, the page styles are loaded and applied and the sidebar
25
32
  // is now `position: fixed` so `<main/>` is now rendered at the top of the page
26
33
  // but `VirtualScroller`'s `.render()` has already been called
27
34
  // and it won't re-render until the user scrolls or the window is resized.
28
- // This type of a bug doesn't occur in production, but it can appear
35
+ //
36
+ // This type of a bug doesn't seem to occur in production, but it can appear
29
37
  // in development mode when using Webpack. The workaround `VirtualScroller`
30
- // implements for such cases is calling `.getListTopOffsetInsideScrollableContainer()`
38
+ // implements for such cases is calling `.getListTopOffset()`
31
39
  // on the list container DOM element periodically (every second) to check
32
40
  // if the `top` coordinate has changed as a result of CSS being applied:
33
41
  // if it has then it recalculates the shown item indexes.
34
- export default class WaitForStylesToLoad {
42
+ //
43
+ // Maybe this bug could occur in production when using Webpack chunks.
44
+ // That depends on how a style of a chunk is added to the page:
45
+ // if it's added via `javascript` after the page has been rendered
46
+ // then this workaround will also work for that case.
47
+ //
48
+ // Another example would be a page having a really tall expanded "accordion"
49
+ // section, below which a `VirtualScroller` list resides. If the user un-expands
50
+ // such expanded "accordion" section, the list would become visible but
51
+ // it wouldn't get re-rendered because no `scroll` event has occured,
52
+ // and the list only re-renders automatically on `scroll` events.
53
+ // To work around such cases, call `virtualScroller.updateLayout()` method manually.
54
+ // The workaround below could be extended to refresh the list's top coordinate
55
+ // indefinitely and at higher intervals, but why waste CPU time on that.
56
+ // There doesn't seem to be any DOM API for tracking an element's top position.
57
+ // There is `IntersectionObserver` API but it doesn't exactly do that.
58
+ //
59
+ export default class ListTopOffsetWatcher {
35
60
  constructor({
36
- updateLayout,
37
- getListTopOffsetInsideScrollableContainer
61
+ getListTopOffset,
62
+ onListTopOffsetChange
38
63
  }) {
39
- this.updateLayout = updateLayout
40
- this.getListTopOffsetInsideScrollableContainer = getListTopOffsetInsideScrollableContainer
64
+ this.getListTopOffset = getListTopOffset
65
+ this.onListTopOffsetChange = onListTopOffsetChange
41
66
  }
42
67
 
43
- onGotListTopOffset(listTopOffset) {
68
+ onListTopOffset(listTopOffset) {
44
69
  if (this.listTopOffsetInsideScrollableContainer === undefined) {
45
70
  // Start periodical checks of the list's top offset
46
71
  // in order to perform a re-layout in case it changes.
47
- // See the comments in `WaitForStylesToLoad.js` file
72
+ // See the comments in `ListTopOffsetWatcher.js` file
48
73
  // on why can the list's top offset change, and in which circumstances.
49
74
  this.start()
50
75
  }
@@ -58,7 +83,11 @@ export default class WaitForStylesToLoad {
58
83
 
59
84
  stop() {
60
85
  this.isRendered = false
61
- clearTimeout(this.watchListTopOffsetTimer)
86
+
87
+ if (this.watchListTopOffsetTimer) {
88
+ clearTimeout(this.watchListTopOffsetTimer)
89
+ this.watchListTopOffsetTimer = undefined
90
+ }
62
91
  }
63
92
 
64
93
  watchListTopOffset() {
@@ -72,11 +101,11 @@ export default class WaitForStylesToLoad {
72
101
  // Skip comparing `top` coordinate of the list
73
102
  // when this function is called for the first time.
74
103
  if (this.listTopOffsetInsideScrollableContainer !== undefined) {
75
- // Calling `this.getListTopOffsetInsideScrollableContainer()`
76
- // on an element is about 0.003 milliseconds on a modern desktop CPU,
104
+ // Calling `this.getListTopOffset()` on an element
105
+ // runs about 0.003 milliseconds on a modern desktop CPU,
77
106
  // so I guess it's fine calling it twice a second.
78
- if (this.getListTopOffsetInsideScrollableContainer() !== this.listTopOffsetInsideScrollableContainer) {
79
- this.updateLayout({ reason: LAYOUT_REASON.TOP_OFFSET_CHANGED })
107
+ if (this.getListTopOffset() !== this.listTopOffsetInsideScrollableContainer) {
108
+ this.onListTopOffsetChange()
80
109
  }
81
110
  }
82
111
  // Compare `top` coordinate of the list twice a second
@@ -93,7 +122,4 @@ export default class WaitForStylesToLoad {
93
122
  // Run the cycle.
94
123
  check()
95
124
  }
96
- }
97
-
98
- const WATCH_LIST_TOP_OFFSET_INTERVAL = 500
99
- const WATCH_LIST_TOP_OFFSET_MAX_DURATION = 3000
125
+ }
@@ -2,9 +2,11 @@ export default class ScrollableContainer {
2
2
  /**
3
3
  * Constructs a new "scrollable container" from an element.
4
4
  * @param {Element} scrollableContainer
5
+ * @param {func} getItemsContainerElement — Returns items "container" element.
5
6
  */
6
- constructor(element) {
7
+ constructor(element, getItemsContainerElement) {
7
8
  this.element = element
9
+ this.getItemsContainerElement = getItemsContainerElement
8
10
  }
9
11
 
10
12
  /**
@@ -20,7 +22,14 @@ export default class ScrollableContainer {
20
22
  * @param {number} scrollY
21
23
  */
22
24
  scrollToY(scrollY) {
23
- this.element.scrollTo(0, scrollY)
25
+ // IE 11 doesn't seem to have a `.scrollTo()` method.
26
+ // https://gitlab.com/catamphetamine/virtual-scroller/-/issues/10
27
+ // https://stackoverflow.com/questions/39908825/window-scrollto-is-not-working-in-internet-explorer-11
28
+ if (this.element.scrollTo) {
29
+ this.element.scrollTo(0, scrollY)
30
+ } else {
31
+ this.element.scrollTop = scrollY
32
+ }
24
33
  }
25
34
 
26
35
  /**
@@ -45,30 +54,15 @@ export default class ScrollableContainer {
45
54
  }
46
55
 
47
56
  /**
48
- * Returns the height of the content in a scrollable container.
49
- * For example, a scrollable container can have a height of 500px,
50
- * but the content in it could have a height of 5000px,
51
- * in which case a vertical scrollbar is rendered, and only
52
- * one-tenth of all the items are shown at any given moment.
53
- * This function is currently only used when using the
54
- * `preserveScrollPositionOfTheBottomOfTheListOnMount` feature.
55
- * @return {number}
56
- */
57
- getContentHeight() {
58
- return this.element.scrollHeight
59
- }
60
-
61
- /**
62
- * Returns a "top offset" of an element
57
+ * Returns a "top offset" of an items container element
63
58
  * relative to the "scrollable container"'s top edge.
64
- * @param {Element} element
65
59
  * @return {number}
66
60
  */
67
- getTopOffset(element) {
61
+ getItemsContainerTopOffset() {
68
62
  const scrollableContainerTop = this.element.getBoundingClientRect().top
69
63
  const scrollableContainerBorderTopWidth = this.element.clientTop
70
- const top = element.getBoundingClientRect().top
71
- return (top - scrollableContainerTop) + this.getScrollY() - scrollableContainerBorderTopWidth
64
+ const itemsContainerTop = this.getItemsContainerElement().getBoundingClientRect().top
65
+ return (itemsContainerTop - scrollableContainerTop) + this.getScrollY() - scrollableContainerBorderTopWidth
72
66
  }
73
67
 
74
68
  // isVisible() {
@@ -81,7 +75,7 @@ export default class ScrollableContainer {
81
75
  * @param {onScroll} Should be called whenever the scroll position inside the "scrollable container" (potentially) changes.
82
76
  * @return {function} Returns a function that stops listening.
83
77
  */
84
- addScrollListener(onScroll) {
78
+ onScroll(onScroll) {
85
79
  this.element.addEventListener('scroll', onScroll)
86
80
  return () => this.element.removeEventListener('scroll', onScroll)
87
81
  }
@@ -89,13 +83,10 @@ export default class ScrollableContainer {
89
83
  /**
90
84
  * Adds a "resize" event listener to the "scrollable container".
91
85
  * @param {onResize} Should be called whenever the "scrollable container"'s width or height (potentially) changes.
92
- * @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.
93
- * @return {function} Returns a function that stops listening.
86
+ * @return {function} Returns a function that stops listening.
94
87
  */
95
- onResize(onResize, { container }) {
96
- // Could somehow track DOM Element size.
97
- // For now, `scrollableContainer` is supposed to have constant width and height.
98
- // (unless window is resized).
88
+ onResize(onResize) {
89
+ // Watches "scrollable container"'s dimensions via a `ResizeObserver`.
99
90
  // https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver
100
91
  // https://web.dev/resize-observer/
101
92
  let unobserve
@@ -122,7 +113,9 @@ export default class ScrollableContainer {
122
113
  // hasn't changed since the previous time `onResize()` has been called,
123
114
  // then `onResize()` doesn't do anything, so, I guess, there shouldn't be
124
115
  // any "performance implications" of running the listener twice in such case.
125
- const unlistenGlobalResize = addGlobalResizeListener(onResize, { container })
116
+ const unlistenGlobalResize = addGlobalResizeListener(onResize, {
117
+ itemsContainerElement: this.getItemsContainerElement()
118
+ })
126
119
  return () => {
127
120
  if (unobserve) {
128
121
  unobserve()
@@ -133,8 +126,12 @@ export default class ScrollableContainer {
133
126
  }
134
127
 
135
128
  export class ScrollableWindowContainer extends ScrollableContainer {
136
- constructor() {
137
- super(window)
129
+ /**
130
+ * Constructs a new window "scrollable container".
131
+ * @param {func} getItemsContainerElement — Returns items "container" element.
132
+ */
133
+ constructor(getItemsContainerElement) {
134
+ super(window, getItemsContainerElement)
138
135
  }
139
136
 
140
137
  /**
@@ -182,38 +179,24 @@ export class ScrollableWindowContainer extends ScrollableContainer {
182
179
  }
183
180
 
184
181
  /**
185
- * Returns the height of the content in a scrollable container.
186
- * For example, a scrollable container can have a height of 500px,
187
- * but the content in it could have a height of 5000px,
188
- * in which case a vertical scrollbar is rendered, and only
189
- * one-tenth of all the items are shown at any given moment.
190
- * This function is currently only used when using the
191
- * `preserveScrollPositionOfTheBottomOfTheListOnMount` feature.
192
- * @return {number}
193
- */
194
- getContentHeight() {
195
- return document.documentElement.scrollHeight
196
- }
197
-
198
- /**
199
- * Returns a "top offset" of an element
182
+ * Returns a "top offset" of an items container element
200
183
  * relative to the "scrollable container"'s top edge.
201
- * @param {Element} element
202
184
  * @return {number}
203
185
  */
204
- getTopOffset(element) {
186
+ getItemsContainerTopOffset() {
205
187
  const borderTopWidth = document.clientTop || document.body.clientTop || 0
206
- return element.getBoundingClientRect().top + this.getScrollY() - borderTopWidth
188
+ return this.getItemsContainerElement().getBoundingClientRect().top + this.getScrollY() - borderTopWidth
207
189
  }
208
190
 
209
191
  /**
210
192
  * Adds a "resize" event listener to the "scrollable container".
211
193
  * @param {onScroll} Should be called whenever the "scrollable container"'s width or height (potentially) changes.
212
- * @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.
213
194
  * @return {function} Returns a function that stops listening.
214
195
  */
215
- onResize(onResize, { container }) {
216
- return addGlobalResizeListener(onResize, { container })
196
+ onResize(onResize) {
197
+ return addGlobalResizeListener(onResize, {
198
+ itemsContainerElement: this.getItemsContainerElement()
199
+ })
217
200
  }
218
201
 
219
202
  // isVisible() {
@@ -223,11 +206,11 @@ export class ScrollableWindowContainer extends ScrollableContainer {
223
206
 
224
207
  /**
225
208
  * Adds a "resize" event listener to the `window`.
226
- * @param {onResize} Should be called whenever the "container"'s width or height (potentially) changes.
227
- * @param {Element} options.container — The "container".
209
+ * @param {onResize} Should be called whenever the "scrollable container"'s width or height (potentially) changes.
210
+ * @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.
228
211
  * @return {function} Returns a function that stops listening.
229
212
  */
230
- function addGlobalResizeListener(onResize, { container }) {
213
+ function addGlobalResizeListener(onResize, { itemsContainerElement }) {
231
214
  const onResizeListener = () => {
232
215
  // By default, `VirtualScroller` always performs a re-layout
233
216
  // on window `resize` event. But browsers (Chrome, Firefox)
@@ -258,7 +241,7 @@ function addGlobalResizeListener(onResize, { container }) {
258
241
  // the layout wouldn't be affected too, so such `resize` event should also be
259
242
  // ignored: when `document.fullscreenElement` is inside the `container`.
260
243
  //
261
- if (document.fullscreenElement.contains(container)) {
244
+ if (document.fullscreenElement.contains(itemsContainerElement)) {
262
245
  // The element is either the `container`'s ancestor,
263
246
  // Or is the `container` itself.
264
247
  // (`a.contains(b)` includes the `a === b` case).
@@ -4,8 +4,8 @@ import log, { warn } from '../utility/debug'
4
4
  import px from '../utility/px'
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
  const {
11
11
  onMount,
@@ -55,7 +55,6 @@ export default class VirtualScroller {
55
55
  const diffRender = prevState && items === prevState.items && items.length > 0
56
56
  // Remove no longer visible items from the DOM.
57
57
  if (diffRender) {
58
- log('Incremental rerender')
59
58
  // Decrement instead of increment here because
60
59
  // `this.container.removeChild()` changes indexes.
61
60
  let i = prevState.lastShownItemIndex
@@ -63,14 +62,14 @@ export default class VirtualScroller {
63
62
  if (i >= firstShownItemIndex && i <= lastShownItemIndex) {
64
63
  // The item is still visible.
65
64
  } else {
66
- log('Remove item index', i)
65
+ log('DOM: Remove element for item index', i)
67
66
  // The item is no longer visible. Remove it.
68
67
  this.unmountItem(this.container.childNodes[i - prevState.firstShownItemIndex])
69
68
  }
70
69
  i--
71
70
  }
72
71
  } else {
73
- log('Rerender from scratch')
72
+ log('DOM: Rerender the list from scratch')
74
73
  while (this.container.firstChild) {
75
74
  this.unmountItem(this.container.firstChild)
76
75
  }
@@ -89,11 +88,11 @@ export default class VirtualScroller {
89
88
  } else {
90
89
  const item = this.renderItem(items[i])
91
90
  if (shouldPrependItems) {
92
- log('Prepend item index', i)
91
+ log('DOM: Prepend element for item index', i)
93
92
  // Append `item` to `this.container` before the retained items.
94
93
  this.container.insertBefore(item, prependBeforeItemElement)
95
94
  } else {
96
- log('Append item index', i)
95
+ log('DOM: Append element for item index', i)
97
96
  // Append `item` to `this.container`.
98
97
  this.container.appendChild(item)
99
98
  }
@@ -1,9 +1,8 @@
1
1
  import log, { warn, isDebug, reportError } from './utility/debug'
2
2
 
3
3
  export default class ItemHeights {
4
- constructor(screen, getContainerElement, getItemHeight, setItemHeight) {
5
- this.screen = screen
6
- this.getContainerElement = getContainerElement
4
+ constructor(container, getItemHeight, setItemHeight) {
5
+ this.container = container
7
6
  this._get = getItemHeight
8
7
  this._set = setItemHeight
9
8
  this.reset()
@@ -64,13 +63,7 @@ export default class ItemHeights {
64
63
  // }
65
64
 
66
65
  _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
- }
66
+ return this.container.getNthRenderedItemHeight(i - firstShownItemIndex)
74
67
  }
75
68
 
76
69
  /**
@@ -92,6 +85,7 @@ export default class ItemHeights {
92
85
  * @return {number[]} The indexes of the items that have not previously been measured and have been measured now.
93
86
  */
94
87
  measureItemHeights(firstShownItemIndex, lastShownItemIndex) {
88
+ log('~ Measure item heights ~')
95
89
  // If no items are rendered, don't measure anything.
96
90
  if (firstShownItemIndex === undefined) {
97
91
  return
@@ -102,8 +96,10 @@ export default class ItemHeights {
102
96
  // then reset `this.measuredItemsHeight` and "first measured"/"last measured" item indexes.
103
97
  // For example, this could happen when `.setItems()` prepends a lot of new items.
104
98
  if (this.firstMeasuredItemIndex !== undefined) {
105
- if (firstShownItemIndex > this.lastMeasuredItemIndex + 1 ||
106
- lastShownItemIndex < this.firstMeasuredItemIndex - 1) {
99
+ if (
100
+ firstShownItemIndex > this.lastMeasuredItemIndex + 1 ||
101
+ lastShownItemIndex < this.firstMeasuredItemIndex - 1
102
+ ) {
107
103
  // Reset.
108
104
  log('Non-measured items gap detected. Reset first and last measured item indexes.')
109
105
  this.reset()
@@ -115,20 +111,16 @@ export default class ItemHeights {
115
111
  let firstMeasuredItemIndexHasBeenUpdated = false
116
112
  let i = firstShownItemIndex
117
113
  while (i <= lastShownItemIndex) {
114
+ // Measure item heights that haven't been measured previously.
118
115
  // Don't re-measure item heights that have been measured previously.
119
116
  // The rationale is that developers are supposed to manually call
120
117
  // `.onItemHeightChange()` every time an item's height changes.
121
- // If developers aren't neglecting that rule, item heights won't
118
+ // If developers don't neglect that rule, item heights won't
122
119
  // 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
120
  if (this._get(i) === undefined) {
128
121
  nonPreviouslyMeasuredItemIndexes.push(i)
129
- log('Item', i, 'hasn\'t been previously measured')
130
122
  const height = this._measureItemHeight(i, firstShownItemIndex)
131
- log('Height', height)
123
+ log('Item index', i, 'height', height)
132
124
  this._set(i, height)
133
125
  // Update average item height calculation variables
134
126
  // related to the previously measured items
@@ -164,14 +156,17 @@ export default class ItemHeights {
164
156
  this.lastMeasuredItemIndex = i
165
157
  }
166
158
  } 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.
159
+ // Validate that the item's height didn't change since it was last measured.
160
+ // If it did, then display a warning and update the item's height
161
+ // as an attempt to fix things.
162
+ // If an item's height changes unexpectedly then it means that there'll
163
+ // likely be "content jumping".
171
164
  const previousHeight = this._get(i)
172
165
  const height = this._measureItemHeight(i, firstShownItemIndex)
173
166
  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.')
167
+ 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.')
168
+ // Update the item's height as an attempt to fix things.
169
+ this._set(i, height)
175
170
  }
176
171
  }
177
172
  i++