virtual-scroller 1.7.6 → 1.8.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 (153) hide show
  1. package/CHANGELOG.md +29 -1
  2. package/README.md +139 -33
  3. package/babel.config.js +25 -0
  4. package/babel.js +5 -0
  5. package/bundle/index-bypass.html +1 -1
  6. package/bundle/index-dom.html +1 -1
  7. package/bundle/index-grid.html +1 -2
  8. package/bundle/index-scrollableContainer.html +1 -1
  9. package/bundle/index-tbody-scrollableContainer.html +2 -0
  10. package/bundle/index-tbody.html +2 -0
  11. package/bundle/virtual-scroller-dom.js +1 -1
  12. package/bundle/virtual-scroller-dom.js.map +1 -1
  13. package/bundle/virtual-scroller-react.js +1 -1
  14. package/bundle/virtual-scroller-react.js.map +1 -1
  15. package/bundle/virtual-scroller.js +1 -1
  16. package/bundle/virtual-scroller.js.map +1 -1
  17. package/commonjs/BeforeResize.js +319 -0
  18. package/commonjs/BeforeResize.js.map +1 -0
  19. package/commonjs/DOM/Engine.js +46 -0
  20. package/commonjs/DOM/Engine.js.map +1 -0
  21. package/commonjs/DOM/ItemsContainer.js +78 -0
  22. package/commonjs/DOM/ItemsContainer.js.map +1 -0
  23. package/commonjs/DOM/{WaitForStylesToLoad.js → ListTopOffsetWatcher.js} +56 -35
  24. package/commonjs/DOM/ListTopOffsetWatcher.js.map +1 -0
  25. package/commonjs/DOM/ScrollableContainer.js +56 -81
  26. package/commonjs/DOM/ScrollableContainer.js.map +1 -1
  27. package/commonjs/DOM/VirtualScroller.js +20 -15
  28. package/commonjs/DOM/VirtualScroller.js.map +1 -1
  29. package/commonjs/DOM/tbody.js +2 -2
  30. package/commonjs/ItemHeights.js +13 -20
  31. package/commonjs/ItemHeights.js.map +1 -1
  32. package/commonjs/Layout.js +588 -215
  33. package/commonjs/Layout.js.map +1 -1
  34. package/commonjs/Layout.test.js +191 -0
  35. package/commonjs/Layout.test.js.map +1 -0
  36. package/commonjs/ListHeightChangeWatcher.js +126 -0
  37. package/commonjs/ListHeightChangeWatcher.js.map +1 -0
  38. package/commonjs/Resize.js +22 -21
  39. package/commonjs/Resize.js.map +1 -1
  40. package/commonjs/Scroll.js +148 -88
  41. package/commonjs/Scroll.js.map +1 -1
  42. package/commonjs/VirtualScroller.js +1269 -390
  43. package/commonjs/VirtualScroller.js.map +1 -1
  44. package/commonjs/getItemCoordinates.js.map +1 -1
  45. package/commonjs/getItemsDiff.js.map +1 -1
  46. package/commonjs/getVerticalSpacing.js +8 -8
  47. package/commonjs/getVerticalSpacing.js.map +1 -1
  48. package/commonjs/react/VirtualScroller.js +31 -37
  49. package/commonjs/react/VirtualScroller.js.map +1 -1
  50. package/commonjs/utility/debounce.js +26 -4
  51. package/commonjs/utility/debounce.js.map +1 -1
  52. package/commonjs/utility/debug.js +51 -12
  53. package/commonjs/utility/debug.js.map +1 -1
  54. package/commonjs/utility/getStateSnapshot.js +50 -0
  55. package/commonjs/utility/getStateSnapshot.js.map +1 -0
  56. package/commonjs/utility/px.js +1 -1
  57. package/commonjs/utility/px.js.map +1 -1
  58. package/commonjs/utility/px.test.js +14 -0
  59. package/commonjs/utility/px.test.js.map +1 -0
  60. package/commonjs/utility/shallowEqual.js +1 -1
  61. package/commonjs/utility/shallowEqual.js.map +1 -1
  62. package/commonjs/utility/throttle.js.map +1 -1
  63. package/dom/index.d.ts +23 -0
  64. package/index.d.ts +84 -0
  65. package/modules/BeforeResize.js +310 -0
  66. package/modules/BeforeResize.js.map +1 -0
  67. package/modules/DOM/Engine.js +27 -0
  68. package/modules/DOM/Engine.js.map +1 -0
  69. package/modules/DOM/ItemsContainer.js +71 -0
  70. package/modules/DOM/ItemsContainer.js.map +1 -0
  71. package/modules/DOM/{WaitForStylesToLoad.js → ListTopOffsetWatcher.js} +57 -35
  72. package/modules/DOM/ListTopOffsetWatcher.js.map +1 -0
  73. package/modules/DOM/ScrollableContainer.js +55 -80
  74. package/modules/DOM/ScrollableContainer.js.map +1 -1
  75. package/modules/DOM/VirtualScroller.js +15 -14
  76. package/modules/DOM/VirtualScroller.js.map +1 -1
  77. package/modules/ItemHeights.js +8 -19
  78. package/modules/ItemHeights.js.map +1 -1
  79. package/modules/Layout.js +582 -213
  80. package/modules/Layout.js.map +1 -1
  81. package/modules/Layout.test.js +185 -0
  82. package/modules/Layout.test.js.map +1 -0
  83. package/modules/ListHeightChangeWatcher.js +119 -0
  84. package/modules/ListHeightChangeWatcher.js.map +1 -0
  85. package/modules/Resize.js +21 -20
  86. package/modules/Resize.js.map +1 -1
  87. package/modules/Scroll.js +148 -87
  88. package/modules/Scroll.js.map +1 -1
  89. package/modules/VirtualScroller.js +1263 -390
  90. package/modules/VirtualScroller.js.map +1 -1
  91. package/modules/getItemCoordinates.js.map +1 -1
  92. package/modules/getItemsDiff.js.map +1 -1
  93. package/modules/getVerticalSpacing.js +8 -8
  94. package/modules/getVerticalSpacing.js.map +1 -1
  95. package/modules/react/VirtualScroller.js +31 -37
  96. package/modules/react/VirtualScroller.js.map +1 -1
  97. package/modules/utility/debounce.js +26 -4
  98. package/modules/utility/debounce.js.map +1 -1
  99. package/modules/utility/debug.js +47 -10
  100. package/modules/utility/debug.js.map +1 -1
  101. package/modules/utility/getStateSnapshot.js +43 -0
  102. package/modules/utility/getStateSnapshot.js.map +1 -0
  103. package/modules/utility/px.js +1 -1
  104. package/modules/utility/px.js.map +1 -1
  105. package/modules/utility/px.test.js +9 -0
  106. package/modules/utility/px.test.js.map +1 -0
  107. package/modules/utility/shallowEqual.js +1 -1
  108. package/modules/utility/shallowEqual.js.map +1 -1
  109. package/modules/utility/throttle.js.map +1 -1
  110. package/package.json +24 -22
  111. package/react/index.d.ts +27 -0
  112. package/source/BeforeResize.js +317 -0
  113. package/source/DOM/Engine.js +32 -0
  114. package/source/DOM/ItemsContainer.js +48 -0
  115. package/source/DOM/{WaitForStylesToLoad.js → ListTopOffsetWatcher.js} +48 -22
  116. package/source/DOM/ScrollableContainer.js +39 -56
  117. package/source/DOM/VirtualScroller.js +6 -7
  118. package/source/ItemHeights.js +10 -15
  119. package/source/Layout.js +626 -252
  120. package/source/Layout.test.js +171 -0
  121. package/source/ListHeightChangeWatcher.js +94 -0
  122. package/source/Resize.js +23 -15
  123. package/source/Scroll.js +139 -78
  124. package/source/VirtualScroller.js +1240 -286
  125. package/source/getVerticalSpacing.js +7 -7
  126. package/source/react/VirtualScroller.js +2 -18
  127. package/source/utility/debounce.js +20 -3
  128. package/source/utility/debug.js +34 -3
  129. package/source/utility/getStateSnapshot.js +36 -0
  130. package/source/utility/px.js +1 -1
  131. package/source/utility/px.test.js +9 -0
  132. package/website/index-bypass.html +195 -0
  133. package/website/index-grid.html +0 -1
  134. package/website/index-scrollableContainer.html +208 -0
  135. package/website/index-tbody-scrollableContainer.html +68 -0
  136. package/website/index-tbody.html +55 -0
  137. package/commonjs/DOM/RenderingEngine.js +0 -33
  138. package/commonjs/DOM/RenderingEngine.js.map +0 -1
  139. package/commonjs/DOM/Screen.js +0 -87
  140. package/commonjs/DOM/Screen.js.map +0 -1
  141. package/commonjs/DOM/WaitForStylesToLoad.js.map +0 -1
  142. package/commonjs/RestoreScroll.js +0 -118
  143. package/commonjs/RestoreScroll.js.map +0 -1
  144. package/modules/DOM/RenderingEngine.js +0 -19
  145. package/modules/DOM/RenderingEngine.js.map +0 -1
  146. package/modules/DOM/Screen.js +0 -80
  147. package/modules/DOM/Screen.js.map +0 -1
  148. package/modules/DOM/WaitForStylesToLoad.js.map +0 -1
  149. package/modules/RestoreScroll.js +0 -111
  150. package/modules/RestoreScroll.js.map +0 -1
  151. package/source/DOM/RenderingEngine.js +0 -22
  152. package/source/DOM/Screen.js +0 -51
  153. package/source/RestoreScroll.js +0 -86
