virtual-scroller 1.14.0 → 1.15.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 (235) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +403 -254
  3. package/bundle/index-dom-bypass.html +197 -0
  4. package/bundle/index-dom-grid.html +203 -0
  5. package/bundle/index-dom-scrollableContainer.html +214 -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 +114 -84
  9. package/bundle/index-react-bypass.html +194 -0
  10. package/bundle/{index-bypass.html → index-react-grid.html} +122 -120
  11. package/bundle/index-react-hook.html +209 -0
  12. package/bundle/index-react-scrollableContainer.html +207 -0
  13. package/bundle/index-react-strictMode.html +193 -0
  14. package/bundle/index-react-tbody-scrollableContainer.html +94 -0
  15. package/bundle/index-react-tbody.html +78 -0
  16. package/bundle/{messages.js → items.js} +1 -1
  17. package/bundle/lib/babel.min.js +25 -0
  18. package/bundle/lib/prop-types.min.js +1 -0
  19. package/bundle/lib/react-dom.development.js +29924 -0
  20. package/bundle/lib/react-dom.production.min.js +267 -0
  21. package/bundle/lib/react.development.js +3343 -0
  22. package/bundle/lib/react.production.min.js +31 -0
  23. package/bundle/style.base.css +33 -0
  24. package/{website/style.css → bundle/style.list.css} +10 -43
  25. package/bundle/style.list.grid.css +23 -0
  26. package/bundle/virtual-scroller-dom.js +1 -1
  27. package/bundle/virtual-scroller-dom.js.map +1 -1
  28. package/bundle/virtual-scroller-react.js +1 -1
  29. package/bundle/virtual-scroller-react.js.map +1 -1
  30. package/bundle/virtual-scroller.js +1 -1
  31. package/bundle/virtual-scroller.js.map +1 -1
  32. package/commonjs/BeforeResize.js +1 -2
  33. package/commonjs/BeforeResize.js.map +1 -1
  34. package/commonjs/DOM/VirtualScroller.js +7 -13
  35. package/commonjs/DOM/VirtualScroller.js.map +1 -1
  36. package/commonjs/DOM/tbody.js +6 -6
  37. package/commonjs/DOM/tbody.js.map +1 -1
  38. package/commonjs/ItemHeights.js +10 -13
  39. package/commonjs/ItemHeights.js.map +1 -1
  40. package/commonjs/Layout.defaults.js +21 -0
  41. package/commonjs/Layout.defaults.js.map +1 -0
  42. package/commonjs/Layout.js +75 -64
  43. package/commonjs/Layout.js.map +1 -1
  44. package/commonjs/Layout.test.js +8 -4
  45. package/commonjs/Layout.test.js.map +1 -1
  46. package/commonjs/VirtualScroller.constructor.js +38 -4
  47. package/commonjs/VirtualScroller.constructor.js.map +1 -1
  48. package/commonjs/VirtualScroller.items.js +50 -4
  49. package/commonjs/VirtualScroller.items.js.map +1 -1
  50. package/commonjs/VirtualScroller.js +23 -14
  51. package/commonjs/VirtualScroller.js.map +1 -1
  52. package/commonjs/VirtualScroller.layout.js +40 -29
  53. package/commonjs/VirtualScroller.layout.js.map +1 -1
  54. package/commonjs/VirtualScroller.onContainerResize.js +1 -2
  55. package/commonjs/VirtualScroller.onContainerResize.js.map +1 -1
  56. package/commonjs/VirtualScroller.state.js +10 -9
  57. package/commonjs/VirtualScroller.state.js.map +1 -1
  58. package/commonjs/VirtualScroller.verticalSpacing.js +39 -6
  59. package/commonjs/VirtualScroller.verticalSpacing.js.map +1 -1
  60. package/commonjs/react/VirtualScroller.js +124 -131
  61. package/commonjs/react/VirtualScroller.js.map +1 -1
  62. package/commonjs/react/useClassName.js +2 -2
  63. package/commonjs/react/useClassName.js.map +1 -1
  64. package/commonjs/react/useCreateVirtualScroller.js +64 -0
  65. package/commonjs/react/useCreateVirtualScroller.js.map +1 -0
  66. package/commonjs/react/useInstanceMethods.js +4 -4
  67. package/commonjs/react/useInstanceMethods.js.map +1 -1
  68. package/commonjs/react/useItemKeys.js +28 -5
  69. package/commonjs/react/useItemKeys.js.map +1 -1
  70. package/commonjs/react/useMergeRefs.js +52 -0
  71. package/commonjs/react/useMergeRefs.js.map +1 -0
  72. package/commonjs/react/useOnItemHeightDidChange.js +28 -12
  73. package/commonjs/react/useOnItemHeightDidChange.js.map +1 -1
  74. package/commonjs/react/useSetItemState.js +31 -12
  75. package/commonjs/react/useSetItemState.js.map +1 -1
  76. package/commonjs/react/{useVirtualScrollerStartStop.js → useStartStopVirtualScroller.js} +1 -1
  77. package/commonjs/react/{useVirtualScrollerStartStop.js.map → useStartStopVirtualScroller.js.map} +1 -1
  78. package/commonjs/react/useState.js +9 -9
  79. package/commonjs/react/useState.js.map +1 -1
  80. package/commonjs/react/{useStateNoStaleBug.js → useStateWithRepeatableRead.js} +3 -3
  81. package/commonjs/react/useStateWithRepeatableRead.js.map +1 -0
  82. package/commonjs/react/useStyle.js +28 -4
  83. package/commonjs/react/useStyle.js.map +1 -1
  84. package/commonjs/react/useValidateTableBodyItemsContainer.js +24 -0
  85. package/commonjs/react/useValidateTableBodyItemsContainer.js.map +1 -0
  86. package/commonjs/react/useVirtualScroller.js +142 -42
  87. package/commonjs/react/useVirtualScroller.js.map +1 -1
  88. package/commonjs/test/ItemsContainer.js +10 -10
  89. package/commonjs/test/ItemsContainer.js.map +1 -1
  90. package/commonjs/test/VirtualScroller.js +25 -10
  91. package/commonjs/test/VirtualScroller.js.map +1 -1
  92. package/dom/index.d.ts +6 -5
  93. package/index.d.ts +19 -8
  94. package/modules/BeforeResize.js +1 -2
  95. package/modules/BeforeResize.js.map +1 -1
  96. package/modules/DOM/VirtualScroller.js +7 -13
  97. package/modules/DOM/VirtualScroller.js.map +1 -1
  98. package/modules/DOM/tbody.js +4 -4
  99. package/modules/DOM/tbody.js.map +1 -1
  100. package/modules/ItemHeights.js +11 -14
  101. package/modules/ItemHeights.js.map +1 -1
  102. package/modules/Layout.defaults.js +11 -0
  103. package/modules/Layout.defaults.js.map +1 -0
  104. package/modules/Layout.js +74 -64
  105. package/modules/Layout.js.map +1 -1
  106. package/modules/Layout.test.js +8 -4
  107. package/modules/Layout.test.js.map +1 -1
  108. package/modules/VirtualScroller.constructor.js +37 -4
  109. package/modules/VirtualScroller.constructor.js.map +1 -1
  110. package/modules/VirtualScroller.items.js +51 -5
  111. package/modules/VirtualScroller.items.js.map +1 -1
  112. package/modules/VirtualScroller.js +23 -14
  113. package/modules/VirtualScroller.js.map +1 -1
  114. package/modules/VirtualScroller.layout.js +40 -29
  115. package/modules/VirtualScroller.layout.js.map +1 -1
  116. package/modules/VirtualScroller.onContainerResize.js +1 -2
  117. package/modules/VirtualScroller.onContainerResize.js.map +1 -1
  118. package/modules/VirtualScroller.state.js +10 -9
  119. package/modules/VirtualScroller.state.js.map +1 -1
  120. package/modules/VirtualScroller.verticalSpacing.js +38 -6
  121. package/modules/VirtualScroller.verticalSpacing.js.map +1 -1
  122. package/modules/react/VirtualScroller.js +122 -124
  123. package/modules/react/VirtualScroller.js.map +1 -1
  124. package/modules/react/useClassName.js +3 -3
  125. package/modules/react/useClassName.js.map +1 -1
  126. package/modules/react/useCreateVirtualScroller.js +53 -0
  127. package/modules/react/useCreateVirtualScroller.js.map +1 -0
  128. package/modules/react/useInstanceMethods.js +4 -4
  129. package/modules/react/useInstanceMethods.js.map +1 -1
  130. package/modules/react/useItemKeys.js +28 -5
  131. package/modules/react/useItemKeys.js.map +1 -1
  132. package/modules/react/useMergeRefs.js +44 -0
  133. package/modules/react/useMergeRefs.js.map +1 -0
  134. package/modules/react/useOnItemHeightDidChange.js +28 -12
  135. package/modules/react/useOnItemHeightDidChange.js.map +1 -1
  136. package/modules/react/useSetItemState.js +31 -12
  137. package/modules/react/useSetItemState.js.map +1 -1
  138. package/modules/react/{useVirtualScrollerStartStop.js → useStartStopVirtualScroller.js} +1 -1
  139. package/modules/react/{useVirtualScrollerStartStop.js.map → useStartStopVirtualScroller.js.map} +1 -1
  140. package/modules/react/useState.js +9 -9
  141. package/modules/react/useState.js.map +1 -1
  142. package/modules/react/{useStateNoStaleBug.js → useStateWithRepeatableRead.js} +2 -2
  143. package/modules/react/useStateWithRepeatableRead.js.map +1 -0
  144. package/modules/react/useStyle.js +27 -4
  145. package/modules/react/useStyle.js.map +1 -1
  146. package/modules/react/useValidateTableBodyItemsContainer.js +16 -0
  147. package/modules/react/useValidateTableBodyItemsContainer.js.map +1 -0
  148. package/modules/react/useVirtualScroller.js +136 -42
  149. package/modules/react/useVirtualScroller.js.map +1 -1
  150. package/modules/test/ItemsContainer.js +10 -10
  151. package/modules/test/ItemsContainer.js.map +1 -1
  152. package/modules/test/VirtualScroller.js +25 -10
  153. package/modules/test/VirtualScroller.js.map +1 -1
  154. package/package.json +4 -1
  155. package/react/as.d.ts +42 -0
  156. package/react/index.cjs +2 -1
  157. package/react/index.d.ts +248 -63
  158. package/react/index.js +1 -0
  159. package/rollup.config.mjs +15 -1
  160. package/source/BeforeResize.js +1 -2
  161. package/source/DOM/VirtualScroller.js +7 -13
  162. package/source/DOM/tbody.js +5 -5
  163. package/source/ItemHeights.js +11 -12
  164. package/source/Layout.defaults.js +8 -0
  165. package/source/Layout.js +66 -53
  166. package/source/Layout.test.js +7 -2
  167. package/source/VirtualScroller.constructor.js +27 -4
  168. package/source/VirtualScroller.items.js +47 -2
  169. package/source/VirtualScroller.js +23 -14
  170. package/source/VirtualScroller.layout.js +41 -28
  171. package/source/VirtualScroller.onContainerResize.js +1 -2
  172. package/source/VirtualScroller.state.js +10 -11
  173. package/source/VirtualScroller.verticalSpacing.js +32 -6
  174. package/source/react/VirtualScroller.js +135 -133
  175. package/source/react/useClassName.js +3 -3
  176. package/source/react/useCreateVirtualScroller.js +65 -0
  177. package/source/react/useInstanceMethods.js +12 -4
  178. package/source/react/useItemKeys.js +22 -4
  179. package/source/react/useMergeRefs.js +45 -0
  180. package/source/react/useOnItemHeightDidChange.js +29 -10
  181. package/source/react/useSetItemState.js +32 -10
  182. package/source/react/useState.js +6 -6
  183. package/source/react/{useStateNoStaleBug.js → useStateWithRepeatableRead.js} +1 -1
  184. package/source/react/useStyle.js +18 -2
  185. package/source/react/useValidateTableBodyItemsContainer.js +16 -0
  186. package/source/react/useVirtualScroller.js +155 -47
  187. package/source/test/ItemsContainer.js +10 -10
  188. package/source/test/VirtualScroller.js +16 -10
  189. package/website/index-dom-bypass.html +197 -0
  190. package/website/index-dom-grid.html +203 -0
  191. package/website/index-dom-scrollableContainer.html +214 -0
  192. package/website/index-dom-tbody-scrollableContainer.html +81 -0
  193. package/website/index-dom-tbody.html +65 -0
  194. package/website/index-dom.html +116 -84
  195. package/website/index-react-bypass.html +194 -0
  196. package/website/index-react-grid.html +197 -0
  197. package/website/index-react-hook.html +209 -0
  198. package/website/index-react-scrollableContainer.html +207 -0
  199. package/website/index-react-strictMode.html +193 -0
  200. package/website/index-react-tbody-scrollableContainer.html +94 -0
  201. package/website/index-react-tbody.html +78 -0
  202. package/website/index-react.html +193 -0
  203. package/website/index.html +120 -111
  204. package/website/{messages.js → items.js} +1 -1
  205. package/website/lib/babel.min.js +25 -0
  206. package/website/lib/prop-types.min.js +1 -0
  207. package/website/lib/react-dom.development.js +29924 -0
  208. package/website/lib/react-dom.production.min.js +267 -0
  209. package/website/lib/react.development.js +3343 -0
  210. package/website/lib/react.production.min.js +31 -0
  211. package/website/style.base.css +33 -0
  212. package/website/style.list.css +92 -0
  213. package/website/style.list.grid.css +23 -0
  214. package/bundle/index-grid.html +0 -216
  215. package/bundle/index-scrollableContainer.html +0 -208
  216. package/bundle/index-tbody-scrollableContainer.html +0 -70
  217. package/bundle/index-tbody.html +0 -57
  218. package/bundle/on-scroll-to-dom.js +0 -2
  219. package/bundle/on-scroll-to-dom.js.map +0 -1
  220. package/bundle/on-scroll-to-react.js +0 -2
  221. package/bundle/on-scroll-to-react.js.map +0 -1
  222. package/bundle/on-scroll-to.js +0 -2
  223. package/bundle/on-scroll-to.js.map +0 -1
  224. package/commonjs/react/useStateNoStaleBug.js.map +0 -1
  225. package/modules/react/useStateNoStaleBug.js.map +0 -1
  226. package/website/index-bypass.html +0 -185
  227. package/website/index-grid.html +0 -216
  228. package/website/index-scrollableContainer.html +0 -208
  229. package/website/index-tbody-scrollableContainer.html +0 -70
  230. package/website/index-tbody.html +0 -57
  231. package/website/lib/on-scroll-to-dom.js +0 -2
  232. package/website/lib/on-scroll-to-dom.js.map +0 -1
  233. package/website/lib/on-scroll-to-react.js +0 -2
  234. package/website/lib/on-scroll-to-react.js.map +0 -1
  235. /package/source/react/{useVirtualScrollerStartStop.js → useStartStopVirtualScroller.js} +0 -0
