virtual-scroller 1.14.0 → 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 (216) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +309 -250
  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 +115 -84
  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 +7 -13
  34. package/commonjs/DOM/VirtualScroller.js.map +1 -1
  35. package/commonjs/DOM/tbody.js +6 -6
  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 +75 -64
  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/VirtualScroller.constructor.js +38 -4
  46. package/commonjs/VirtualScroller.constructor.js.map +1 -1
  47. package/commonjs/VirtualScroller.items.js +50 -4
  48. package/commonjs/VirtualScroller.items.js.map +1 -1
  49. package/commonjs/VirtualScroller.js +23 -14
  50. package/commonjs/VirtualScroller.js.map +1 -1
  51. package/commonjs/VirtualScroller.layout.js +40 -29
  52. package/commonjs/VirtualScroller.layout.js.map +1 -1
  53. package/commonjs/VirtualScroller.onContainerResize.js +1 -2
  54. package/commonjs/VirtualScroller.onContainerResize.js.map +1 -1
  55. package/commonjs/VirtualScroller.state.js +10 -9
  56. package/commonjs/VirtualScroller.state.js.map +1 -1
  57. package/commonjs/VirtualScroller.verticalSpacing.js +39 -6
  58. package/commonjs/VirtualScroller.verticalSpacing.js.map +1 -1
  59. package/commonjs/react/VirtualScroller.js +85 -34
  60. package/commonjs/react/VirtualScroller.js.map +1 -1
  61. package/commonjs/react/useClassName.js +2 -2
  62. package/commonjs/react/useClassName.js.map +1 -1
  63. package/commonjs/react/useForwardedRef.js +50 -0
  64. package/commonjs/react/useForwardedRef.js.map +1 -0
  65. package/commonjs/react/useInstanceMethods.js +4 -4
  66. package/commonjs/react/useInstanceMethods.js.map +1 -1
  67. package/commonjs/react/useItemKeys.js +28 -5
  68. package/commonjs/react/useItemKeys.js.map +1 -1
  69. package/commonjs/react/useOnItemHeightDidChange.js +28 -12
  70. package/commonjs/react/useOnItemHeightDidChange.js.map +1 -1
  71. package/commonjs/react/useSetItemState.js +31 -12
  72. package/commonjs/react/useSetItemState.js.map +1 -1
  73. package/commonjs/react/useState.js +9 -9
  74. package/commonjs/react/useState.js.map +1 -1
  75. package/commonjs/react/{useStateNoStaleBug.js → useStateWithRepeatableRead.js} +3 -3
  76. package/commonjs/react/useStateWithRepeatableRead.js.map +1 -0
  77. package/commonjs/react/useStyle.js +10 -4
  78. package/commonjs/react/useStyle.js.map +1 -1
  79. package/commonjs/react/useValidateTableBodyItemsContainer.js +24 -0
  80. package/commonjs/react/useValidateTableBodyItemsContainer.js.map +1 -0
  81. package/commonjs/react/useVirtualScroller.js +4 -3
  82. package/commonjs/react/useVirtualScroller.js.map +1 -1
  83. package/commonjs/test/ItemsContainer.js +10 -10
  84. package/commonjs/test/ItemsContainer.js.map +1 -1
  85. package/commonjs/test/VirtualScroller.js +25 -10
  86. package/commonjs/test/VirtualScroller.js.map +1 -1
  87. package/dom/index.d.ts +6 -5
  88. package/index.d.ts +19 -8
  89. package/modules/BeforeResize.js +1 -2
  90. package/modules/BeforeResize.js.map +1 -1
  91. package/modules/DOM/VirtualScroller.js +7 -13
  92. package/modules/DOM/VirtualScroller.js.map +1 -1
  93. package/modules/DOM/tbody.js +4 -4
  94. package/modules/DOM/tbody.js.map +1 -1
  95. package/modules/ItemHeights.js +11 -14
  96. package/modules/ItemHeights.js.map +1 -1
  97. package/modules/Layout.defaults.js +11 -0
  98. package/modules/Layout.defaults.js.map +1 -0
  99. package/modules/Layout.js +74 -64
  100. package/modules/Layout.js.map +1 -1
  101. package/modules/Layout.test.js +8 -4
  102. package/modules/Layout.test.js.map +1 -1
  103. package/modules/VirtualScroller.constructor.js +37 -4
  104. package/modules/VirtualScroller.constructor.js.map +1 -1
  105. package/modules/VirtualScroller.items.js +51 -5
  106. package/modules/VirtualScroller.items.js.map +1 -1
  107. package/modules/VirtualScroller.js +23 -14
  108. package/modules/VirtualScroller.js.map +1 -1
  109. package/modules/VirtualScroller.layout.js +40 -29
  110. package/modules/VirtualScroller.layout.js.map +1 -1
  111. package/modules/VirtualScroller.onContainerResize.js +1 -2
  112. package/modules/VirtualScroller.onContainerResize.js.map +1 -1
  113. package/modules/VirtualScroller.state.js +10 -9
  114. package/modules/VirtualScroller.state.js.map +1 -1
  115. package/modules/VirtualScroller.verticalSpacing.js +38 -6
  116. package/modules/VirtualScroller.verticalSpacing.js.map +1 -1
  117. package/modules/react/VirtualScroller.js +84 -35
  118. package/modules/react/VirtualScroller.js.map +1 -1
  119. package/modules/react/useClassName.js +3 -3
  120. package/modules/react/useClassName.js.map +1 -1
  121. package/modules/react/useForwardedRef.js +42 -0
  122. package/modules/react/useForwardedRef.js.map +1 -0
  123. package/modules/react/useInstanceMethods.js +4 -4
  124. package/modules/react/useInstanceMethods.js.map +1 -1
  125. package/modules/react/useItemKeys.js +28 -5
  126. package/modules/react/useItemKeys.js.map +1 -1
  127. package/modules/react/useOnItemHeightDidChange.js +28 -12
  128. package/modules/react/useOnItemHeightDidChange.js.map +1 -1
  129. package/modules/react/useSetItemState.js +31 -12
  130. package/modules/react/useSetItemState.js.map +1 -1
  131. package/modules/react/useState.js +9 -9
  132. package/modules/react/useState.js.map +1 -1
  133. package/modules/react/{useStateNoStaleBug.js → useStateWithRepeatableRead.js} +2 -2
  134. package/modules/react/useStateWithRepeatableRead.js.map +1 -0
  135. package/modules/react/useStyle.js +10 -4
  136. package/modules/react/useStyle.js.map +1 -1
  137. package/modules/react/useValidateTableBodyItemsContainer.js +16 -0
  138. package/modules/react/useValidateTableBodyItemsContainer.js.map +1 -0
  139. package/modules/react/useVirtualScroller.js +4 -3
  140. package/modules/react/useVirtualScroller.js.map +1 -1
  141. package/modules/test/ItemsContainer.js +10 -10
  142. package/modules/test/ItemsContainer.js.map +1 -1
  143. package/modules/test/VirtualScroller.js +25 -10
  144. package/modules/test/VirtualScroller.js.map +1 -1
  145. package/package.json +1 -1
  146. package/react/as.d.ts +42 -0
  147. package/react/index.d.ts +204 -63
  148. package/source/BeforeResize.js +1 -2
  149. package/source/DOM/VirtualScroller.js +7 -13
  150. package/source/DOM/tbody.js +5 -5
  151. package/source/ItemHeights.js +11 -12
  152. package/source/Layout.defaults.js +8 -0
  153. package/source/Layout.js +66 -53
  154. package/source/Layout.test.js +7 -2
  155. package/source/VirtualScroller.constructor.js +27 -4
  156. package/source/VirtualScroller.items.js +47 -2
  157. package/source/VirtualScroller.js +23 -14
  158. package/source/VirtualScroller.layout.js +41 -28
  159. package/source/VirtualScroller.onContainerResize.js +1 -2
  160. package/source/VirtualScroller.state.js +10 -11
  161. package/source/VirtualScroller.verticalSpacing.js +32 -6
  162. package/source/react/VirtualScroller.js +96 -33
  163. package/source/react/useClassName.js +3 -3
  164. package/source/react/useForwardedRef.js +39 -0
  165. package/source/react/useInstanceMethods.js +12 -4
  166. package/source/react/useItemKeys.js +22 -4
  167. package/source/react/useOnItemHeightDidChange.js +29 -10
  168. package/source/react/useSetItemState.js +32 -10
  169. package/source/react/useState.js +6 -6
  170. package/source/react/{useStateNoStaleBug.js → useStateWithRepeatableRead.js} +1 -1
  171. package/source/react/useStyle.js +3 -2
  172. package/source/react/useValidateTableBodyItemsContainer.js +16 -0
  173. package/source/react/useVirtualScroller.js +4 -3
  174. package/source/test/ItemsContainer.js +10 -10
  175. package/source/test/VirtualScroller.js +16 -10
  176. package/website/index-dom-bypass.html +198 -0
  177. package/website/index-dom-grid.html +204 -0
  178. package/website/index-dom-scrollableContainer.html +215 -0
  179. package/website/index-dom-tbody-scrollableContainer.html +81 -0
  180. package/website/index-dom-tbody.html +65 -0
  181. package/website/index-dom.html +117 -84
  182. package/website/index-react-bypass.html +200 -0
  183. package/website/{index-grid.html → index-react-grid.html} +79 -92
  184. package/website/index-react-scrollableContainer.html +213 -0
  185. package/website/index-react-strictMode.html +199 -0
  186. package/website/index-react-tbody-scrollableContainer.html +96 -0
  187. package/website/index-react-tbody.html +80 -0
  188. package/website/{index-bypass.html → index-react.html} +84 -70
  189. package/website/index.html +84 -69
  190. package/website/{messages.js → items.js} +1 -1
  191. package/website/lib/babel.min.js +25 -0
  192. package/website/lib/prop-types.min.js +1 -0
  193. package/website/lib/react-dom.development.js +29924 -0
  194. package/website/lib/react-dom.production.min.js +267 -0
  195. package/website/lib/react.development.js +3343 -0
  196. package/website/lib/react.production.min.js +31 -0
  197. package/website/style.base.css +33 -0
  198. package/website/style.list.css +92 -0
  199. package/website/style.list.grid.css +23 -0
  200. package/bundle/index-tbody-scrollableContainer.html +0 -70
  201. package/bundle/index-tbody.html +0 -57
  202. package/bundle/on-scroll-to-dom.js +0 -2
  203. package/bundle/on-scroll-to-dom.js.map +0 -1
  204. package/bundle/on-scroll-to-react.js +0 -2
  205. package/bundle/on-scroll-to-react.js.map +0 -1
  206. package/bundle/on-scroll-to.js +0 -2
  207. package/bundle/on-scroll-to.js.map +0 -1
  208. package/commonjs/react/useStateNoStaleBug.js.map +0 -1
  209. package/modules/react/useStateNoStaleBug.js.map +0 -1
  210. package/website/index-scrollableContainer.html +0 -208
  211. package/website/index-tbody-scrollableContainer.html +0 -70
  212. package/website/index-tbody.html +0 -57
  213. package/website/lib/on-scroll-to-dom.js +0 -2
  214. package/website/lib/on-scroll-to-dom.js.map +0 -1
  215. package/website/lib/on-scroll-to-react.js +0 -2
  216. 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
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
  /**
@@ -101,7 +101,7 @@ export default function() {
101
101
  // or an "Expand YouTube video" button, which would result
102
102
  // in the actual height of the list item being different
103
103
  // from what has been initially measured in `this.itemHeights[i]`,
104
- // if the developer didn't call `.setItemState(i, newState)` and `.onItemHeightDidChange(i)`.
104
+ // if the developer didn't call `.setItemState(item, newState)` and `.onItemHeightDidChange(item)`.
105
105
  if (!validateWillBeHiddenItemHeightsAreAccurate.call(this, firstShownItemIndex, lastShownItemIndex)) {
106
106
  log('~ Because some of the will-be-hidden item heights (listed above) have changed since they\'ve last been measured, redo layout. ~')
107
107
  // Redo layout, now with the correct item heights.
@@ -135,7 +135,7 @@ export default function() {
135
135
  log('Last shown item index', lastShownItemIndex)
136
136
  log('Before items height', beforeItemsHeight)
137
137
  log('After items height (actual or estimated)', afterItemsHeight)
138
- log('Average item height (used for estimated after items height calculation)', this.itemHeights.getAverage())
138
+ log('Average item height (used for estimated after items height calculation)', this.getAverageItemHeight())
139
139
  if (isDebug()) {
140
140
  log('Item heights', this.getState().itemHeights.slice())
141
141
  log('Item states', this.getState().itemStates.slice())
@@ -177,9 +177,9 @@ export default function() {
177
177
  // Instead of using a `this.previouslyCalculatedLayout` instance variable,
178
178
  // this code could use `this.getState()` because it reflects what's currently on screen,
179
179
  // but there's a single edge case when it could go out of sync —
180
- // updating item heights externally via `.onItemHeightDidChange(i)`.
180
+ // updating item heights externally via `.onItemHeightDidChange(item)`.
181
181
  //
182
- // If, for example, an item height was updated externally via `.onItemHeightDidChange(i)`
182
+ // If, for example, an item height was updated externally via `.onItemHeightDidChange(item)`
183
183
  // then `this.getState().itemHeights` would get updated immediately but
184
184
  // `this.getState().beforeItemsHeight` or `this.getState().afterItemsHeight`
185
185
  // would still correspond to the previous item height, so those would be "stale".
@@ -270,7 +270,7 @@ export default function() {
270
270
  * or an "Expand YouTube video" button, which would result
271
271
  * in the actual height of the list item being different
272
272
  * from what has been initially measured in `this.itemHeights[i]`,
273
- * if the developer didn't call `.setItemState(i, newState)` and `.onItemHeightDidChange(i)`.
273
+ * if the developer didn't call `.setItemState(item, newState)` and `.onItemHeightDidChange(item)`.
274
274
  */