@@ -0,0 +1,171 @@
1
+ import Layout from './Layout'
2
+
3
+ import Engine from '../test/Engine'
4
+
5
+ describe('Layout', function() {
6
+ it('should work', function() {
7
+ const SCREEN_HEIGHT = 400
8
+
9
+ const scrollableContainer = {
10
+ width: 800,
11
+ height: SCREEN_HEIGHT
12
+ }
13
+
14
+ const ITEM_WIDTH = scrollableContainer.width
15
+ const ITEM_HEIGHT = 200
16
+
17
+ const VERTICAL_SPACING = 100
18
+
19
+ const items = new Array(9).fill(ITEM_WIDTH * ITEM_HEIGHT)
20
+
21
+ const layout = new Layout({
22
+ getPrerenderMargin: () => SCREEN_HEIGHT,
23
+ getVerticalSpacing: () => VERTICAL_SPACING,
24
+ getColumnsCount: () => 1,
25
+ getItemHeight: (i) => items[i] / scrollableContainer.width,
26
+ getBeforeResizeItemsCount: () => 0,
27
+ getAverageItemHeight: () => ITEM_HEIGHT,
28
+ getScrollableContainerHeight: () => scrollableContainer.height
29
+ })
30
+
31
+ // Initial render.
32
+ layout.getShownItemIndexes({
33
+ itemsCount: items.length,
34
+ visibleAreaTop: 0,
35
+ visibleAreaBottom: SCREEN_HEIGHT
36
+ }).should.deep.equal({
37
+ firstShownItemIndex: 0,
38
+ lastShownItemIndex: 2
39
+ })
40
+
41
+ // The first item is almost hidden.
42
+ layout.getShownItemIndexes({
43
+ itemsCount: items.length,
44
+ visibleAreaTop: SCREEN_HEIGHT + ITEM_HEIGHT - 1,
45
+ visibleAreaBottom: (SCREEN_HEIGHT + ITEM_HEIGHT - 1) + SCREEN_HEIGHT
46
+ }).should.deep.equal({
47
+ firstShownItemIndex: 0,
48
+ lastShownItemIndex: 4
49
+ })
50
+
51
+ // The first item is hidden.
52
+ layout.getShownItemIndexes({
53
+ itemsCount: items.length,
54
+ visibleAreaTop: SCREEN_HEIGHT + ITEM_HEIGHT,
55
+ visibleAreaBottom: (SCREEN_HEIGHT + ITEM_HEIGHT) + SCREEN_HEIGHT
56
+ }).should.deep.equal({
57
+ firstShownItemIndex: 1,
58
+ lastShownItemIndex: 4
59
+ })
60
+
61
+ // A new item at the bottom is almost visible.
62
+ layout.getShownItemIndexes({
63
+ itemsCount: items.length,
64
+ visibleAreaTop: (ITEM_HEIGHT + VERTICAL_SPACING) * 5 - SCREEN_HEIGHT * 2,
65
+ visibleAreaBottom: (ITEM_HEIGHT + VERTICAL_SPACING) * 5 - SCREEN_HEIGHT
66
+ }).should.deep.equal({
67
+ firstShownItemIndex: 1,
68
+ lastShownItemIndex: 4
69
+ })
70
+
71
+ // A new item at the bottom is visible.
72
+ layout.getShownItemIndexes({
73
+ itemsCount: items.length,
74
+ visibleAreaTop: (ITEM_HEIGHT + VERTICAL_SPACING) * 5 + 1 - SCREEN_HEIGHT * 2,
75
+ visibleAreaBottom: (ITEM_HEIGHT + VERTICAL_SPACING) * 5 + 1 - SCREEN_HEIGHT
76
+ }).should.deep.equal({
77
+ firstShownItemIndex: 1,
78
+ lastShownItemIndex: 5
79
+ })
80
+ })
81
+
82
+ it('should update layout for items incremental change', function() {
83
+ const scrollableContainer = {
84
+ width: 800,
85
+ height: 400
86
+ }
87
+
88
+ const ITEM_WIDTH = scrollableContainer.width
89
+ const ITEM_HEIGHT = 200
90
+
91
+ const items = new Array(9).fill(ITEM_WIDTH * ITEM_HEIGHT)
92
+
93
+ const VERTICAL_SPACING = 100
94
+
95
+ const layout = new Layout({
96
+ getPrerenderMargin: () => scrollableContainer.height,
97
+ getVerticalSpacing: () => VERTICAL_SPACING,
98
+ getColumnsCount: () => 1,
99
+ getItemHeight: (i) => ITEM_HEIGHT,
100
+ getBeforeResizeItemsCount: () => 0,
101
+ getAverageItemHeight: () => ITEM_HEIGHT,
102
+ getScrollableContainerHeight: () => scrollableContainer.height
103
+ })
104
+
105
+ layout.getLayoutUpdateForItemsDiff(
106
+ {
107
+ firstShownItemIndex: 3,
108
+ lastShownItemIndex: 5,
109
+ beforeItemsHeight: 3 * (ITEM_HEIGHT + VERTICAL_SPACING),
110
+ afterItemsHeight: 3 * (ITEM_HEIGHT + VERTICAL_SPACING)
111
+ },
112
+ {
113
+ prependedItemsCount: 5,
114
+ appendedItemsCount: 5
115
+ }, {
116
+ itemsCount: 5 + 5 + items.length,
117
+ columnsCount: 1
118
+ }
119
+ ).should.deep.equal({
120
+ firstShownItemIndex: 5 + 3,
121
+ lastShownItemIndex: 5 + 5,
122
+ beforeItemsHeight: (5 + 3) * (ITEM_HEIGHT + VERTICAL_SPACING),
123
+ afterItemsHeight: (3 + 5) * (ITEM_HEIGHT + VERTICAL_SPACING)
124
+ })
125
+ })
126
+
127
+ it('should update layout for items incremental change (rows get rebalanced)', function() {
128
+ const scrollableContainer = {
129
+ width: 800,
130
+ height: 400
131
+ }
132
+ const ITEM_WIDTH = scrollableContainer.width
133
+ const ITEM_HEIGHT = 400
134
+
135
+ const items = new Array(9).fill(ITEM_WIDTH * ITEM_HEIGHT)
136
+
137
+ const VERTICAL_SPACING = 100
138
+
139
+ const layout = new Layout({
140
+ getPrerenderMargin: () => scrollableContainer.height,
141
+ getVerticalSpacing: () => VERTICAL_SPACING,
142
+ getColumnsCount: () => 4,
143
+ getItemHeight: () => ITEM_HEIGHT,
144
+ getBeforeResizeItemsCount: () => 0,
145
+ getAverageItemHeight: () => ITEM_HEIGHT,
146
+ getScrollableContainerHeight: () => scrollableContainer.height
147
+ })
148
+
149
+ layout.getLayoutUpdateForItemsDiff(
150
+ {
151
+ firstShownItemIndex: 3,
152
+ lastShownItemIndex: 5,
153
+ beforeItemsHeight: 3 * (ITEM_HEIGHT + VERTICAL_SPACING),
154
+ afterItemsHeight: 3 * (ITEM_HEIGHT + VERTICAL_SPACING)
155
+ },
156
+ {
157
+ prependedItemsCount: 5,
158
+ appendedItemsCount: 5
159
+ }, {
160
+ itemsCount: 5 + 5 + items.length,
161
+ columnsCount: 4,
162
+ shouldRestoreScrollPosition: true
163
+ }
164
+ ).should.deep.equal({
165
+ firstShownItemIndex: 0,
166
+ lastShownItemIndex: 5 + 5,
167
+ beforeItemsHeight: 0,
168
+ afterItemsHeight: 5 * (ITEM_HEIGHT + VERTICAL_SPACING)
169
+ })
170
+ })
171
+ })
@@ -0,0 +1,94 @@
1
+ export default class ListHeightChangeWatcher {
2
+ constructor({
3
+ itemsContainer,
4
+ getListTopOffset
5
+ }) {
6
+ this.itemsContainer = itemsContainer
7
+ this.getListTopOffset = getListTopOffset
8
+ }
9
+
10
+ /**
11
+ * `<ReactVirtualScroller/>` calls this method.
12
+ * @param {any[]} previousItems
13
+ * @param {any[]} newItems
14
+ * @param {number} prependedItemsCount
15
+ */
16
+ snapshot({
17
+ previousItems,
18
+ newItems,
19
+ prependedItemsCount
20
+ }) {
21
+ // If there were no items in the list
22
+ // then there's no point in restoring scroll position.
23
+ if (previousItems.length === 0) {
24
+ return
25
+ }
26
+ // If no items were prepended then no need to restore scroll position.
27
+ if (prependedItemsCount === 0) {
28
+ return
29
+ }
30
+ // The first item is supposed to be shown when the user clicks
31
+ // "Show previous items" button. If it isn't shown though,
32
+ // could still calculate the first item's top position using
33
+ // the values from `itemHeights` and `verticalSpacing`.
34
+ // But that would be a weird non-realistic scenario.
35
+ // if (firstShownItemIndex > 0) {
36
+ // let i = firstShownItemIndex - 1
37
+ // while (i >= 0) {
38
+ // firstItemTopOffset += itemHeights[i] + verticalSpacing
39
+ // i--
40
+ // }
41
+ // }
42
+ // If the scroll position has already been captured for restoration,
43
+ // then don't capture it the second time.
44
+ // Capturing scroll position could happen when using `<ReactVirtualScroller/>`
45
+ // because it calls `ListHeightChangeWatcher.snapshot()` inside `ReactVirtualScroller.render()`
46
+ // which is followed by `<VirtualScroller/>`'s `.componentDidUpdate()`
47
+ // that also calls `ListHeightChangeWatcher.snapshot()` with the same arguments,
48
+ // so that second call to `ListHeightChangeWatcher.snapshot()` is ignored.
49
+ // Calling `ListHeightChangeWatcher.snapshot()` inside `ReactVirtualScroller.render()`
50
+ // is done to prevent scroll Y position from jumping
51
+ // when showing the first page of the "Previous items".
52
+ // See the long section of comments in `ReactVirtualScroller.render()`
53
+ // method for more info on why is `ListHeightChangeWatcher.snapshot()` called there.
54
+ if (this._snapshot &&
55
+ this._snapshot.previousItems === previousItems &&
56
+ this._snapshot.newItems === newItems) {
57
+ return
58
+ }
59
+ this._snapshot = {
60
+ previousItems,
61
+ newItems,
62
+ itemIndex: prependedItemsCount,
63
+ itemTopOffset: this.itemsContainer.getNthRenderedItemTopOffset(0),
64
+ // Snapshot list top offset inside the scrollable container too
65
+ // because it's common to hide the "Show previous items" button
66
+ // when the user has browsed to the top of the list, which causes
67
+ // the list's top position to shift upwards due to the button
68
+ // no longer being rendered. Tracking list top offset doesn't
69
+ // fit here that well, but it makes sense in real-world applications.
70
+ listTopOffset: this.getListTopOffset()
71
+ }
72
+ }
73
+
74
+ getAnchorItemIndex() {
75
+ return this._snapshot.itemIndex
76
+ }
77
+
78
+ hasSnapshot() {
79
+ return this._snapshot !== undefined
80
+ }
81
+
82
+ getListBottomOffsetChange() {
83
+ const { itemIndex, itemTopOffset, listTopOffset } = this._snapshot
84
+ // `firstShownItemIndex` is supposed to be `0` at this point,
85
+ // so `renderedElementIndex` would be the same as the `itemIndex`.
86
+ const itemTopOffsetNew = this.itemsContainer.getNthRenderedItemTopOffset(itemIndex)
87
+ const listTopOffsetNew = this.getListTopOffset()
88
+ return (itemTopOffsetNew - itemTopOffset) + (listTopOffsetNew - listTopOffset)
89
+ }
90
+
91
+ reset() {
92
+ this._snapshot = undefined
93
+ }
94
+ }
package/source/Resize.js CHANGED
@@ -1,19 +1,28 @@
1
- import { LAYOUT_REASON } from './Layout'
2
1
  import debounce from './utility/debounce'
