virtual-scroller 1.13.1 → 1.15.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 (231) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +825 -578
  3. package/bundle/index-dom-bypass.html +198 -0
  4. package/bundle/index-dom-grid.html +204 -0
  5. package/bundle/index-dom-scrollableContainer.html +215 -0
  6. package/bundle/index-dom-tbody-scrollableContainer.html +81 -0
  7. package/bundle/index-dom-tbody.html +65 -0
  8. package/bundle/index-dom.html +116 -83
  9. package/bundle/{index-bypass.html → index-react-bypass.html} +83 -78
  10. package/bundle/{index-grid.html → index-react-grid.html} +78 -91
  11. package/bundle/{index-scrollableContainer.html → index-react-scrollableContainer.html} +96 -91
  12. package/bundle/index-react-strictMode.html +199 -0
  13. package/bundle/index-react-tbody-scrollableContainer.html +96 -0
  14. package/bundle/index-react-tbody.html +80 -0
  15. package/bundle/{messages.js → items.js} +1 -1
  16. package/bundle/lib/babel.min.js +25 -0
  17. package/bundle/lib/prop-types.min.js +1 -0
  18. package/bundle/lib/react-dom.development.js +29924 -0
  19. package/bundle/lib/react-dom.production.min.js +267 -0
  20. package/bundle/lib/react.development.js +3343 -0
  21. package/bundle/lib/react.production.min.js +31 -0
  22. package/bundle/style.base.css +33 -0
  23. package/{website/style.css → bundle/style.list.css} +10 -43
  24. package/bundle/style.list.grid.css +23 -0
  25. package/bundle/virtual-scroller-dom.js +1 -1
  26. package/bundle/virtual-scroller-dom.js.map +1 -1
  27. package/bundle/virtual-scroller-react.js +1 -1
  28. package/bundle/virtual-scroller-react.js.map +1 -1
  29. package/bundle/virtual-scroller.js +1 -1
  30. package/bundle/virtual-scroller.js.map +1 -1
  31. package/commonjs/BeforeResize.js +1 -2
  32. package/commonjs/BeforeResize.js.map +1 -1
  33. package/commonjs/DOM/VirtualScroller.js +67 -44
  34. package/commonjs/DOM/VirtualScroller.js.map +1 -1
  35. package/commonjs/DOM/tbody.js +15 -15
  36. package/commonjs/DOM/tbody.js.map +1 -1
  37. package/commonjs/ItemHeights.js +10 -13
  38. package/commonjs/ItemHeights.js.map +1 -1
  39. package/commonjs/Layout.defaults.js +21 -0
  40. package/commonjs/Layout.defaults.js.map +1 -0
  41. package/commonjs/Layout.js +78 -67
  42. package/commonjs/Layout.js.map +1 -1
  43. package/commonjs/Layout.test.js +8 -4
  44. package/commonjs/Layout.test.js.map +1 -1
  45. package/commonjs/Scroll.js +3 -3
  46. package/commonjs/Scroll.js.map +1 -1
  47. package/commonjs/ScrollableContainerResizeHandler.js +4 -5
  48. package/commonjs/ScrollableContainerResizeHandler.js.map +1 -1
  49. package/commonjs/VirtualScroller.constructor.js +53 -31
  50. package/commonjs/VirtualScroller.constructor.js.map +1 -1
  51. package/commonjs/VirtualScroller.items.js +50 -4
  52. package/commonjs/VirtualScroller.items.js.map +1 -1
  53. package/commonjs/VirtualScroller.js +44 -28
  54. package/commonjs/VirtualScroller.js.map +1 -1
  55. package/commonjs/VirtualScroller.layout.js +42 -31
  56. package/commonjs/VirtualScroller.layout.js.map +1 -1
  57. package/commonjs/VirtualScroller.onContainerResize.js +1 -2
  58. package/commonjs/VirtualScroller.onContainerResize.js.map +1 -1
  59. package/commonjs/VirtualScroller.onRender.js +1 -1
  60. package/commonjs/VirtualScroller.onRender.js.map +1 -1
  61. package/commonjs/VirtualScroller.state.js +18 -9
  62. package/commonjs/VirtualScroller.state.js.map +1 -1
  63. package/commonjs/VirtualScroller.verticalSpacing.js +39 -6
  64. package/commonjs/VirtualScroller.verticalSpacing.js.map +1 -1
  65. package/commonjs/react/VirtualScroller.js +98 -37
  66. package/commonjs/react/VirtualScroller.js.map +1 -1
  67. package/commonjs/react/useClassName.js +2 -2
  68. package/commonjs/react/useClassName.js.map +1 -1
  69. package/commonjs/react/useForwardedRef.js +50 -0
  70. package/commonjs/react/useForwardedRef.js.map +1 -0
  71. package/commonjs/react/useInstanceMethods.js +4 -4
  72. package/commonjs/react/useInstanceMethods.js.map +1 -1
  73. package/commonjs/react/useItemKeys.js +28 -5
  74. package/commonjs/react/useItemKeys.js.map +1 -1
  75. package/commonjs/react/useOnItemHeightDidChange.js +28 -12
  76. package/commonjs/react/useOnItemHeightDidChange.js.map +1 -1
  77. package/commonjs/react/useSetItemState.js +31 -12
  78. package/commonjs/react/useSetItemState.js.map +1 -1
  79. package/commonjs/react/useState.js +10 -11
  80. package/commonjs/react/useState.js.map +1 -1
  81. package/commonjs/react/{useStateNoStaleBug.js → useStateWithRepeatableRead.js} +3 -3
  82. package/commonjs/react/useStateWithRepeatableRead.js.map +1 -0
  83. package/commonjs/react/useStyle.js +10 -4
  84. package/commonjs/react/useStyle.js.map +1 -1
  85. package/commonjs/react/useValidateTableBodyItemsContainer.js +24 -0
  86. package/commonjs/react/useValidateTableBodyItemsContainer.js.map +1 -0
  87. package/commonjs/react/useVirtualScroller.js +12 -14
  88. package/commonjs/react/useVirtualScroller.js.map +1 -1
  89. package/commonjs/test/ItemsContainer.js +10 -10
  90. package/commonjs/test/ItemsContainer.js.map +1 -1
  91. package/commonjs/test/VirtualScroller.js +25 -10
  92. package/commonjs/test/VirtualScroller.js.map +1 -1
  93. package/dom/index.d.ts +11 -9
  94. package/index.d.ts +19 -9
  95. package/modules/BeforeResize.js +1 -2
  96. package/modules/BeforeResize.js.map +1 -1
  97. package/modules/DOM/VirtualScroller.js +67 -44
  98. package/modules/DOM/VirtualScroller.js.map +1 -1
  99. package/modules/DOM/tbody.js +14 -13
  100. package/modules/DOM/tbody.js.map +1 -1
  101. package/modules/ItemHeights.js +11 -14
  102. package/modules/ItemHeights.js.map +1 -1
  103. package/modules/Layout.defaults.js +11 -0
  104. package/modules/Layout.defaults.js.map +1 -0
  105. package/modules/Layout.js +77 -67
  106. package/modules/Layout.js.map +1 -1
  107. package/modules/Layout.test.js +8 -4
  108. package/modules/Layout.test.js.map +1 -1
  109. package/modules/Scroll.js +3 -3
  110. package/modules/Scroll.js.map +1 -1
  111. package/modules/ScrollableContainerResizeHandler.js +4 -5
  112. package/modules/ScrollableContainerResizeHandler.js.map +1 -1
  113. package/modules/VirtualScroller.constructor.js +53 -31
  114. package/modules/VirtualScroller.constructor.js.map +1 -1
  115. package/modules/VirtualScroller.items.js +51 -5
  116. package/modules/VirtualScroller.items.js.map +1 -1
  117. package/modules/VirtualScroller.js +44 -28
  118. package/modules/VirtualScroller.js.map +1 -1
  119. package/modules/VirtualScroller.layout.js +42 -31
  120. package/modules/VirtualScroller.layout.js.map +1 -1
  121. package/modules/VirtualScroller.onContainerResize.js +1 -2
  122. package/modules/VirtualScroller.onContainerResize.js.map +1 -1
  123. package/modules/VirtualScroller.onRender.js +1 -1
  124. package/modules/VirtualScroller.onRender.js.map +1 -1
  125. package/modules/VirtualScroller.state.js +18 -9
  126. package/modules/VirtualScroller.state.js.map +1 -1
  127. package/modules/VirtualScroller.verticalSpacing.js +38 -6
  128. package/modules/VirtualScroller.verticalSpacing.js.map +1 -1
  129. package/modules/react/VirtualScroller.js +97 -38
  130. package/modules/react/VirtualScroller.js.map +1 -1
  131. package/modules/react/useClassName.js +3 -3
  132. package/modules/react/useClassName.js.map +1 -1
  133. package/modules/react/useForwardedRef.js +42 -0
  134. package/modules/react/useForwardedRef.js.map +1 -0
  135. package/modules/react/useInstanceMethods.js +4 -4
  136. package/modules/react/useInstanceMethods.js.map +1 -1
  137. package/modules/react/useItemKeys.js +28 -5
  138. package/modules/react/useItemKeys.js.map +1 -1
  139. package/modules/react/useOnItemHeightDidChange.js +28 -12
  140. package/modules/react/useOnItemHeightDidChange.js.map +1 -1
  141. package/modules/react/useSetItemState.js +31 -12
  142. package/modules/react/useSetItemState.js.map +1 -1
  143. package/modules/react/useState.js +10 -11
  144. package/modules/react/useState.js.map +1 -1
  145. package/modules/react/{useStateNoStaleBug.js → useStateWithRepeatableRead.js} +2 -2
  146. package/modules/react/useStateWithRepeatableRead.js.map +1 -0
  147. package/modules/react/useStyle.js +10 -4
  148. package/modules/react/useStyle.js.map +1 -1
  149. package/modules/react/useValidateTableBodyItemsContainer.js +16 -0
  150. package/modules/react/useValidateTableBodyItemsContainer.js.map +1 -0
  151. package/modules/react/useVirtualScroller.js +10 -12
  152. package/modules/react/useVirtualScroller.js.map +1 -1
  153. package/modules/test/ItemsContainer.js +10 -10
  154. package/modules/test/ItemsContainer.js.map +1 -1
  155. package/modules/test/VirtualScroller.js +25 -10
  156. package/modules/test/VirtualScroller.js.map +1 -1
  157. package/package.json +1 -1
  158. package/react/as.d.ts +42 -0
  159. package/react/index.d.ts +204 -63
  160. package/source/BeforeResize.js +1 -2
  161. package/source/DOM/VirtualScroller.js +65 -40
  162. package/source/DOM/tbody.js +15 -14
  163. package/source/ItemHeights.js +11 -12
  164. package/source/Layout.defaults.js +8 -0
  165. package/source/Layout.js +69 -56
  166. package/source/Layout.test.js +7 -2
  167. package/source/Scroll.js +3 -3
  168. package/source/ScrollableContainerResizeHandler.js +4 -4
  169. package/source/VirtualScroller.constructor.js +40 -31
  170. package/source/VirtualScroller.items.js +47 -2
  171. package/source/VirtualScroller.js +49 -27
  172. package/source/VirtualScroller.layout.js +43 -30
  173. package/source/VirtualScroller.onContainerResize.js +1 -2
  174. package/source/VirtualScroller.onRender.js +1 -1
  175. package/source/VirtualScroller.state.js +18 -11
  176. package/source/VirtualScroller.verticalSpacing.js +32 -6
  177. package/source/react/VirtualScroller.js +111 -36
  178. package/source/react/useClassName.js +3 -3
  179. package/source/react/useForwardedRef.js +39 -0
  180. package/source/react/useInstanceMethods.js +12 -4
  181. package/source/react/useItemKeys.js +22 -4
  182. package/source/react/useOnItemHeightDidChange.js +29 -10
  183. package/source/react/useSetItemState.js +32 -10
  184. package/source/react/useState.js +7 -8
  185. package/source/react/{useStateNoStaleBug.js → useStateWithRepeatableRead.js} +1 -1
  186. package/source/react/useStyle.js +3 -2
  187. package/source/react/useValidateTableBodyItemsContainer.js +16 -0
  188. package/source/react/useVirtualScroller.js +4 -6
  189. package/source/test/ItemsContainer.js +10 -10
  190. package/source/test/VirtualScroller.js +16 -10
  191. package/website/index-dom-bypass.html +198 -0
  192. package/website/index-dom-grid.html +204 -0
  193. package/website/index-dom-scrollableContainer.html +215 -0
  194. package/website/index-dom-tbody-scrollableContainer.html +81 -0
  195. package/website/index-dom-tbody.html +65 -0
  196. package/website/index-dom.html +117 -84
  197. package/website/index-react-bypass.html +200 -0
  198. package/website/{index-grid.html → index-react-grid.html} +79 -92
  199. package/website/index-react-scrollableContainer.html +213 -0
  200. package/website/index-react-strictMode.html +199 -0
  201. package/website/index-react-tbody-scrollableContainer.html +96 -0
  202. package/website/index-react-tbody.html +80 -0
  203. package/website/{index-bypass.html → index-react.html} +84 -70
  204. package/website/index.html +84 -69
  205. package/website/{messages.js → items.js} +1 -1
  206. package/website/lib/babel.min.js +25 -0
  207. package/website/lib/prop-types.min.js +1 -0
  208. package/website/lib/react-dom.development.js +29924 -0
  209. package/website/lib/react-dom.production.min.js +267 -0
  210. package/website/lib/react.development.js +3343 -0
  211. package/website/lib/react.production.min.js +31 -0
  212. package/website/style.base.css +33 -0
  213. package/website/style.list.css +92 -0
  214. package/website/style.list.grid.css +23 -0
  215. package/bundle/index-tbody-scrollableContainer.html +0 -70
  216. package/bundle/index-tbody.html +0 -57
  217. package/bundle/on-scroll-to-dom.js +0 -2
  218. package/bundle/on-scroll-to-dom.js.map +0 -1
  219. package/bundle/on-scroll-to-react.js +0 -2
  220. package/bundle/on-scroll-to-react.js.map +0 -1
  221. package/bundle/on-scroll-to.js +0 -2
  222. package/bundle/on-scroll-to.js.map +0 -1
  223. package/commonjs/react/useStateNoStaleBug.js.map +0 -1
  224. package/modules/react/useStateNoStaleBug.js.map +0 -1
  225. package/website/index-scrollableContainer.html +0 -208
  226. package/website/index-tbody-scrollableContainer.html +0 -70
  227. package/website/index-tbody.html +0 -57
  228. package/website/lib/on-scroll-to-dom.js +0 -2
  229. package/website/lib/on-scroll-to-dom.js.map +0 -1
  230. package/website/lib/on-scroll-to-react.js +0 -2
  231. package/website/lib/on-scroll-to-react.js.map +0 -1