275
275
  function validateWillBeHiddenItemHeightsAreAccurate(firstShownItemIndex, lastShownItemIndex) {
276
276
  let isValid = true
@@ -282,26 +282,26 @@ export default function() {
282
282
  // The item will be hidden. Re-measure its height.
283
283
  // The rationale is that there could be a situation when an item's
284
284
  // height has changed, and the developer has properly added an
285
- // `.onItemHeightDidChange(i)` call to notify `VirtualScroller`
285
+ // `.onItemHeightDidChange(item)` call to notify `VirtualScroller`
286
286
  // about that change, but at the same time that wouldn't work.
287
287
  // For example, suppose there's a list of several items on a page,
288
288
  // and those items are in "minimized" state (having height 100px).
289
289
  // Then, a user clicks an "Expand all items" button, and all items
290
290
  // in the list are expanded (expanded item height is gonna be 700px).
291
- // `VirtualScroller` demands that `.onItemHeightDidChange(i)` is called
291
+ // `VirtualScroller` demands that `.onItemHeightDidChange(item)` is called
292
292
  // in such cases, and the developer has properly added the code to do that.
293
293
  // So, if there were 10 "minimized" items visible on a page, then there
294
- // will be 10 individual `.onItemHeightDidChange(i)` calls. No issues so far.
295
- // But, as the first `.onItemHeightDidChange(i)` call executes, it immediately
294
+ // will be 10 individual `.onItemHeightDidChange(item)` calls. No issues so far.
295
+ // But, as the first `.onItemHeightDidChange(item)` call executes, it immediately
296
296
  // ("synchronously") triggers a re-layout, and that re-layout finds out
297
297
  // that now, because the first item is big, it occupies most of the screen
298
298
  // space, and only the first 3 items are visible on screen instead of 10,
299
299
  // and so it leaves the first 3 items mounted and unmounts the rest 7.
300
300
  // Then, after `VirtualScroller` has rerendered, the code returns to
301
- // where it was executing, and calls `.onItemHeightDidChange(i)` for the
301
+ // where it was executing, and calls `.onItemHeightDidChange(item)` for the
302
302
  // second item. It also triggers an immediate re-layout that finds out
303
303
  // that only the first 2 items are visible on screen, and it unmounts
304
- // the third one too. After that, it calls `.onItemHeightDidChange(i)`
304
+ // the third one too. After that, it calls `.onItemHeightDidChange(item)`
305
305
  // for the third item, but that item is no longer rendered, so its height
306
306
  // can't be measured, and the same's for all the rest of the original 10 items.
307
307
  // So, even though the developer has written their code properly, the
@@ -319,7 +319,7 @@ export default function() {
319
319
  updatePreviouslyCalculatedLayoutOnItemHeightChange.call(this, i, previouslyMeasuredItemHeight, actualItemHeight)
320
320
  }
321
321
  isValid = false
322
- warn('Item index', i, 'is no longer visible and will be unmounted. Its height has changed from', previouslyMeasuredItemHeight, 'to', actualItemHeight, 'since it was last measured. This is not necessarily a bug, and could happen, for example, on screen width change, or when there\'re several `onItemHeightDidChange(i)` calls issued at the same time, and the first one triggers a re-layout before the rest of them have had a chance to be executed.')
322
+ warn('Item index', i, 'is no longer visible and will be unmounted. Its height has changed from', previouslyMeasuredItemHeight, 'to', actualItemHeight, 'since it was last measured. This is not necessarily a bug, and could happen, for example, on screen width change, or when there\'re several `onItemHeightDidChange(item)` calls issued at the same time, and the first one triggers a re-layout before the rest of them have had a chance to be executed.')
323
323
  }
324
324
  }