2
+ import log from './utility/debug'
3
3
 
4
4
  export default class Resize {
5
5
  constructor({
6
6
  bypass,
7
7
  scrollableContainer,
8
- getContainerElement,
9
- updateLayout,
10
- resetStateAndLayout
8
+ onStart,
9
+ onStop,
10
+ onHeightChange,
11
+ onWidthChange,
12
+ onNoChange
11
13
  }) {
12
14
  this.bypass = bypass
13
15
  this.scrollableContainer = scrollableContainer
14
- this.getContainerElement = getContainerElement
15
- this.updateLayout = updateLayout
16
- this.resetStateAndLayout = resetStateAndLayout
16
+
17
+ this.onHeightChange = onHeightChange
18
+ this.onWidthChange = onWidthChange
19
+ this.onNoChange = onNoChange
20
+
21
+ this.onResize = debounce(
22
+ this._onResize,
23
+ SCROLLABLE_CONTAINER_RESIZE_DEBOUNCE_INTERVAL,
24
+ { onStart, onStop }
25
+ )
17
26
  }
18
27
 
19
28
  listen() {
@@ -23,22 +32,21 @@ export default class Resize {
23
32
  this.isRendered = true
24
33
  this.scrollableContainerWidth = this.scrollableContainer.getWidth()
25
34
  this.scrollableContainerHeight = this.scrollableContainer.getHeight()
26
- this.scrollableContainerUnlistenResize = this.scrollableContainer.onResize(this.onResize, {
27
- container: this.getContainerElement()
28
- })
35
+ this.scrollableContainerUnlistenResize = this.scrollableContainer.onResize(this.onResize)
29
36
  }
30
37
 
31
38
  stop() {
32
39
  this.isRendered = false
33
40
  if (this.scrollableContainerUnlistenResize) {
34
41
  this.scrollableContainerUnlistenResize()
42
+ this.scrollableContainerUnlistenResize = undefined
35
43
  }
36
44
  }
37
45
 
38
46
  /**
39
47
  * On scrollable container resize.
40
48
  */
41
- onResize = debounce(() => {
49
+ _onResize = () => {
42
50
  // If `VirtualScroller` has been unmounted
43
51
  // while `debounce()`'s `setTimeout()` was waiting, then exit.
44
52
  if (!this.isRendered) {
@@ -52,21 +60,21 @@ export default class Resize {
52
60
  if (this.scrollableContainerHeight === prevScrollableContainerHeight) {
53
61
  // The dimensions of the container didn't change,
54
62
  // so there's no need to re-layout anything.
55
- return
63
+ this.onNoChange()
56
64
  } else {
57
65
  // Scrollable container height has changed,
58
66
  // so just recalculate shown item indexes.
59
67
  // No need to perform a re-layout from scratch.
60
- this.updateLayout({ reason: LAYOUT_REASON.RESIZE })
68
+ this.onHeightChange(prevScrollableContainerHeight, this.scrollableContainerHeight)
61
69
  }
62
70
  } else {
63
71
  // Reset item heights, because if scrollable container's width (or height)
64
72
  // has changed, then the list width (or height) most likely also has changed,
65
73
  // and also some CSS `@media()` rules might have been added or removed.
66
74
  // So re-render the list entirely.
67
- this.resetStateAndLayout()
75
+ this.onWidthChange(prevScrollableContainerWidth, this.scrollableContainerWidth)
68
76
  }
69
- }, SCROLLABLE_CONTAINER_RESIZE_DEBOUNCE_INTERVAL)
77
+ }
70
78
  }
71
79
 
72
80
  const SCROLLABLE_CONTAINER_RESIZE_DEBOUNCE_INTERVAL = 250
package/source/Scroll.js CHANGED
@@ -4,39 +4,39 @@
4
4
  // https://github.com/bvaughn/react-virtualized/issues/722
5
5
  import { setTimeout, clearTimeout } from 'request-animation-frame-timeout'
6
6
 
7
- import { LAYOUT_REASON } from './Layout'
8
7
  import log from './utility/debug'
9
8
 
10
9
  export default class Scroll {
11
10
  constructor({
12
11
  bypass,
13
12
  scrollableContainer,
14
- updateLayout,
13
+ itemsContainer,
14
+ onScroll,
15
15
  initialScrollPosition,
16
16
  onScrollPositionChange,
17
17
  isImmediateLayoutScheduled,
18
18
  hasNonRenderedItemsAtTheTop,
19
19
  hasNonRenderedItemsAtTheBottom,
20
- getLatestLayoutVisibleAreaIncludingMargins,
21
- preserveScrollPositionOfTheBottomOfTheListOnMount
20
+ getLatestLayoutVisibleArea,
21
+ getListTopOffset,
22
+ getPrerenderMargin,
23
+ onScrolledToTop,
24
+ waitForScrollingToStop
22
25
  }) {
23
26
  this.bypass = bypass
24
27
  this.scrollableContainer = scrollableContainer
25
- this.updateLayout = updateLayout
28
+ this.itemsContainer = itemsContainer
29
+ this.onScroll = onScroll
26
30
  this.initialScrollPosition = initialScrollPosition
27
31
  this.onScrollPositionChange = onScrollPositionChange
28
32
  this.isImmediateLayoutScheduled = isImmediateLayoutScheduled
29
33
  this.hasNonRenderedItemsAtTheTop = hasNonRenderedItemsAtTheTop
30
34
  this.hasNonRenderedItemsAtTheBottom = hasNonRenderedItemsAtTheBottom
31
- this.getLatestLayoutVisibleAreaIncludingMargins = getLatestLayoutVisibleAreaIncludingMargins
32
-
33
- if (preserveScrollPositionOfTheBottomOfTheListOnMount) {
34
- if (scrollableContainer) {
35
- this.preserveScrollPositionOfTheBottomOfTheListOnMount = {
36
- scrollableContainerContentHeight: scrollableContainer.getContentHeight()
37
- }
38
- }
39
- }
35
+ this.getLatestLayoutVisibleArea = getLatestLayoutVisibleArea
36
+ this.getListTopOffset = getListTopOffset
37
+ this.getPrerenderMargin = getPrerenderMargin
38
+ this.onScrolledToTop = onScrolledToTop
39
+ this.waitForScrollingToStop = waitForScrollingToStop
40
40
  }
41
41
 
42
42
  listen() {
@@ -44,32 +44,36 @@ export default class Scroll {
44
44
  this.scrollToY(this.initialScrollPosition)
45
45
  }
46
46
  if (this.onScrollPositionChange) {
47
- this.updateScrollPosition()
48
- this.removeScrollPositionListener = this.scrollableContainer.addScrollListener(this.updateScrollPosition)
49
- }
50
- if (!this.bypass) {
51
- this.removeScrollListener = this.scrollableContainer.addScrollListener(this.onScroll)
52
- }
53
- if (this.preserveScrollPositionOfTheBottomOfTheListOnMount) {
54
- this.scrollToY(this.getScrollY() + (this.scrollableContainer.getContentHeight() - this.preserveScrollPositionOfTheBottomOfTheListOnMount.scrollableContainerContentHeight))
47
+ this.onScrollPositionChange(this.getScrollY())
55
48
  }
49
+ this.stopListeningToScroll = this.scrollableContainer.onScroll(this.onScrollListener)
56
50
  }
57
51
 
58
52
  stop() {
59
- if (this.removeScrollPositionListener) {
60
- this.removeScrollPositionListener()
53
+ if (this.stopReportingScrollPositionChange) {
54
+ this.stopReportingScrollPositionChange()
55
+ this.stopReportingScrollPositionChange = undefined
56
+ }
57
+ if (this.stopListeningToScroll) {
58
+ this.stopListeningToScroll()
59
+ this.stopListeningToScroll = undefined
60
+ }
61
+ if (this.onStopScrollingListener) {
62
+ this.onStopScrollingListener = undefined
61
63
  }
62
- if (this.removeScrollListener) {
63
- this.removeScrollListener()
64
+ if (this.onScrollOnStopScrolling) {
65
+ this.onScrollOnStopScrolling = undefined
64
66
  }
65
- this.cancelOnUserStopsScrollingTimer()
67
+ this.cancelOnStopScrollingTimer()
66
68
  }
67
69
 
68
70
  scrollToY(scrollY) {
71
+ this.ignoreScrollEvents = true
69
72
  this.scrollableContainer.scrollToY(scrollY)
73
+ this.ignoreScrollEvents = undefined
70
74
  }
71
75
 
72
- scrollByY(scrollByY) {
76
+ scrollByY = (scrollByY) => {
73
77
  this.scrollToY(this.getScrollY() + scrollByY)
74
78
  }
75
79
 
@@ -77,86 +81,143 @@ export default class Scroll {
77
81
  return this.scrollableContainer.getScrollY()
78
82
  }
79
83
 
80
- /**
81
- * Updates the current scroll Y position in state.
82
- */
83
- updateScrollPosition = () => {
84
- this.onScrollPositionChange(this.getScrollY())
85
- }
86
-
87
- cancelOnUserStopsScrollingTimer() {
88
- if (this.onUserStopsScrollingTimer) {
89
- clearTimeout(this.onUserStopsScrollingTimer)
90
- this.onUserStopsScrollingTimer = undefined
84
+ cancelOnStopScrollingTimer() {
85
+ if (this.onStopScrollingTimer) {
86
+ clearTimeout(this.onStopScrollingTimer)
87
+ this.onStopScrollingTimer = undefined
91
88
  }
92
89
  }
93
90
 
94
- onLayout() {
91
+ cancelScheduledLayout() {
95
92
  // Cancel a "re-layout when user stops scrolling" timer.
96
- this.cancelOnUserStopsScrollingTimer()
93
+ this.cancelOnStopScrollingTimer()
97
94
  }
98
95
 
99
- onScroll = () => {
96
+ onScrollListener = () => {
97
+ if (this.onScrollPositionChange) {
98
+ this.onScrollPositionChange(this.getScrollY())
99
+ }
100
+
101
+ // If the user has scrolled up to the top of the items container.
102
+ // (this option isn't currently used)
103
+ if (this.onScrolledToTop) {
104
+ if (this.getScrollY() < this.getListTopOffset()) {
105
+ this.onScrolledToTop()
106
+ }
107
+ }
108
+
109
+ if (this.bypass) {
110
+ return
111
+ }
112
+
113
+ if (this.ignoreScrollEvents) {
114
+ return
115
+ }
116
+
100
117
  // Prefer not performing a re-layout while the user is scrolling (if possible).
101
118
  // If the user doesn't scroll too far and then stops for a moment,
102
119
  // then a mid-scroll re-layout could be delayed until such a brief stop:
103
120
  // presumably, this results in better (smoother) scrolling performance,
104
121
  // delaying the work to when it doesn't introduce any stutter or "jank".
105
122
 
106
- // Reset `this.onUserStopsScrollingTimer` (will be re-created below).
107
- this.cancelOnUserStopsScrollingTimer()
123
+ // Reset `this.onStopScrollingTimer` (will be re-created below).
124
+ this.cancelOnStopScrollingTimer()
108
125
 
109
- // See whether rendering "new" previous/next items is required
110
- // right now, or it can wait until the user stops scrolling.
126
+ // See if the latest "layout" (the currently rendered set of items)
127
+ // is still sufficient in order to show all the items that're
128
+ // currently inside the viewport. If there're some non-rendered items
129
+ // that're visible in the current viewport, then those items
130
+ // should be rendered "immediately" rather than waiting until
131
+ // the user stops scrolling.
111
132
  const forceUpdate =
112
133
  // If the items have been rendered at least once
113
- this.getLatestLayoutVisibleAreaIncludingMargins() && (
134
+ this.getLatestLayoutVisibleArea() && (
114
135
  (
115
- // If the user has scrolled up past the extra "margin"
116
- (this.getScrollY() < this.getLatestLayoutVisibleAreaIncludingMargins().top) &&
117
- // and if there're any previous non-rendered items to render.
136
+ // If the user has scrolled up past the "prerender margin"
137
+ // and there're some non-rendered items at the top,
138
+ // then force a re-layout.
139
+ //
140
+ // (during these calculations we assume that the list's top coordinate
141
+ // hasn't changed since previous layout; even if that's not exactly true,
142
+ // the items will be re-layout when the user stops scrolling anyway)
143
+ //
144
+ (this.getScrollY() < this.getLatestLayoutVisibleArea().top - this.getPrerenderMargin()) &&
118
145
  this.hasNonRenderedItemsAtTheTop()
119
146
  )
120
147
  ||
121
148
  (
122
- // If the user has scrolled down past the extra "margin"
123
- (this.getScrollY() + this.scrollableContainer.getHeight() > this.getLatestLayoutVisibleAreaIncludingMargins().bottom) &&
124
- // and if there're any next non-rendered items to render.
149
+ // If the user has scrolled down past the "prerender margin"
150
+ // and there're any non-rendered items left at the end,
151
+ // then force a re-layout.
152
+ //
153
+ // (during these calculations we assume that the list's top coordinate
154
+ // hasn't changed since previous layout; even if that's not exactly true,
155
+ // the items will be re-layout when the user stops scrolling anyway)
156
+ //
157
+ (this.getScrollY() + this.scrollableContainer.getHeight() > this.getLatestLayoutVisibleArea().bottom + this.getPrerenderMargin()) &&
125
158
  this.hasNonRenderedItemsAtTheBottom()
126
159
  )
127
160
  )
128
161
 
129
162
  if (forceUpdate) {
130
- log('The user has scrolled far enough: force re-layout')
163
+ log('The user has scrolled far enough: perform a re-layout')
131
164
  } else {
132
- log('The user hasn\'t scrolled too much: delay re-layout')
165
+ log('The user is scrolling: perform a re-layout when they stop scrolling')
133
166
  }
134
167
 
135
- if (!forceUpdate) {
136
- // If a re-layout is already scheduled at the next "frame",
137
- // don't schedule a "re-layout when user stops scrolling" timer.
138
- if (this.isImmediateLayoutScheduled()) {
139
- return
140
- }
141
- this.onUserStopsScrollingTimer = setTimeout(
142
- () => {
143
- this.onUserStopsScrollingTimer = undefined
144
- this.updateLayout({ reason: LAYOUT_REASON.STOPPED_SCROLLING })
145
- },
146
- // "scroll" events are usually dispatched every 16 milliseconds
147
- // for 60fps refresh rate, so waiting for 100 milliseconds feels
148
- // reasonable: that would be about 6 frames of inactivity period,
149
- // which could mean that either the user has stopped scrolling
150
- // (for a moment) or the browser is lagging and stuttering
151
- // (skipping frames due to high load).
152
- // If the user continues scrolling then this timeout is constantly
153
- // refreshed (cancelled and then re-created).
154
- WAIT_FOR_USER_TO_STOP_SCROLLING_TIMEOUT
155
- )
168
+ if (forceUpdate || this.waitForScrollingToStop === false) {
169
+ return this.onScroll()
170
+ }
171
+
172
+ // If a re-layout is already scheduled at the next "frame",
173
+ // don't schedule a "re-layout when user stops scrolling" timer.
174
+ if (this.isImmediateLayoutScheduled()) {
156
175
  return
157
176
  }
158
177
 
159
- this.updateLayout({ reason: LAYOUT_REASON.SCROLL })
178
+ this.onScrollOnStopScrolling = true
179
+ this.watchOnStopScrolling()
180
+ }
181
+
182
+ watchOnStopScrolling() {
183
+ this.onStopScrollingTimer = setTimeout(
184
+ () => {
185
+ this.onStopScrollingTimer = undefined
186
+
187
+ if (this.onScrollOnStopScrolling) {
188
+ this.onScrollOnStopScrolling = undefined
189
+ this.onScroll({ delayed: true })
190
+ }
191
+
192
+ if (this.onStopScrollingListener) {
193
+ const onStopScrollingListener = this.onStopScrollingListener
194
+ this.onStopScrollingListener = undefined
195
+ // `onStopScrollingListener()` may hypothetically schedule
196
+ // another `onStopScrolling()` listener, so set
197
+ // `this.onStopScrollingListener` to `undefined` before
198
+ // calling it rather than after.
199
+ log('~ The user has stopped scrolling ~')
200
+ onStopScrollingListener()
201
+ }
202
+ },
203
+ // "scroll" events are usually dispatched every 16 milliseconds
204
+ // for 60fps refresh rate, so waiting for 100 milliseconds feels
205
+ // reasonable: that would be about 6 frames of inactivity period,
206
+ // which could mean that either the user has stopped scrolling
207
+ // (for a moment) or the browser is lagging and stuttering
208
+ // (skipping frames due to high load).
209
+ // If the user continues scrolling then this timeout is constantly
210
+ // refreshed (cancelled and then re-created).
211
+ ON_STOP_SCROLLING_INACTIVE_PERIOD
212
+ )
213
+ }
214
+
215
+ // (this function isn't currently used)
216
+ onStopScrolling(onStopScrollingListener) {
217
+ this.onStopScrollingListener = onStopScrollingListener
218
+ if (!this.onStopScrollingTimer) {
219
+ this.watchOnStopScrolling()
220
+ }
160
221
  }
161
222
 
162
223
  /**
@@ -174,4 +235,4 @@ export default class Scroll {
174
235
  }
175
236
  }
176
237
 
177
- const WAIT_FOR_USER_TO_STOP_SCROLLING_TIMEOUT = 100
238
+ const ON_STOP_SCROLLING_INACTIVE_PERIOD = 100