package/source/Layout.js CHANGED
@@ -1,13 +1,14 @@
1
1
  import log, { warn } from './utility/debug.js'
2
2
  import ScrollableContainerNotReadyError from './ScrollableContainerNotReadyError.js'
3
+ import { DEFAULT_VISIBLE_ITEM_ROWS_COUNT } from './Layout.defaults.js'
3
4
 
4
5
  export default class Layout {
5
6
  constructor({
6
- bypass,
7
- getInitialEstimatedItemHeight,
8
- getInitialEstimatedVisibleItemRowsCount,
7
+ isInBypassMode,
8
+ getEstimatedVisibleItemRowsCountForInitialRender,
9
9
  measureItemsBatchSize,
10
10
  getPrerenderMargin,
11
+ getPrerenderMarginRatio,
11
12
  getVerticalSpacing,
12
13
  getVerticalSpacingBeforeResize,
13
14
  getColumnsCount,
@@ -19,11 +20,11 @@ export default class Layout {
19
20
  getMaxVisibleAreaHeight,
20
21
  getPreviouslyCalculatedLayout
21
22
  }) {
22
- this.bypass = bypass
23
- this.getInitialEstimatedItemHeight = getInitialEstimatedItemHeight
24
- this.getInitialEstimatedVisibleItemRowsCount = getInitialEstimatedVisibleItemRowsCount
23
+ this.isInBypassMode = isInBypassMode
24
+ this.getEstimatedVisibleItemRowsCountForInitialRender = getEstimatedVisibleItemRowsCountForInitialRender
25
25
  this.measureItemsBatchSize = measureItemsBatchSize
26
26
  this.getPrerenderMargin = getPrerenderMargin
27
+ this.getPrerenderMarginRatio = getPrerenderMarginRatio
27
28
  this.getVerticalSpacing = getVerticalSpacing
28
29
  this.getVerticalSpacingBeforeResize = getVerticalSpacingBeforeResize
29
30
  this.getColumnsCount = getColumnsCount
@@ -69,6 +70,10 @@ export default class Layout {
69
70
  }) {
70
71
  let firstShownItemIndex
71
72
  let lastShownItemIndex
73
+
74
+ let beforeItemsHeight = 0
75
+ let afterItemsHeight = 0
76
+
72
77
  // If there're no items then `firstShownItemIndex` stays `undefined`.
73
78
  if (itemsCount > 0) {
74
79
  const getLastShownItemIndex = () => {
@@ -78,18 +83,30 @@ export default class Layout {
78
83
  firstShownItemIndex
79
84
  })
80
85
  }
86
+
81
87
  firstShownItemIndex = 0
88
+
82
89
  lastShownItemIndex = beforeStart
83
90
  ? this.getInitialLayoutValueWithFallback(
84
91
  'lastShownItemIndex',
85
92
  getLastShownItemIndex,
86
- 0
93
+ firstShownItemIndex
87
94
  )
88
95
  : getLastShownItemIndex()
96
+
97
+ const averageItemHeight = this.getAverageItemHeight()
98
+ const verticalSpacing = this.getVerticalSpacing()
99
+
100
+ const beforeItemsCount = firstShownItemIndex
101
+ const afterItemsCount = itemsCount - (lastShownItemIndex + 1)
102
+
103
+ beforeItemsHeight = Math.ceil(beforeItemsCount / columnsCount) * (verticalSpacing + averageItemHeight)
104
+ afterItemsHeight = Math.ceil(afterItemsCount / columnsCount) * (verticalSpacing + averageItemHeight)
89
105
  }
106
+
90
107
  return {
91
- beforeItemsHeight: 0,
92
- afterItemsHeight: 0,
108
+ beforeItemsHeight,
109
+ afterItemsHeight,
93
110
  firstShownItemIndex,
94
111
  lastShownItemIndex
95
112
  }
@@ -100,57 +117,50 @@ export default class Layout {
100
117
  columnsCount,
101
118
  firstShownItemIndex
102
119
  }) {
103
- if (this.bypass) {
120
+ if (this.isInBypassMode()) {
104
121
  return itemsCount - 1
105
122
  }
106
- // On server side, at initialization time,
107
- // `scrollableContainer` is `undefined`,
108
- // so default to `1` estimated rows count.
109
- let estimatedRowsCount = 1
110
- if (this.getMaxVisibleAreaHeight()) {
111
- estimatedRowsCount = this.getEstimatedRowsCountForHeight(this.getMaxVisibleAreaHeight() + this.getPrerenderMargin())
112
- } else if (this.getInitialEstimatedVisibleItemRowsCount) {
113
- estimatedRowsCount = this.getInitialEstimatedVisibleItemRowsCount()
114
- if (isNaN(estimatedRowsCount)) {
115
- throw new Error('[virtual-scroller] `getEstimatedVisibleItemRowsCount()` must return a number')
116
- }
117
- }
118
123
  return Math.min(
119
- firstShownItemIndex + (estimatedRowsCount * columnsCount - 1),
124
+ firstShownItemIndex + (this.getInitialRenderedRowsCount() * columnsCount - 1),
120
125
  itemsCount - 1
121
126
  )
122
127
  }
123
128
 
124
- getEstimatedRowsCountForHeight(height) {
125
- const estimatedItemHeight = this.getEstimatedItemHeight()
126
- const verticalSpacing = this.getVerticalSpacing()
127
- if (estimatedItemHeight) {
128
- return Math.ceil((height + verticalSpacing) / (estimatedItemHeight + verticalSpacing))
129
- } else {
130
- // If no items have been rendered yet, and no `estimatedItemHeight` option
131
- // has been passed, then default to `1` estimated rows count in any `height`.
132
- return 1
129
+ getInitialRenderedRowsCount() {
130
+ const estimatedVisibleItemRowsCount = this.getEstimatedVisibleItemRowsCount()
131
+ if (typeof estimatedVisibleItemRowsCount === 'number') {
132
+ return Math.ceil(estimatedVisibleItemRowsCount * (1 + this.getPrerenderMarginRatio()))
133
133
  }
134
+ // `DEFAULT_VISIBLE_ITEM_ROWS_COUNT` will be used in server-side render
135
+ // unless `getEstimatedVisibleItemRowsCount()` parameter is specified.
136
+ return DEFAULT_VISIBLE_ITEM_ROWS_COUNT
134
137
  }
135
138
 
136
- /**
137
- * Returns estimated list item height.
138
- * (depends on which items have been previously rendered and measured).
139
- * @return {number}
140
- */
141
- getEstimatedItemHeight() {
142
- const averageItemHeight = this.getAverageItemHeight()
143
- if (averageItemHeight) {
144
- return averageItemHeight
139
+ getEstimatedVisibleItemRowsCount() {
140
+ const maxVisibleAreaHeight = this.getMaxVisibleAreaHeight()
141
+ if (typeof maxVisibleAreaHeight === 'number') {
142
+ const estimatedRowsCount = this.getEstimatedRowsCountForHeight(maxVisibleAreaHeight)
143
+ if (typeof estimatedRowsCount === 'number') {
144
+ return estimatedRowsCount
145
+ }
145
146
  }
146
- if (this.getInitialEstimatedItemHeight) {
147
- const estimatedItemHeight = this.getInitialEstimatedItemHeight()
148
- if (isNaN(estimatedItemHeight)) {
149
- throw new Error('[virtual-scroller] `getInitialEstimatedItemHeight()` must return a number')
147
+ if (this.getEstimatedVisibleItemRowsCountForInitialRender) {
148
+ const estimatedRowsCount = this.getEstimatedVisibleItemRowsCountForInitialRender()
149
+ if (typeof estimatedRowsCount === 'number') {
150
+ return estimatedRowsCount
150
151
  }
151
- return estimatedItemHeight
152
+ throw new Error('[virtual-scroller] `getEstimatedVisibleItemRowsCount()` must return a number')
153
+ }
154
+ }
155
+
156
+ getEstimatedRowsCountForHeight(height) {
157
+ const averageItemHeight = this.getAverageItemHeight()
158
+ const verticalSpacing = this.getVerticalSpacing()
159
+ // `estimatedItemHeight` will most likely be `0` if it hasn't been specified explicitly.
160
+ // In that case, it can't divide by `0`.
161
+ if (averageItemHeight + verticalSpacing > 0) {
162
+ return Math.ceil((height + verticalSpacing) / (averageItemHeight + verticalSpacing))
152
163
  }
153
- return 0
154
164
  }
155
165
 
156
166
  getLayoutUpdateForItemsDiff({
@@ -328,8 +338,17 @@ export default class Layout {
328
338
 
329
339
  const columnsCount = this.getColumnsCount()
330
340
 
341
+ const getNonMeasuredItemRowsCount = () => {
342
+ const estimatedRowsCount = this.getEstimatedRowsCountForHeight(nonMeasuredAreaHeight)
343
+ if (typeof estimatedRowsCount === 'number') {
344
+ return estimatedRowsCount
345
+ }
346
+ // Render all available item rows as a sensible fallback behavior.
347
+ return Math.ceil(itemsCount / columnsCount)
348
+ }
349
+
331
350
  const itemsCountToRenderForMeasurement = Math.min(
332
- this.getEstimatedRowsCountForHeight(nonMeasuredAreaHeight) * columnsCount,
351
+ getNonMeasuredItemRowsCount() * columnsCount,
333
352
  this.measureItemsBatchSize || Infinity,
334
353
  )
335
354
 
@@ -434,15 +453,12 @@ export default class Layout {
434
453
  // then `shownItemsHeight` would also have to be returned from this function:
435
454
  // the total height of all shown items including vertical spacing between them.
436
455
  //
437
- // If "previously calculated layout" would be used then it would first find
438
- // `firstShownItemIndex` and then find `lastShownItemIndex` as part of two
456
+ // If "previously calculated layout" would be used then it would first calculate
457
+ // `firstShownItemIndex` and then calculate `lastShownItemIndex` as part of two
439
458
  // separate calls of this function, each with or without `backwards` flag,
440
459
  // depending on whether `visibleAreaInsideTheList.top` and `visibleAreaInsideTheList.top`
441
460
  // have shifted up or down.
442
461
 
443
- let firstShownItemIndex
444
- let lastShownItemIndex
445
-
446
462
  // It's not always required to pass `beforeItemsHeight` parameter:
447
463
  // when `fromIndex` is `0`, it's also assumed to be `0`.
448
464
  if (fromIndex === 0) {
@@ -678,8 +694,6 @@ export default class Layout {
678
694
  const verticalSpacing = beforeResize ? this.getVerticalSpacingBeforeResize() : this.getVerticalSpacing()
679
695
 
680
696
  while (i < beforeItemsCount) {
681
- const currentRowFirstItemIndex = i
682
-
683
697
  let rowHeight = 0
684
698
  let columnIndex = 0
685
699
  // Not checking for `itemsCount` overflow here because `i = beforeItemsCount`
@@ -734,7 +748,6 @@ export default class Layout {
734
748
  // Which becomes negligible in my project's use case (a couple thousands items max).
735
749
 
736
750
  const columnsCount = this.getColumnsCount()
737
- const lastShownRowIndex = Math.floor(lastShownItemIndex / columnsCount)
738
751
 
739
752
  let afterItemsHeight = 0
740
753
 
@@ -158,9 +158,11 @@ describe('Layout', function() {
158
158
 
159
159
  let shouldResetGridLayout
160
160
 
161
+ // Don't `throw` `VirtualScroller` errors but rather collect them in an array.
161
162
  const errors = []
162
-
163
- global.VirtualScrollerCatchError = (error) => errors.push(error)
163
+ global.VirtualScrollerCatchError = (error) => {
164
+ errors.push(error)
165
+ }
164
166
 
165
167
  layout.getLayoutUpdateForItemsDiff(
166
168
  {
@@ -185,7 +187,10 @@ describe('Layout', function() {
185
187
  afterItemsHeight: 5 * (ITEM_HEIGHT + VERTICAL_SPACING)
186
188
  })
187
189
 
190
+ // Stop collecting `VirtualScroller` errors in the `errors` array.
191
+ // Use the default behavior of just `throw`-ing such errors.
188
192
  global.VirtualScrollerCatchError = undefined
193
+ // Verify the errors that have been `throw`-n.
189
194
  errors.length.should.equal(2)
190
195
  errors[0].message.should.equal('[virtual-scroller] ~ Prepended items count 5 is not divisible by Columns Count 4 ~')
191
196
  errors[1].message.should.equal('[virtual-scroller] Layout reset required')
package/source/Scroll.js CHANGED
@@ -8,7 +8,7 @@ import log from './utility/debug.js'
8
8
 
9
9
  export default class Scroll {
10
10
  constructor({
11
- bypass,
11
+ isInBypassMode,
12
12
  scrollableContainer,
13
13
  itemsContainer,
14
14
  onScroll,
@@ -23,7 +23,7 @@ export default class Scroll {
23
23
  onScrolledToTop,
24
24
  waitForScrollingToStop
25
25
  }) {
26
- this.bypass = bypass
26
+ this.isInBypassMode = isInBypassMode
27
27
  this.scrollableContainer = scrollableContainer
28
28
  this.itemsContainer = itemsContainer
29
29
  this.onScroll = onScroll
@@ -98,7 +98,7 @@ export default class Scroll {
98
98
  }
99
99
  }
100
100
 
101
- if (this.bypass) {
101
+ if (this.isInBypassMode()) {
102
102
  return
103
103
  }
104
104
 
@@ -1,9 +1,9 @@
1
1
  import debounce from './utility/debounce.js'
2
- import log from './utility/debug.js'
2
+ // import log from './utility/debug.js'
3
3
 
4
4
  export default class ScrollableContainerResizeHandler {
5
5
  constructor({
6
- bypass,
6
+ isInBypassMode,
7
7
  getWidth,
8
8
  getHeight,
9
9
  listenForResize,
@@ -13,7 +13,7 @@ export default class ScrollableContainerResizeHandler {
13
13
  onWidthChange,
14
14
  onNoChange
15
15
  }) {
16
- this.bypass = bypass
16
+ this.isInBypassMode = isInBypassMode
17
17
 
18
18
  this.onHeightChange = onHeightChange
19
19
  this.onWidthChange = onWidthChange
@@ -35,7 +35,7 @@ export default class ScrollableContainerResizeHandler {
35
35
 
36
36
  start() {
37
37
  this.isActive = true
38
- if (this.bypass) {
38
+ if (this.isInBypassMode()) {
39
39
  return
40
40
  }
41
41
  this.width = this.getWidth()
@@ -1,18 +1,14 @@
1
- import {
2
- supportsTbody,
3
- BROWSER_NOT_SUPPORTED_ERROR
4
- } from './DOM/tbody.js'
5
-
6
1
  import DOMEngine from './DOM/Engine.js'
7
2
 
8
3
  import Layout, { LAYOUT_REASON } from './Layout.js'
4
+ import { DEFAULT_ITEM_HEIGHT } from './Layout.defaults.js'
9
5
  import ScrollableContainerResizeHandler from './ScrollableContainerResizeHandler.js'
10
6
  import BeforeResize from './BeforeResize.js'
11
7
  import Scroll from './Scroll.js'
12
8
  import ListHeightMeasurement from './ListHeightMeasurement.js'
13
9
  import ItemHeights from './ItemHeights.js'
14
10
 
15
- import log, { warn, isDebug, reportError } from './utility/debug.js'
11
+ import log, { warn } from './utility/debug.js'
16
12
 
17
13
  import createStateHelpers from './VirtualScroller.state.js'
18
14
  import createVerticalSpacingHelpers from './VirtualScroller.verticalSpacing.js'
@@ -34,6 +30,7 @@ export default function VirtualScrollerConstructor(
34
30
  options = {}
35
31
  ) {
36
32
  const {
33
+ bypass,
37
34
  render,
38
35
  state,
39
36
  getInitialItemState = () => {},
@@ -46,10 +43,10 @@ export default function VirtualScrollerConstructor(
46
43
  measureItemsBatchSize = 50,
47
44
  getColumnsCount,
48
45
  getItemId,
49
- tbody,
50
46
  // `estimatedItemHeight` is deprecated, use `getEstimatedItemHeight()` instead.
51
47
  estimatedItemHeight,
52
48
  getEstimatedVisibleItemRowsCount,
49
+ getEstimatedInterItemVerticalSpacing,
53
50
  onItemInitialRender,
54
51
  // `onItemFirstRender(i)` is deprecated, use `onItemInitialRender(item)` instead.
55
52
  onItemFirstRender,
@@ -59,7 +56,6 @@ export default function VirtualScrollerConstructor(
59
56
  } = options
60
57
 
61
58
  let {
62
- bypass,
63
59
  getEstimatedItemHeight,
64
60
  getScrollableContainer
65
61
  } = options
@@ -104,21 +100,6 @@ export default function VirtualScrollerConstructor(
104
100
  throw new Error('[virtual-scroller] `getState`/`setState` options usage has changed in the new version. See the readme for more details.')
105
101
  }
106
102
 
107
- // Work around `<tbody/>` not being able to have `padding`.
108
- // https://gitlab.com/catamphetamine/virtual-scroller/-/issues/1
109
- if (tbody) {
110
- if (this.engine !== DOMEngine) {
111
- throw new Error('[virtual-scroller] `tbody` option is only supported for DOM rendering engine')
112
- }
113
- log('~ <tbody/> detected ~')
114
- this.tbody = true
115
- if (!supportsTbody()) {
116
- log('~ <tbody/> not supported ~')
117
- reportError(BROWSER_NOT_SUPPORTED_ERROR)
118
- bypass = true
119
- }
120
- }
121
-
122
103
  if (bypass) {
123
104
  log('~ "bypass" mode ~')
124
105
  }
@@ -133,7 +114,7 @@ export default function VirtualScrollerConstructor(
133
114
  // It turned out that unmounting large React component trees
134
115
  // is a very long process, so `VirtualScroller` does seem to
135
116
  // make sense when used in a React application.
136
- this.bypass = bypass
117
+ this._bypass = bypass
137
118
  // this.bypassBatchSize = bypassBatchSize || 10
138
119
 
139
120
  // Using `setTimeout()` in render loop is a workaround
@@ -188,7 +169,7 @@ export default function VirtualScrollerConstructor(
188
169
 
189
170
  createStateHelpers.call(this, { state, getInitialItemState, onStateChange, render, items })
190
171
 
191
- createVerticalSpacingHelpers.call(this)
172
+ createVerticalSpacingHelpers.call(this, { getEstimatedInterItemVerticalSpacing })
192
173
  createColumnsHelpers.call(this, { getColumnsCount })
193
174
 
194
175
  createLayoutHelpers.call(this)
@@ -238,6 +219,13 @@ function createHelpers({
238
219
  this.itemsContainer.clear()
239
220
  }
240
221
 
222
+ this.isItemsContainerElementTableBody = () => {
223
+ return this.engine === DOMEngine &&
224
+ this.getItemsContainerElement().tagName === 'TBODY'
225
+ }
226
+
227
+ this.isInBypassMode = () => this._bypass
228
+
241
229
  this.scrollableContainer = this.engine.createScrollableContainer(
242
230
  getScrollableContainer,
243
231
  this.getItemsContainerElement
@@ -250,12 +238,33 @@ function createHelpers({
250
238
  setItemHeight: (i, height) => this.getState().itemHeights[i] = height
251
239
  })
252
240
 
241
+ this.getAverageItemHeight = () => {
242
+ const averageItemHeight = this.itemHeights.getAverageItemHeight()
243
+ if (typeof averageItemHeight === 'number') {
244
+ return averageItemHeight
245
+ }
246
+ return this.getEstimatedItemHeight()
247
+ }
248
+
249
+ this.getEstimatedItemHeight = () => {
250
+ if (getEstimatedItemHeight) {
251
+ const estimatedItemHeight = getEstimatedItemHeight()
252
+ if (typeof estimatedItemHeight === 'number') {
253
+ return estimatedItemHeight
254
+ }
255
+ throw new Error('[virtual-scroller] `getEstimatedItemHeight()` must return a number')
256
+ }
257
+ // `DEFAULT_ITEM_HEIGHT` will be used in server-side render
258
+ // unless `getEstimatedItemHeight()` parameter is specified.
259
+ return DEFAULT_ITEM_HEIGHT
260
+ }
261
+
253
262
  this.layout = new Layout({
254
- bypass: this.bypass,
255
- getInitialEstimatedItemHeight: getEstimatedItemHeight,
256
- getInitialEstimatedVisibleItemRowsCount: getEstimatedVisibleItemRowsCount,
263
+ isInBypassMode: this.isInBypassMode,
264
+ getEstimatedVisibleItemRowsCountForInitialRender: getEstimatedVisibleItemRowsCount,
257
265
  measureItemsBatchSize,
258
266
  getPrerenderMargin: () => this.getPrerenderMargin(),
267
+ getPrerenderMarginRatio: () => this.getPrerenderMarginRatio(),
259
268
  getVerticalSpacing: () => this.getVerticalSpacing(),
260
269
  getVerticalSpacingBeforeResize: () => this.getVerticalSpacingBeforeResize(),
261
270
  getColumnsCount: () => this.getColumnsCount(),
@@ -263,7 +272,7 @@ function createHelpers({
263
272
  getItemHeight: (i) => this.getState().itemHeights[i],
264
273
  getItemHeightBeforeResize: (i) => this.getState().beforeResize && this.getState().beforeResize.itemHeights[i],
265
274
  getBeforeResizeItemsCount: () => this.getState().beforeResize ? this.getState().beforeResize.itemHeights.length : 0,
266
- getAverageItemHeight: () => this.itemHeights.getAverage(),
275
+ getAverageItemHeight: () => this.getAverageItemHeight(),
267
276
  // `this.scrollableContainer` is gonna be `undefined` during server-side rendering.
268
277
  // https://gitlab.com/catamphetamine/virtual-scroller/-/issues/30
269
278
  getMaxVisibleAreaHeight: () => this.scrollableContainer && this.scrollableContainer.getHeight(),
@@ -278,7 +287,7 @@ function createHelpers({
278
287
  })
279
288
 
280
289
  this.scrollableContainerResizeHandler = new ScrollableContainerResizeHandler({
281
- bypass: this.bypass,
290
+ isInBypassMode: this.isInBypassMode,
282
291
  getWidth: () => this.scrollableContainer.getWidth(),
283
292
  getHeight: () => this.scrollableContainer.getHeight(),
284
293
  listenForResize: (listener) => this.scrollableContainer.onResize(listener),
@@ -308,7 +317,7 @@ function createHelpers({
308
317
  })
309
318
 
310
319
  this.scroll = new Scroll({
311
- bypass: this.bypass,
320
+ isInBypassMode: this.isInBypassMode,
312
321
  scrollableContainer: this.scrollableContainer,
313
322
  itemsContainer: this.itemsContainer,
314
323
  waitForScrollingToStop,
@@ -1,4 +1,4 @@
1
- import log, { isDebug } from './utility/debug.js'
1
+ import log, { reportError } from './utility/debug.js'
2
2
  import getItemsDiff from './getItemsDiff.js'
3
3
  import fillArray from './utility/fillArray.js'
4
4
 
@@ -7,6 +7,49 @@ export default function() {
7
7
  return this.getState().items.length
8
8
  }
9
9
 
10
+ this._getItemIndexByItemOrIndex = (itemOrIndex) => {
11
+ const item = itemOrIndex
12
+ const { items } = this.getState()
13
+ // Find the `item`'s index in the `items` array.
14
+ //
15
+ // Performance notes:
16
+ // Doing an `.indexOf()` operation on a very large array could be inefficient
17
+ // because it would have to check every element of the array.
18
+ // In case of any hypothetical performance-related issues,
19
+ // the code could use a `new Map()` as an "index" for finding individual items.
20
+ const i = items.indexOf(item)
21
+ // validate that the `item` exists in the `items` array.
22
+ if (i >= 0) {
23
+ return i
24
+ }
25
+
26
+ // Legacy behavior: some functions used to accept an `i: number` item index as an argument.
27
+ // Later the argument was changed to `item: Item`.
28
+ //
29
+ // The old way of passing an `i: number` argument would only result in weird application behavior
30
+ // when all of the below circumstances are true, which is assumed extremely unlikely:
31
+ //
32
+ // BOTH use a previous version of `virtual-scroller` (ones before December 2025, with downloads count that is not very high)
33
+ // AND to be used with an `items` array of type `number[]`
34
+ // AND to use any of the "advanced" functions:
35
+ // * `setItemState(i, newState)`
36
+ // * `onItemHeightDidChange(i)`
37
+ // * `getItemScrollPosition(i)`
38
+ //
39
+ // The code below handles the legacy compatibility aspect of the old type of argument
40
+ // except maybe for those "extremely unlikely" cases in which the app is still unlikely to crash.
41
+ //
42
+ if (typeof itemOrIndex === 'number') {
43
+ const i = itemOrIndex
44
+ // Validate the item index argument.
45
+ if (i >= 0 && i < items.length) {
46
+ return i
47
+ }
48
+ }
49
+
50
+ reportError(`Item not found: ${JSON.stringify(item)}`)
51
+ }
52
+
10
53
  /**
11
54
  * Updates `items`. For example, can prepend or append new items to the list.
12
55
  * @param {any[]} newItems
@@ -81,7 +124,9 @@ export default function() {
81
124
  if (prependedItemsCount > 0) {
82
125
  log('Prepend', prependedItemsCount, 'items')
83
126
 
84
- itemHeights = new Array(prependedItemsCount).concat(itemHeights)
127
+ itemHeights = new Array(prependedItemsCount)
128
+ .concat(itemHeights)
129
+
85
130
  itemStates = fillArray(
86
131
  new Array(prependedItemsCount),
87
132
  (i) => this.getInitialItemState(newItems[i])
@@ -1,11 +1,17 @@
1
1
  import VirtualScrollerConstructor from './VirtualScroller.constructor.js'
2
- import { hasTbodyStyles, addTbodyStyles } from './DOM/tbody.js'
3
2
  import { LAYOUT_REASON } from './Layout.js'
4
- import log, { warn } from './utility/debug.js'
3
+ import log, { warn, reportError } from './utility/debug.js'
4
+
5
+ import {
6
+ supportsTbody,
7
+ hasTbodyStyles,
8
+ addTbodyStyles,
9
+ BROWSER_NOT_SUPPORTED_ERROR
10
+ } from './DOM/tbody.js'
5
11
 
6
12
  export default class VirtualScroller {
7
13
  /**
8
- * @param {function} getItemsContainerElement — Returns the container DOM `Element`.
14
+ * @param {function} getItemsContainerElement — Returns the items container DOM `Element`.
9
15
  * @param {any[]} items — The list of items.
10
16
  * @param {Object} [options] — See README.md.
11
17
  * @return {VirtualScroller}
@@ -35,13 +41,8 @@ export default class VirtualScroller {
35
41
  const isRestart = this._isActive === false
36
42
 
37
43
  if (!isRestart) {
44
+ this.setUpState()
38
45
  this.waitingForRender = true
39
-
40
- // If no custom state storage has been configured, use the default one.
41
- // Also sets the initial state.
42
- if (!this._usesCustomStateStorage) {
43
- this.useDefaultStateStorage()
44
- }
45
46
  // If `render()` function parameter was passed,
46
47
  // perform an initial render.
47
48
  if (this._render) {
@@ -67,11 +68,23 @@ export default class VirtualScroller {
67
68
  // Reset `_isSettingNewItems` flag just in case it has some "leftover" value.
68
69
  this._isSettingNewItems = undefined
69
70
 
70
- // Work around `<tbody/>` not being able to have `padding`.
71
+ // When `<tbody/>` is used as an items container element,
72
+ // `virtual-scroller` has to work around the HTML bug of
73
+ // `padding` not working on a `<tbody/>` element.
71
74
  // https://gitlab.com/catamphetamine/virtual-scroller/-/issues/1
72
- if (this.tbody) {
73
- if (!hasTbodyStyles(this.getItemsContainerElement())) {
74
- addTbodyStyles(this.getItemsContainerElement())
75
+ if (!this.isInBypassMode()) {
76
+ if (this.isItemsContainerElementTableBody()) {
77
+ if (supportsTbody()) {
78
+ if (!hasTbodyStyles(this.getItemsContainerElement())) {
79
+ log('~ <tbody/> container ~')
80
+ addTbodyStyles(this.getItemsContainerElement())
81
+ }
82
+ } else {
83
+ log('~ <tbody/> container not supported ~')
84
+ reportError(BROWSER_NOT_SUPPORTED_ERROR)
85
+ log('~ enter "bypass" mode ~')
86
+ this._bypass = true
87
+ }
75
88
  }
76
89
  }
77
90
 
@@ -193,10 +206,19 @@ export default class VirtualScroller {
193
206
 
194
207
  /**
195
208
  * Returns the items's top offset relative to the scrollable container's top edge.
196
- * @param {number} i — Item index
209
+ * @param {any} item — Item
197
210
  * @return {[number]} Returns the item's scroll Y position. Returns `undefined` if any of the previous items haven't been rendered yet.
198
211
  */
199
- getItemScrollPosition(i) {
212
+ getItemScrollPosition(itemOrIndex) {
213
+ // Item index.
214
+ const i = this._getItemIndexByItemOrIndex(itemOrIndex)
215
+
216
+ // If the item wasn't found, the error was already reported,
217
+ // so just return some "sensible" default value.
218
+ if (i === undefined) {
219
+ return
220
+ }
221
+
200
222
  const itemTopOffsetInList = this.layout.getItemTopOffset(i)
201
223
  if (itemTopOffsetInList === undefined) {
202
224
  return
@@ -208,28 +230,28 @@ export default class VirtualScroller {
208
230
  * @deprecated
209
231
  * `.onItemHeightChange()` has been renamed to `.onItemHeightDidChange()`.
210
232
  */
211
- onItemHeightChange(i) {
212
- warn('`.onItemHeightChange(i)` method was renamed to `.onItemHeightDidChange(i)`')
213
- this.onItemHeightDidChange(i)
233
+ onItemHeightChange(item) {
234
+ warn('`.onItemHeightChange(item)` method was renamed to `.onItemHeightDidChange(item)`')
235
+ this.onItemHeightDidChange(item)
214
236
  }
215
237
 
216
238
  /**
217
239
  * Forces a re-measure of an item's height.
218
- * @param {number} i — Item index
240
+ * @param {any} item — Item. Legacy argument variant: Item index.
219
241
  */
220
- onItemHeightDidChange(i) {
242
+ onItemHeightDidChange(itemOrIndex) {
221
243
  // See the comments in the `setItemState()` function below for the rationale
222
244
  // on why the `hasToBeStarted()` check was commented out.
223
245
  // this.hasToBeStarted()
224
- this._onItemHeightDidChange(i)
246
+ this._onItemHeightDidChange(itemOrIndex)
225
247
  }
226
248
 
227
249
  /**
228
250
  * Updates an item's state in `state.itemStates[]`.
229
- * @param {number} i — Item index
230
- * @param {any} i — Item's new state
251
+ * @param {any} item — Item. Legacy argument variant: Item index.
252
+ * @param {any} newItemState — Item's new state
231
253
  */
232
- setItemState(i, newItemState) {
254
+ setItemState(itemOrIndex, newItemState) {
233
255
  // There is an issue in React 18.2.0 when `useInsertionEffect()` doesn't run twice
234
256
  // on mount unlike `useLayoutEffect()` in "strict" mode. That causes a bug in a React
235
257
  // implementation of the `virtual-scroller`.
@@ -272,13 +294,13 @@ export default class VirtualScroller {
272
294
  // Commenting it out wouldn't result in any potential bugs because the code would work correctly
273
295
  // in both cases.
274
296
  // this.hasToBeStarted()
275
- this._setItemState(i, newItemState)
297
+ this._setItemState(itemOrIndex, newItemState)
276
298
  }
277
299
 
278
300
  // (deprecated)
279
301
  // Use `.setItemState()` method name instead.
280
- onItemStateChange(i, newItemState) {
281
- this.setItemState(i, newItemState)
302
+ onItemStateChange(item, newItemState) {
303
+ this.setItemState(item, newItemState)
282
304
  }
283
305
 
284
306
  /**