325
325
  i++
@@ -372,9 +372,15 @@ export default function() {
372
372
  return listTopOffset
373
373
  }
374
374
 
375
- this._onItemHeightDidChange = (i) => {
376
- log('~ On Item Height Did Change was called ~')
377
- log('Item index', i)
375
+ this._onItemHeightDidChange = (itemOrIndex) => {
376
+ // Item index.
377
+ const i = this._getItemIndexByItemOrIndex(itemOrIndex)
378
+
379
+ // If the item wasn't found, the error was already reported,
380
+ // so just don't do anything else.
381
+ if (i === undefined) {
382
+ return
383
+ }
378
384
 
379
385
  const {
380
386
  itemHeights,
@@ -382,39 +388,42 @@ export default function() {
382
388
  lastShownItemIndex
383
389
  } = this.getState()
384
390
 
391
+ log('~ On Item Height Did Change was called ~')
392
+ log('Item index', i)
393
+
385
394
  // Check if the item is still rendered.
386
395
  if (!(i >= firstShownItemIndex && i <= lastShownItemIndex)) {
387
396
  // There could be valid cases when an item is no longer rendered
388
- // by the time `.onItemHeightDidChange(i)` gets called.
397
+ // by the time `.onItemHeightDidChange(item)` gets called.
389
398
  // For example, suppose there's a list of several items on a page,
390
399
  // and those items are in "minimized" state (having height 100px).
391
400
  // Then, a user clicks an "Expand all items" button, and all items
392
401
  // in the list are expanded (expanded item height is gonna be 700px).
393
- // `VirtualScroller` demands that `.onItemHeightDidChange(i)` is called
402
+ // `VirtualScroller` demands that `.onItemHeightDidChange(item)` is called
394
403
  // in such cases, and the developer has properly added the code to do that.
395
404
  // So, if there were 10 "minimized" items visible on a page, then there
396
- // will be 10 individual `.onItemHeightDidChange(i)` calls. No issues so far.
397
- // But, as the first `.onItemHeightDidChange(i)` call executes, it immediately
405
+ // will be 10 individual `.onItemHeightDidChange(item)` calls. No issues so far.
406
+ // But, as the first `.onItemHeightDidChange(item)` call executes, it immediately
398
407
  // ("synchronously") triggers a re-layout, and that re-layout finds out
399
408
  // that now, because the first item is big, it occupies most of the screen
400
409
  // space, and only the first 3 items are visible on screen instead of 10,
401
410
  // and so it leaves the first 3 items mounted and unmounts the rest 7.
402
411
  // Then, after `VirtualScroller` has rerendered, the code returns to
403
- // where it was executing, and calls `.onItemHeightDidChange(i)` for the
412
+ // where it was executing, and calls `.onItemHeightDidChange(item)` for the
404
413
  // second item. It also triggers an immediate re-layout that finds out
405
414
  // that only the first 2 items are visible on screen, and it unmounts
406
- // the third one too. After that, it calls `.onItemHeightDidChange(i)`
415
+ // the third one too. After that, it calls `.onItemHeightDidChange(item)`
407
416
  // for the third item, but that item is no longer rendered, so its height
408
417
  // can't be measured, and the same's for all the rest of the original 10 items.
409
418
  // So, even though the developer has written their code properly, there're
410
419
  // still situations when the item could be no longer rendered by the time
411
- // `.onItemHeightDidChange(i)` gets called.
412
- return warn('The item is no longer rendered. This is not necessarily a bug, and could happen, for example, when when a developer calls `onItemHeightDidChange(i)` while looping through a batch of items.')
420
+ // `.onItemHeightDidChange(item)` gets called.
421
+ return warn('The item is no longer rendered. This is not necessarily a bug, and could happen, for example, when when a developer calls `onItemHeightDidChange(item)` while looping through a batch of items.')
413
422
  }
414
423
 
415
424
  const previousHeight = itemHeights[i]
416
425
  if (previousHeight === undefined) {
417
- // There're valid cases when the item still hasn't been measured and `onItemHeightDidChange()`
426
+ // There're valid cases when the item still hasn't been measured and `onItemHeightDidChange(item)`
418
427
  // function was called for it. That's because measuring items is only done after the `VirtualScroller`
419
428
  // has `start()`ed. But it's not neccessarily `start()`ed by the time it has been rendered (mounted).
420
429
  // For example, the React component `<VirtualScroller/>` provides an `readyToStart={false}` property
@@ -435,7 +444,7 @@ export default function() {
435
444
  try {
436
445
  newHeight = remeasureItemHeight.call(this, i)
437
446
  } catch (error) {
438
- // Successfully finishing an `onItemHeightDidChange(i)` call is not considered
447
+ // Successfully finishing an `onItemHeightDidChange(item)` call is not considered
439
448
  // critical for `VirtualScroller`'s operation, so such errors could be ignored.
440
449
  if (error instanceof ItemNotRenderedError) {
441
450
  return reportError(`"onItemHeightDidChange()" has been called for item index ${i} but the item is not currently rendered and can\'t be measured. The exact error was: ${error.message}`)
@@ -460,7 +469,7 @@ export default function() {
460
469
  // Recalculate layout.
461
470
  //
462
471
  // If the `VirtualScroller` is already waiting for a state update to be rendered,
463
- // delay `onItemHeightDidChange(i)`'s re-layout until that state update is rendered.
472
+ // delay `onItemHeightDidChange(item)`'s re-layout until that state update is rendered.
464
473
  // The reason is that React `<VirtualScroller/>`'s `onHeightDidChange()` is meant to
465
474
  // be called inside `useLayoutEffect()` hook. Due to how React is implemented internally,
466
475
  // that might happen in the middle of the currently pending `setState()` operation
@@ -503,8 +512,12 @@ export default function() {
503
512
  // scroll past the screen height, because they'd stop to read through
504
513
  // the newly visible items first, and when they do stop scrolling, that's
505
514
  // when layout gets recalculated).
506
- const renderAheadMarginRatio = 1 // in scrollable container heights.
507
- return this.scrollableContainer.getHeight() * renderAheadMarginRatio
515
+ return this.scrollableContainer.getHeight() * this.getPrerenderMarginRatio()
516
+ }
517
+
518
+ this.getPrerenderMarginRatio = () => {
519
+ // See the readme for the description of `prerenderMarginRatio` option.
520
+ return 1 // in scrollable container heights.
508
521
  }
509
522
 
510
523
  /**
@@ -134,8 +134,7 @@ export default function() {
134
134
  columnsCount,
135
135
  itemHeights: this.beforeResize.snapshotBeforeResizeItemHeights({
136
136
  firstShownItemIndex,
137
- newFirstShownItemIndex,
138
- newColumnsCount
137
+ newFirstShownItemIndex
139
138
  })
140
139
  }
141
140
  }