@@ -1,15 +1,15 @@
1
- import log, { warn, isDebug, reportError } from './utility/debug.js'
1
+ import log, { warn } from './utility/debug.js'
2
2
 
3
3
  export default class ItemHeights {
4
4
  constructor({
5
5
  container,
6
- itemHeights,
7
6
  getItemHeight,
8
7
  setItemHeight
9
8
  }) {
10
9
  this.container = container
11
10
  this._get = getItemHeight
12
11
  this._set = setItemHeight
12
+
13
13
  this.reset()
14
14
  }
15
15
 
@@ -20,9 +20,9 @@ export default class ItemHeights {
20
20
  // is called, some items might get prepended, in which case
21
21
  // `this.lastMeasuredItemIndex` is updated. If there was no
22
22
  // `this.firstMeasuredItemIndex`, then the average item height
23
- // calculated in `.getAverage()` would be incorrect in the timeframe
23
+ // calculated in `.getAverageItemHeight()` would be incorrect in the timeframe
24
24
  // between `.setItems()` is called and those changes have been rendered.
25
- // And in that timeframe, `.getAverage()` is used to calculate the "layout":
25
+ // And in that timeframe, `.getAverageItemHeight()` is used to calculate the "layout":
26
26
  // stuff like "before/after items height" and "estimated items count on screen".
27
27
  this.firstMeasuredItemIndex = undefined
28
28
  this.lastMeasuredItemIndex = undefined
@@ -64,7 +64,7 @@ export default class ItemHeights {
64
64
  // this._set(i, itemHeight)
65
65
  // return itemHeight
66
66
  // }
67
- // return this.getAverage()
67
+ // return this.getAverageItemHeight()
68
68
  // }
69
69
 
70
70
  _measureItemHeight(i, firstShownItemIndex) {
@@ -169,7 +169,7 @@ export default class ItemHeights {
169
169
  const previousHeight = this._get(i)
170
170
  const height = this._measureItemHeight(i, firstShownItemIndex)
171
171
  if (previousHeight !== height) {
172
- warn('Item index', i, 'height changed unexpectedly: it was', previousHeight, 'before, but now it is', height, '. Whenever an item\'s height changes for whatever reason, a developer must call `onItemHeightDidChange(i)` right after that change. If you are calling `onItemHeightDidChange(i)` correctly, then there\'re several other possible causes. For example, perhaps you forgot to persist the item\'s "state" by calling `setItemState(i, newState)` when that "state" did change, and so the item\'s "state" got lost when the item element was unmounted, which resulted in a different item height when the item was shown again with no previous "state". Or perhaps you\'re running your application in "devleopment" mode and `VirtualScroller` has initially rendered the list before your CSS styles or custom fonts have loaded, resulting in different item height measurements "before" and "after" the page has fully loaded.')
172
+ warn('Item index', i, 'height changed unexpectedly: it was', previousHeight, 'before, but now it is', height, '. Whenever an item\'s height changes for whatever reason, a developer must call `onItemHeightDidChange(item)` right after that change. If you are calling `onItemHeightDidChange(item)` correctly, then there\'re several other possible causes. For example, perhaps you forgot to persist the item\'s "state" by calling `setItemState(item, newState)` when that "state" did change, and so the item\'s "state" got lost when the item element was unmounted, which resulted in a different item height when the item was shown again with no previous "state". Or perhaps you\'re running your application in "devleopment" mode and `VirtualScroller` has initially rendered the list before your CSS styles or custom fonts have loaded, resulting in different item height measurements "before" and "after" the page has fully loaded.')
173
173
  // Update the item's height as an attempt to fix things.
174
174
  this._set(i, height)
175
175
  }
@@ -231,7 +231,7 @@ export default class ItemHeights {
231
231
  // * Public API: is called by `VirtualScroller`.
232
232
  // * @return {number}
233
233
  // */
234
- // getAverage() {
234
+ // getAvergetAverageItemHeightage() {
235
235
  // // Previously measured average item height might still be
236
236
  // // more precise if it contains more measured items ("samples").
237
237
  // if (this.previousAverageItemHeight) {
@@ -244,13 +244,12 @@ export default class ItemHeights {
244
244
 
245
245
  /**
246
246
  * Public API: is called by `VirtualScroller`.
247
- * @return {number}
247
+ * @return {number} [averageItemHeight] Returns `undefined` until at least one item has been rendered.
248
248
  */
249
- getAverage() {
250
- if (this.lastMeasuredItemIndex === undefined) {
251
- return 0
249
+ getAverageItemHeight() {
250
+ if (this.lastMeasuredItemIndex !== undefined) {
251
+ return this.measuredItemsHeight / (this.lastMeasuredItemIndex - this.firstMeasuredItemIndex + 1)
252
252
  }
253
- return this.measuredItemsHeight / (this.lastMeasuredItemIndex - this.firstMeasuredItemIndex + 1)
254
253
  }
255
254
 
256
255
  onPrepend(count) {
@@ -0,0 +1,8 @@
1
+ // These values are used by default in server-side render,
2
+ // unless the developer explicitly specifies:
3
+ // * `getEstimatedItemHeight()`
4
+ // * `getEstimatedVisibleItemRowsCount()`
5
+ // * `getEstimatedInterItemVerticalSpacing()`
6
+ export const DEFAULT_VISIBLE_ITEM_ROWS_COUNT = 1 // determines the count of items to render.
7
+ export const DEFAULT_ITEM_HEIGHT = 0 // determines the length of the scrollbar, i.e. how much the user can scroll.
8
+ export const DEFAULT_INTER_ITEM_VERTICAL_SPACING = 0 // determines the length of the scrollbar, i.e. how much the user can scroll.
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
7
  isInBypassMode,
7
- getInitialEstimatedItemHeight,
8
- getInitialEstimatedVisibleItemRowsCount,
8
+ getEstimatedVisibleItemRowsCountForInitialRender,
9
9
  measureItemsBatchSize,
10
10
  getPrerenderMargin,
11
+ getPrerenderMarginRatio,
11
12
  getVerticalSpacing,
12
13
  getVerticalSpacingBeforeResize,
13
14
  getColumnsCount,
@@ -20,10 +21,10 @@ export default class Layout {
20
21
  getPreviouslyCalculatedLayout
21
22
  }) {
22
23
  this.isInBypassMode = isInBypassMode
23
- this.getInitialEstimatedItemHeight = getInitialEstimatedItemHeight
24
- this.getInitialEstimatedVisibleItemRowsCount = getInitialEstimatedVisibleItemRowsCount
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
  }
@@ -103,54 +120,47 @@ export default class Layout {
103
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')
@@ -1,6 +1,7 @@
1
1
  import DOMEngine from './DOM/Engine.js'
2
2
 
3
3
  import Layout, { LAYOUT_REASON } from './Layout.js'
4
+ import { DEFAULT_ITEM_HEIGHT } from './Layout.defaults.js'
4
5
  import ScrollableContainerResizeHandler from './ScrollableContainerResizeHandler.js'
5
6
  import BeforeResize from './BeforeResize.js'
6
7
  import Scroll from './Scroll.js'
@@ -45,6 +46,7 @@ export default function VirtualScrollerConstructor(
45
46
  // `estimatedItemHeight` is deprecated, use `getEstimatedItemHeight()` instead.
46
47
  estimatedItemHeight,
47
48
  getEstimatedVisibleItemRowsCount,
49
+ getEstimatedInterItemVerticalSpacing,
48
50
  onItemInitialRender,
49
51
  // `onItemFirstRender(i)` is deprecated, use `onItemInitialRender(item)` instead.
50
52
  onItemFirstRender,
@@ -167,7 +169,7 @@ export default function VirtualScrollerConstructor(
167
169
 
168
170
  createStateHelpers.call(this, { state, getInitialItemState, onStateChange, render, items })
169
171
 
170
- createVerticalSpacingHelpers.call(this)
172
+ createVerticalSpacingHelpers.call(this, { getEstimatedInterItemVerticalSpacing })
171
173
  createColumnsHelpers.call(this, { getColumnsCount })
172
174
 
173
175
  createLayoutHelpers.call(this)
@@ -236,12 +238,33 @@ function createHelpers({
236
238
  setItemHeight: (i, height) => this.getState().itemHeights[i] = height
237
239
  })
238
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
+
239
262
  this.layout = new Layout({
240
263
  isInBypassMode: this.isInBypassMode,
241
- getInitialEstimatedItemHeight: getEstimatedItemHeight,
242
- getInitialEstimatedVisibleItemRowsCount: getEstimatedVisibleItemRowsCount,
264
+ getEstimatedVisibleItemRowsCountForInitialRender: getEstimatedVisibleItemRowsCount,
243
265
  measureItemsBatchSize,
244
266
  getPrerenderMargin: () => this.getPrerenderMargin(),
267
+ getPrerenderMarginRatio: () => this.getPrerenderMarginRatio(),
245
268
  getVerticalSpacing: () => this.getVerticalSpacing(),
246
269
  getVerticalSpacingBeforeResize: () => this.getVerticalSpacingBeforeResize(),
247
270
  getColumnsCount: () => this.getColumnsCount(),
@@ -249,7 +272,7 @@ function createHelpers({
249
272
  getItemHeight: (i) => this.getState().itemHeights[i],
250
273
  getItemHeightBeforeResize: (i) => this.getState().beforeResize && this.getState().beforeResize.itemHeights[i],
251
274
  getBeforeResizeItemsCount: () => this.getState().beforeResize ? this.getState().beforeResize.itemHeights.length : 0,
252
- getAverageItemHeight: () => this.itemHeights.getAverage(),
275
+ getAverageItemHeight: () => this.getAverageItemHeight(),
253
276
  // `this.scrollableContainer` is gonna be `undefined` during server-side rendering.
254
277
  // https://gitlab.com/catamphetamine/virtual-scroller/-/issues/30
255
278
  getMaxVisibleAreaHeight: () => this.scrollableContainer && this.scrollableContainer.getHeight(),
@@ -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])
@@ -206,10 +206,19 @@ export default class VirtualScroller {
206
206
 
207
207
  /**
208
208
  * Returns the items's top offset relative to the scrollable container's top edge.
209
- * @param {number} i — Item index
209
+ * @param {any} item — Item
210
210
  * @return {[number]} Returns the item's scroll Y position. Returns `undefined` if any of the previous items haven't been rendered yet.
211
211
  */
212
- 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
+
213
222
  const itemTopOffsetInList = this.layout.getItemTopOffset(i)
214
223
  if (itemTopOffsetInList === undefined) {
215
224
  return
@@ -221,28 +230,28 @@ export default class VirtualScroller {
221
230
  * @deprecated
222
231
  * `.onItemHeightChange()` has been renamed to `.onItemHeightDidChange()`.
223
232
  */
224
- onItemHeightChange(i) {
225
- warn('`.onItemHeightChange(i)` method was renamed to `.onItemHeightDidChange(i)`')
226
- this.onItemHeightDidChange(i)
233
+ onItemHeightChange(item) {
234
+ warn('`.onItemHeightChange(item)` method was renamed to `.onItemHeightDidChange(item)`')
235
+ this.onItemHeightDidChange(item)
227
236
  }
228
237
 
229
238
  /**
230
239
  * Forces a re-measure of an item's height.
231
- * @param {number} i — Item index
240
+ * @param {any} item — Item. Legacy argument variant: Item index.
232
241
  */
233
- onItemHeightDidChange(i) {
242
+ onItemHeightDidChange(itemOrIndex) {
234
243
  // See the comments in the `setItemState()` function below for the rationale
235
244
  // on why the `hasToBeStarted()` check was commented out.
236
245
  // this.hasToBeStarted()
237
- this._onItemHeightDidChange(i)
246
+ this._onItemHeightDidChange(itemOrIndex)
238
247
  }
239
248
 
240
249
  /**
241
250
  * Updates an item's state in `state.itemStates[]`.
242
- * @param {number} i — Item index
243
- * @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
244
253
  */
245
- setItemState(i, newItemState) {
254
+ setItemState(itemOrIndex, newItemState) {
246
255
  // There is an issue in React 18.2.0 when `useInsertionEffect()` doesn't run twice
247
256
  // on mount unlike `useLayoutEffect()` in "strict" mode. That causes a bug in a React
248
257
  // implementation of the `virtual-scroller`.
@@ -285,13 +294,13 @@ export default class VirtualScroller {
285
294
  // Commenting it out wouldn't result in any potential bugs because the code would work correctly
286
295
  // in both cases.
287
296
  // this.hasToBeStarted()
288
- this._setItemState(i, newItemState)
297
+ this._setItemState(itemOrIndex, newItemState)
289
298
  }
290
299
 
291
300
  // (deprecated)
292
301
  // Use `.setItemState()` method name instead.
293
- onItemStateChange(i, newItemState) {
294
- this.setItemState(i, newItemState)
302
+ onItemStateChange(item, newItemState) {
303
+ this.setItemState(item, newItemState)
295
304
  }
296
305
 
297
306
  /**