virtual-scroller 1.8.1 → 1.9.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.
- package/CHANGELOG.md +57 -0
- package/README.md +337 -160
- package/bundle/virtual-scroller-dom.js +1 -1
- package/bundle/virtual-scroller-dom.js.map +1 -1
- package/bundle/virtual-scroller-react.js +1 -1
- package/bundle/virtual-scroller-react.js.map +1 -1
- package/bundle/virtual-scroller.js +1 -1
- package/bundle/virtual-scroller.js.map +1 -1
- package/commonjs/BeforeResize.js +23 -27
- package/commonjs/BeforeResize.js.map +1 -1
- package/commonjs/DOM/Engine.js +7 -7
- package/commonjs/DOM/Engine.js.map +1 -1
- package/commonjs/DOM/ItemsContainer.js +1 -1
- package/commonjs/DOM/ItemsContainer.js.map +1 -1
- package/commonjs/DOM/ListTopOffsetWatcher.js +15 -9
- package/commonjs/DOM/ListTopOffsetWatcher.js.map +1 -1
- package/commonjs/DOM/ScrollableContainer.js +28 -28
- package/commonjs/DOM/ScrollableContainer.js.map +1 -1
- package/commonjs/DOM/VirtualScroller.js +20 -17
- package/commonjs/DOM/VirtualScroller.js.map +1 -1
- package/commonjs/DOM/tbody.js +16 -10
- package/commonjs/DOM/tbody.js.map +1 -1
- package/commonjs/ItemHeights.js +13 -7
- package/commonjs/ItemHeights.js.map +1 -1
- package/commonjs/Layout.js +15 -13
- package/commonjs/Layout.js.map +1 -1
- package/commonjs/Layout.test.js +8 -3
- package/commonjs/Layout.test.js.map +1 -1
- package/commonjs/{ListHeightChangeWatcher.js → ListHeightMeasurement.js} +26 -28
- package/commonjs/ListHeightMeasurement.js.map +1 -0
- package/commonjs/Resize.js +38 -28
- package/commonjs/Resize.js.map +1 -1
- package/commonjs/Scroll.js +28 -44
- package/commonjs/Scroll.js.map +1 -1
- package/commonjs/VirtualScroller.columns.js +43 -0
- package/commonjs/VirtualScroller.columns.js.map +1 -0
- package/commonjs/VirtualScroller.constructor.js +408 -0
- package/commonjs/VirtualScroller.constructor.js.map +1 -0
- package/commonjs/VirtualScroller.items.js +305 -0
- package/commonjs/VirtualScroller.items.js.map +1 -0
- package/commonjs/VirtualScroller.js +132 -1872
- package/commonjs/VirtualScroller.js.map +1 -1
- package/commonjs/VirtualScroller.layout.js +562 -0
- package/commonjs/VirtualScroller.layout.js.map +1 -0
- package/commonjs/VirtualScroller.onRender.js +357 -0
- package/commonjs/VirtualScroller.onRender.js.map +1 -0
- package/commonjs/VirtualScroller.resize.js +186 -0
- package/commonjs/VirtualScroller.resize.js.map +1 -0
- package/commonjs/VirtualScroller.state.js +301 -0
- package/commonjs/VirtualScroller.state.js.map +1 -0
- package/commonjs/VirtualScroller.verticalSpacing.js +65 -0
- package/commonjs/VirtualScroller.verticalSpacing.js.map +1 -0
- package/commonjs/getItemCoordinates.js.map +1 -1
- package/commonjs/getItemsDiff.js.map +1 -1
- package/commonjs/getVerticalSpacing.js.map +1 -1
- package/commonjs/package.json +5 -0
- package/commonjs/react/VirtualScroller.js +180 -620
- package/commonjs/react/VirtualScroller.js.map +1 -1
- package/commonjs/react/useClassName.js +26 -0
- package/commonjs/react/useClassName.js.map +1 -0
- package/commonjs/react/useHandleItemsChange.js +116 -0
- package/commonjs/react/useHandleItemsChange.js.map +1 -0
- package/commonjs/react/useInstanceMethods.js +37 -0
- package/commonjs/react/useInstanceMethods.js.map +1 -0
- package/commonjs/react/useItemKeys.js +60 -0
- package/commonjs/react/useItemKeys.js.map +1 -0
- package/commonjs/react/useOnItemHeightChange.js +32 -0
- package/commonjs/react/useOnItemHeightChange.js.map +1 -0
- package/commonjs/react/useOnItemStateChange.js +32 -0
- package/commonjs/react/useOnItemStateChange.js.map +1 -0
- package/commonjs/react/useState.js +140 -0
- package/commonjs/react/useState.js.map +1 -0
- package/commonjs/react/useStyle.js +29 -0
- package/commonjs/react/useStyle.js.map +1 -0
- package/commonjs/react/useVirtualScroller.js +62 -0
- package/commonjs/react/useVirtualScroller.js.map +1 -0
- package/commonjs/react/useVirtualScrollerStartStop.js +20 -0
- package/commonjs/react/useVirtualScrollerStartStop.js.map +1 -0
- package/commonjs/test/Engine.js +23 -0
- package/commonjs/test/Engine.js.map +1 -0
- package/commonjs/test/ItemsContainer.js +127 -0
- package/commonjs/test/ItemsContainer.js.map +1 -0
- package/commonjs/test/ScrollableContainer.js +130 -0
- package/commonjs/test/ScrollableContainer.js.map +1 -0
- package/commonjs/test/VirtualScroller.js +281 -0
- package/commonjs/test/VirtualScroller.js.map +1 -0
- package/commonjs/utility/debounce.js +2 -2
- package/commonjs/utility/debounce.js.map +1 -1
- package/commonjs/utility/debug.js.map +1 -1
- package/commonjs/utility/getStateSnapshot.js +2 -2
- package/commonjs/utility/getStateSnapshot.js.map +1 -1
- package/commonjs/utility/px.js.map +1 -1
- package/commonjs/utility/px.test.js +1 -1
- package/commonjs/utility/px.test.js.map +1 -1
- package/commonjs/utility/shallowEqual.js +1 -1
- package/commonjs/utility/shallowEqual.js.map +1 -1
- package/commonjs/utility/throttle.js.map +1 -1
- package/dom/index.cjs +4 -0
- package/dom/index.cjs.js +9 -0
- package/dom/index.d.ts +6 -4
- package/dom/index.js +1 -1
- package/dom/package.json +10 -4
- package/index.cjs +4 -0
- package/index.cjs.js +9 -0
- package/index.d.ts +30 -15
- package/index.js +1 -1
- package/modules/BeforeResize.js +22 -27
- package/modules/BeforeResize.js.map +1 -1
- package/modules/DOM/Engine.js +6 -6
- package/modules/DOM/Engine.js.map +1 -1
- package/modules/DOM/ItemsContainer.js +1 -1
- package/modules/DOM/ItemsContainer.js.map +1 -1
- package/modules/DOM/ListTopOffsetWatcher.js +15 -9
- package/modules/DOM/ListTopOffsetWatcher.js.map +1 -1
- package/modules/DOM/ScrollableContainer.js +28 -28
- package/modules/DOM/ScrollableContainer.js.map +1 -1
- package/modules/DOM/VirtualScroller.js +19 -16
- package/modules/DOM/VirtualScroller.js.map +1 -1
- package/modules/DOM/tbody.js +11 -9
- package/modules/DOM/tbody.js.map +1 -1
- package/modules/ItemHeights.js +12 -6
- package/modules/ItemHeights.js.map +1 -1
- package/modules/Layout.js +14 -12
- package/modules/Layout.js.map +1 -1
- package/modules/Layout.test.js +8 -3
- package/modules/Layout.test.js.map +1 -1
- package/modules/{ListHeightChangeWatcher.js → ListHeightMeasurement.js} +25 -27
- package/modules/ListHeightMeasurement.js.map +1 -0
- package/modules/Resize.js +38 -28
- package/modules/Resize.js.map +1 -1
- package/modules/Scroll.js +28 -44
- package/modules/Scroll.js.map +1 -1
- package/modules/VirtualScroller.columns.js +36 -0
- package/modules/VirtualScroller.columns.js.map +1 -0
- package/modules/VirtualScroller.constructor.js +371 -0
- package/modules/VirtualScroller.constructor.js.map +1 -0
- package/modules/VirtualScroller.items.js +288 -0
- package/modules/VirtualScroller.items.js.map +1 -0
- package/modules/VirtualScroller.js +132 -1860
- package/modules/VirtualScroller.js.map +1 -1
- package/modules/VirtualScroller.layout.js +549 -0
- package/modules/VirtualScroller.layout.js.map +1 -0
- package/modules/VirtualScroller.onRender.js +337 -0
- package/modules/VirtualScroller.onRender.js.map +1 -0
- package/modules/VirtualScroller.resize.js +176 -0
- package/modules/VirtualScroller.resize.js.map +1 -0
- package/modules/VirtualScroller.state.js +283 -0
- package/modules/VirtualScroller.state.js.map +1 -0
- package/modules/VirtualScroller.verticalSpacing.js +54 -0
- package/modules/VirtualScroller.verticalSpacing.js.map +1 -0
- package/modules/getItemCoordinates.js.map +1 -1
- package/modules/getItemsDiff.js.map +1 -1
- package/modules/getVerticalSpacing.js.map +1 -1
- package/modules/react/VirtualScroller.js +176 -625
- package/modules/react/VirtualScroller.js.map +1 -1
- package/modules/react/useClassName.js +18 -0
- package/modules/react/useClassName.js.map +1 -0
- package/modules/react/useHandleItemsChange.js +108 -0
- package/modules/react/useHandleItemsChange.js.map +1 -0
- package/modules/react/useInstanceMethods.js +28 -0
- package/modules/react/useInstanceMethods.js.map +1 -0
- package/modules/react/useItemKeys.js +52 -0
- package/modules/react/useItemKeys.js.map +1 -0
- package/modules/react/useOnItemHeightChange.js +24 -0
- package/modules/react/useOnItemHeightChange.js.map +1 -0
- package/modules/react/useOnItemStateChange.js +24 -0
- package/modules/react/useOnItemStateChange.js.map +1 -0
- package/modules/react/useState.js +132 -0
- package/modules/react/useState.js.map +1 -0
- package/modules/react/useStyle.js +19 -0
- package/modules/react/useStyle.js.map +1 -0
- package/modules/react/useVirtualScroller.js +51 -0
- package/modules/react/useVirtualScroller.js.map +1 -0
- package/modules/react/useVirtualScrollerStartStop.js +12 -0
- package/modules/react/useVirtualScrollerStartStop.js.map +1 -0
- package/modules/test/Engine.js +11 -0
- package/modules/test/Engine.js.map +1 -0
- package/modules/test/ItemsContainer.js +120 -0
- package/modules/test/ItemsContainer.js.map +1 -0
- package/modules/test/ScrollableContainer.js +123 -0
- package/modules/test/ScrollableContainer.js.map +1 -0
- package/modules/test/VirtualScroller.js +270 -0
- package/modules/test/VirtualScroller.js.map +1 -0
- package/modules/utility/debounce.js +2 -2
- package/modules/utility/debounce.js.map +1 -1
- package/modules/utility/debug.js.map +1 -1
- package/modules/utility/getStateSnapshot.js +2 -2
- package/modules/utility/getStateSnapshot.js.map +1 -1
- package/modules/utility/px.js.map +1 -1
- package/modules/utility/px.test.js +1 -1
- package/modules/utility/px.test.js.map +1 -1
- package/modules/utility/shallowEqual.js +1 -1
- package/modules/utility/shallowEqual.js.map +1 -1
- package/modules/utility/throttle.js.map +1 -1
- package/package.json +46 -23
- package/react/index.cjs +4 -0
- package/react/index.cjs.js +9 -0
- package/react/index.d.ts +10 -9
- package/react/index.js +1 -1
- package/react/package.json +10 -4
- package/rollup.config.mjs +62 -0
- package/runnable/create-commonjs-package-json.js +11 -0
- package/source/BeforeResize.js +16 -21
- package/source/DOM/Engine.js +8 -10
- package/source/DOM/ListTopOffsetWatcher.js +13 -8
- package/source/DOM/ScrollableContainer.js +23 -21
- package/source/DOM/VirtualScroller.js +27 -11
- package/source/DOM/tbody.js +30 -21
- package/source/ItemHeights.js +9 -4
- package/source/Layout.js +12 -9
- package/source/Layout.test.js +8 -3
- package/source/{ListHeightChangeWatcher.js → ListHeightMeasurement.js} +21 -20
- package/source/Resize.js +41 -25
- package/source/Scroll.js +27 -35
- package/source/VirtualScroller.columns.js +26 -0
- package/source/VirtualScroller.constructor.js +336 -0
- package/source/VirtualScroller.items.js +302 -0
- package/source/VirtualScroller.js +144 -1875
- package/source/VirtualScroller.layout.js +539 -0
- package/source/VirtualScroller.onRender.js +345 -0
- package/source/VirtualScroller.resize.js +189 -0
- package/source/VirtualScroller.state.js +284 -0
- package/source/VirtualScroller.verticalSpacing.js +51 -0
- package/source/react/VirtualScroller.js +243 -587
- package/source/react/useClassName.js +14 -0
- package/source/react/useHandleItemsChange.js +115 -0
- package/source/react/useInstanceMethods.js +25 -0
- package/source/react/useItemKeys.js +59 -0
- package/source/react/useOnItemHeightChange.js +28 -0
- package/source/react/useOnItemStateChange.js +28 -0
- package/source/react/useState.js +114 -0
- package/source/react/useStyle.js +20 -0
- package/source/react/useVirtualScroller.js +59 -0
- package/source/react/useVirtualScrollerStartStop.js +12 -0
- package/source/test/Engine.js +11 -0
- package/source/test/ItemsContainer.js +87 -0
- package/source/test/ScrollableContainer.js +88 -0
- package/source/test/VirtualScroller.js +232 -0
- package/source/utility/debounce.js +2 -2
- package/source/utility/px.test.js +1 -1
- package/babel.config.js +0 -25
- package/babel.js +0 -5
- package/commonjs/ListHeightChangeWatcher.js.map +0 -1
- package/dom/index.commonjs.js +0 -4
- package/index.commonjs.js +0 -4
- package/modules/ListHeightChangeWatcher.js.map +0 -1
- package/react/index.commonjs.js +0 -4
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { TBODY_CLASS_NAME } from '../DOM/tbody.js'
|
|
2
|
+
|
|
3
|
+
export default function useClassName(className, { tbody }) {
|
|
4
|
+
// For `<tbody/>`, a workaround is used which uses CSS variables
|
|
5
|
+
// and a special CSS class name "VirtualScroller".
|
|
6
|
+
// See `addTbodyStyles()` function in `../DOM/tbody.js` for more details.
|
|
7
|
+
if (tbody) {
|
|
8
|
+
if (className) {
|
|
9
|
+
return className + ' ' + TBODY_CLASS_NAME
|
|
10
|
+
}
|
|
11
|
+
return TBODY_CLASS_NAME
|
|
12
|
+
}
|
|
13
|
+
return className
|
|
14
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { useRef, useLayoutEffect } from 'react'
|
|
2
|
+
|
|
3
|
+
// If new `items` are passed:
|
|
4
|
+
//
|
|
5
|
+
// * Store the scroll Y position for the first one of the current items
|
|
6
|
+
// so that it could potentially (in some cases) be restored after the
|
|
7
|
+
// new `items` are rendered.
|
|
8
|
+
//
|
|
9
|
+
// * Call `VirtualScroller.setItems()` function.
|
|
10
|
+
//
|
|
11
|
+
// * Re-generate the React `key` prefix for item elements
|
|
12
|
+
// so that all item components are re-rendered for the new `items` list.
|
|
13
|
+
// That's because item components may have their own internal state,
|
|
14
|
+
// and simply passing another `item` property for an item component
|
|
15
|
+
// might result in bugs, which React would do with its "re-using" policy
|
|
16
|
+
// if the unique `key` workaround hasn't been used.
|
|
17
|
+
//
|
|
18
|
+
export default function useHandleItemsChange(items, {
|
|
19
|
+
virtualScroller,
|
|
20
|
+
// `preserveScrollPosition` property name is deprecated,
|
|
21
|
+
// use `preserveScrollPositionOnPrependItems` property instead.
|
|
22
|
+
preserveScrollPosition,
|
|
23
|
+
preserveScrollPositionOnPrependItems,
|
|
24
|
+
updateItemKeysForNewItems
|
|
25
|
+
}) {
|
|
26
|
+
const {
|
|
27
|
+
items: renderedItems,
|
|
28
|
+
firstShownItemIndex
|
|
29
|
+
} = virtualScroller.getState()
|
|
30
|
+
|
|
31
|
+
// During render, check if the `items` list has changed.
|
|
32
|
+
// If it has, capture the Y scroll position and updated item element `key`s.
|
|
33
|
+
|
|
34
|
+
// A long "advanced" sidenote on why capturing scroll Y position
|
|
35
|
+
// is done during render instead of in an "effect":
|
|
36
|
+
//
|
|
37
|
+
// Previously, capturing scroll Y position was being done in `useLayoutEffect()`
|
|
38
|
+
// but it was later found out that it wouldn't work for a "Show previous" button
|
|
39
|
+
// scenario because that button would get hidden by the time `useLayoutEffect()`
|
|
40
|
+
// gets called when there're no more "previous" items to show.
|
|
41
|
+
//
|
|
42
|
+
// Consider this code example:
|
|
43
|
+
//
|
|
44
|
+
// const { fromIndex, items } = this.state
|
|
45
|
+
// const items = allItems.slice(fromIndex)
|
|
46
|
+
// return (
|
|
47
|
+
// {fromIndex > 0 &&
|
|
48
|
+
// <button onClick={this.onShowPrevious}>
|
|
49
|
+
// Show previous
|
|
50
|
+
// </button>
|
|
51
|
+
// }
|
|
52
|
+
// <VirtualScroller
|
|
53
|
+
// items={items}
|
|
54
|
+
// itemComponent={ItemComponent}/>
|
|
55
|
+
// )
|
|
56
|
+
//
|
|
57
|
+
// Consider a user clicks "Show previous" to show the items from the start.
|
|
58
|
+
// By the time `componentDidUpdate()` is called on `<VirtualScroller/>`,
|
|
59
|
+
// the "Show previous" button has already been hidden
|
|
60
|
+
// (because there're no more "previous" items)
|
|
61
|
+
// which results in the scroll Y position jumping forward
|
|
62
|
+
// by the height of that "Show previous" button.
|
|
63
|
+
// This is because `<VirtualScroller/>` captures scroll Y
|
|
64
|
+
// position when items are prepended via `.setItems()`
|
|
65
|
+
// when the "Show previous" button is still being shown,
|
|
66
|
+
// and then restores scroll Y position in `.onRender()`
|
|
67
|
+
// when the "Show previous" button has already been hidden:
|
|
68
|
+
// that's the reason for the scroll Y "jump".
|
|
69
|
+
//
|
|
70
|
+
// To prevent that, scroll Y position is captured at `render()`
|
|
71
|
+
// time rather than later in `componentDidUpdate()`: this way,
|
|
72
|
+
// scroll Y position is captured while the "Show previous" button
|
|
73
|
+
// is still being shown.
|
|
74
|
+
|
|
75
|
+
const previousItems = useRef(items)
|
|
76
|
+
const hasItemsPropertyChanged = items !== previousItems.current
|
|
77
|
+
previousItems.current = items
|
|
78
|
+
if (hasItemsPropertyChanged) {
|
|
79
|
+
let itemsHaveChanged = true
|
|
80
|
+
let shouldUpdateItemKeys = true
|
|
81
|
+
const itemsDiff = virtualScroller.getItemsDiff(renderedItems, items)
|
|
82
|
+
// `itemsDiff` will be `undefined` in case of a non-incremental items list change.
|
|
83
|
+
if (itemsDiff) {
|
|
84
|
+
const {
|
|
85
|
+
prependedItemsCount,
|
|
86
|
+
appendedItemsCount
|
|
87
|
+
} = itemsDiff
|
|
88
|
+
if (prependedItemsCount === 0 && appendedItemsCount === 0) {
|
|
89
|
+
// The items haven't changed. No need to re-generate
|
|
90
|
+
// the `key` prefix or to snapshot the Y scroll position.
|
|
91
|
+
itemsHaveChanged = false
|
|
92
|
+
shouldUpdateItemKeys = false
|
|
93
|
+
}
|
|
94
|
+
else if (prependedItemsCount === 0 && appendedItemsCount > 0) {
|
|
95
|
+
// Just some items got appended. No need to re-generate
|
|
96
|
+
// the `key` prefix or to snapshot the Y scroll position.
|
|
97
|
+
shouldUpdateItemKeys = false
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (itemsHaveChanged) {
|
|
102
|
+
// Set the new `items`.
|
|
103
|
+
virtualScroller.setItems(items, {
|
|
104
|
+
// `preserveScrollPosition` property name is deprecated,
|
|
105
|
+
// use `preserveScrollPositionOnPrependItems` property instead.
|
|
106
|
+
preserveScrollPositionOnPrependItems: preserveScrollPositionOnPrependItems || preserveScrollPosition
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
// Update React element `key`s for the new set of `items`.
|
|
110
|
+
if (shouldUpdateItemKeys) {
|
|
111
|
+
updateItemKeysForNewItems()
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useImperativeHandle } from 'react'
|
|
2
|
+
|
|
3
|
+
import { reportError } from '../utility/debug.js'
|
|
4
|
+
|
|
5
|
+
// Adds instance methods to the React component.
|
|
6
|
+
export default function useInstanceMethods(ref, {
|
|
7
|
+
virtualScroller
|
|
8
|
+
}) {
|
|
9
|
+
useImperativeHandle(ref, () => ({
|
|
10
|
+
// This is a proxy for `VirtualScroller`'s `.updateLayout` instance method.
|
|
11
|
+
updateLayout: () => virtualScroller.updateLayout(),
|
|
12
|
+
|
|
13
|
+
// (deprecated)
|
|
14
|
+
// `.layout()` method name is deprecated, use `.updateLayout()` instead.
|
|
15
|
+
layout: () => virtualScroller.updateLayout(),
|
|
16
|
+
|
|
17
|
+
// (deprecated)
|
|
18
|
+
updateItem: (i) => reportError(`[virtual-scroller] ".updateItem(i)" method of React <VirtualScroller/> has been removed`),
|
|
19
|
+
|
|
20
|
+
// (deprecated)
|
|
21
|
+
renderItem: (i) => reportError(`[virtual-scroller] ".renderItem(i)" method of React <VirtualScroller/> has been removed`)
|
|
22
|
+
}), [
|
|
23
|
+
virtualScroller
|
|
24
|
+
])
|
|
25
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { useRef, useMemo, useCallback } from 'react'
|
|
2
|
+
|
|
3
|
+
export default function useItemKeys({ getItemId }) {
|
|
4
|
+
// List items are rendered with `key`s so that React doesn't
|
|
5
|
+
// "reuse" `itemComponent`s in cases when `items` are changed.
|
|
6
|
+
const itemKeyPrefix = useRef()
|
|
7
|
+
|
|
8
|
+
// Generates a unique `key` prefix for list item components.
|
|
9
|
+
const generateItemKeyPrefix = useMemo(() => {
|
|
10
|
+
let counter = 0
|
|
11
|
+
function getNextCounter() {
|
|
12
|
+
if (counter === Number.MAX_SAFE_INTEGER) {
|
|
13
|
+
counter = 0
|
|
14
|
+
}
|
|
15
|
+
counter++
|
|
16
|
+
return counter
|
|
17
|
+
}
|
|
18
|
+
return () => {
|
|
19
|
+
itemKeyPrefix.current = String(getNextCounter())
|
|
20
|
+
}
|
|
21
|
+
}, [
|
|
22
|
+
itemKeyPrefix
|
|
23
|
+
])
|
|
24
|
+
|
|
25
|
+
useMemo(() => {
|
|
26
|
+
// Generate an initial unique `key` prefix for list item components.
|
|
27
|
+
generateItemKeyPrefix()
|
|
28
|
+
}, [])
|
|
29
|
+
|
|
30
|
+
const generateItemKeyPrefixIfNotUsingItemIds = useCallback(() => {
|
|
31
|
+
if (!getItemId) {
|
|
32
|
+
generateItemKeyPrefix()
|
|
33
|
+
}
|
|
34
|
+
}, [
|
|
35
|
+
getItemId,
|
|
36
|
+
generateItemKeyPrefix
|
|
37
|
+
])
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Returns a `key` for an `item`'s element.
|
|
41
|
+
* @param {object} item — The item.
|
|
42
|
+
* @param {number} i — Item's index in `items` list.
|
|
43
|
+
* @return {any}
|
|
44
|
+
*/
|
|
45
|
+
const getItemKey = useCallback((item, i) => {
|
|
46
|
+
if (getItemId) {
|
|
47
|
+
return getItemId(item)
|
|
48
|
+
}
|
|
49
|
+
return `${itemKeyPrefix.current}:${i}`
|
|
50
|
+
}, [
|
|
51
|
+
getItemId,
|
|
52
|
+
itemKeyPrefix
|
|
53
|
+
])
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
getItemKey,
|
|
57
|
+
updateItemKeysForNewItems: generateItemKeyPrefixIfNotUsingItemIds
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useMemo, useRef, useCallback } from 'react'
|
|
2
|
+
|
|
3
|
+
export default function useOnItemHeightChange({
|
|
4
|
+
items,
|
|
5
|
+
virtualScroller
|
|
6
|
+
}) {
|
|
7
|
+
// Only compute the initial cache value once.
|
|
8
|
+
const initialCacheValue = useMemo(() => {
|
|
9
|
+
return new Array(items.length)
|
|
10
|
+
}, [])
|
|
11
|
+
|
|
12
|
+
// Handler functions cache.
|
|
13
|
+
const cache = useRef(initialCacheValue)
|
|
14
|
+
|
|
15
|
+
// Caches per-item `onItemHeightChange` functions' "references"
|
|
16
|
+
// so that item components don't get re-rendered needlessly.
|
|
17
|
+
const getOnItemHeightChange = useCallback((i) => {
|
|
18
|
+
if (!cache.current[i]) {
|
|
19
|
+
cache.current[i] = () => virtualScroller.onItemHeightChange(i)
|
|
20
|
+
}
|
|
21
|
+
return cache.current[i]
|
|
22
|
+
}, [
|
|
23
|
+
virtualScroller,
|
|
24
|
+
cache
|
|
25
|
+
])
|
|
26
|
+
|
|
27
|
+
return getOnItemHeightChange
|
|
28
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useMemo, useRef, useCallback } from 'react'
|
|
2
|
+
|
|
3
|
+
export default function useOnItemStateChange({
|
|
4
|
+
items,
|
|
5
|
+
virtualScroller
|
|
6
|
+
}) {
|
|
7
|
+
// Only compute the initial cache value once.
|
|
8
|
+
const initialCacheValue = useMemo(() => {
|
|
9
|
+
return new Array(items.length)
|
|
10
|
+
}, [])
|
|
11
|
+
|
|
12
|
+
// Handler functions cache.
|
|
13
|
+
const cache = useRef(initialCacheValue)
|
|
14
|
+
|
|
15
|
+
// Caches per-item `onItemStateChange` functions' "references"
|
|
16
|
+
// so that item components don't get re-rendered needlessly.
|
|
17
|
+
const getOnItemStateChange = useCallback((i) => {
|
|
18
|
+
if (!cache.current[i]) {
|
|
19
|
+
cache.current[i] = (itemState) => virtualScroller.onItemStateChange(i, itemState)
|
|
20
|
+
}
|
|
21
|
+
return cache.current[i]
|
|
22
|
+
}, [
|
|
23
|
+
virtualScroller,
|
|
24
|
+
cache
|
|
25
|
+
])
|
|
26
|
+
|
|
27
|
+
return getOnItemStateChange
|
|
28
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { useState, useRef, useLayoutEffect } from 'react'
|
|
2
|
+
|
|
3
|
+
// Creates state management functions.
|
|
4
|
+
export default function _useState({ initialState, onRender, items }) {
|
|
5
|
+
// `VirtualScroller` state.
|
|
6
|
+
//
|
|
7
|
+
// The `_stateUpdate` variable shouldn't be used directly
|
|
8
|
+
// because in some cases its value may not represent
|
|
9
|
+
// the actual `state` of the `VirtualScroller`.
|
|
10
|
+
//
|
|
11
|
+
// * It will contain an incorrect initial value if `initialState` property is passed
|
|
12
|
+
// because it doesn't get initialized to `initialState`.
|
|
13
|
+
//
|
|
14
|
+
// * If `items` property gets changed, `state` reference variable gets updated immediately
|
|
15
|
+
// but the `_stateUpdate` variable here doesn't (until the component re-renders some other time).
|
|
16
|
+
//
|
|
17
|
+
// Instead, use the `state` reference below.
|
|
18
|
+
//
|
|
19
|
+
const [_stateUpdate, _setStateUpdate] = useState()
|
|
20
|
+
|
|
21
|
+
// This `state` reference is used for accessing the externally stored
|
|
22
|
+
// virtual scroller state from inside a `VirtualScroller` instance.
|
|
23
|
+
//
|
|
24
|
+
// It's also the "source of truth" on the actual `VirtualScroller` state.
|
|
25
|
+
//
|
|
26
|
+
const state = useRef(initialState)
|
|
27
|
+
|
|
28
|
+
// Accumulates state updates until they have been applied.
|
|
29
|
+
const targetState = useRef(initialState)
|
|
30
|
+
|
|
31
|
+
// Update the current state reference.
|
|
32
|
+
//
|
|
33
|
+
// Ignores the cases when `state` reference has already been updated
|
|
34
|
+
// "immediately" bypassing a `_setStateUpdate()` call, because
|
|
35
|
+
// in that case, `_stateUpdate` holds a stale value.
|
|
36
|
+
//
|
|
37
|
+
if (state.current !== targetState.current) {
|
|
38
|
+
state.current = _stateUpdate
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Call `onRender()` right after every state update.
|
|
42
|
+
//
|
|
43
|
+
// When `items` property changes, `useHandleItemsChange()` hook doesn't call
|
|
44
|
+
// `_setStateUpdate()` because there's no need for a re-render.
|
|
45
|
+
// But chaning `items` still does trigger a `VirtualScroller` state update,
|
|
46
|
+
// so added `items` property in the list of this "effect"'s dependencies.
|
|
47
|
+
//
|
|
48
|
+
useLayoutEffect(() => {
|
|
49
|
+
onRender()
|
|
50
|
+
}, [
|
|
51
|
+
_stateUpdate,
|
|
52
|
+
items
|
|
53
|
+
])
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
getState: () => state.current,
|
|
57
|
+
|
|
58
|
+
// Updates existing state.
|
|
59
|
+
//
|
|
60
|
+
// State updates are incremental meaning that this code should mimick
|
|
61
|
+
// the classic `React.Component`'s `this.setState()` behavior
|
|
62
|
+
// when calling `this.setState()` doesn't replace `state` but rather merges
|
|
63
|
+
// a set of the updated state properties with the rest of the old ones.
|
|
64
|
+
//
|
|
65
|
+
// The reason is that `useState()` updates are "asynchronous" (not immediate),
|
|
66
|
+
// and simply merging over `...state` would merge over potentially stale
|
|
67
|
+
// property values in cases when more than a single `updateState()` call is made
|
|
68
|
+
// before the state actually updates, resulting in losing some of the state updates.
|
|
69
|
+
//
|
|
70
|
+
// For example, the first `updateState()` call updates shown item indexes,
|
|
71
|
+
// and the second `updateState()` call updates `verticalSpacing`.
|
|
72
|
+
// If it was simply `updateState({ ...state, ...stateUpdate })`
|
|
73
|
+
// then the second state update could overwrite the first state update,
|
|
74
|
+
// resulting in incorrect items being shown/hidden.
|
|
75
|
+
//
|
|
76
|
+
// Using `...state.current` instead of `...pendingState.current` here
|
|
77
|
+
// would produce "stale" results.
|
|
78
|
+
//
|
|
79
|
+
updateState: (stateUpdate) => {
|
|
80
|
+
const newState = {
|
|
81
|
+
...targetState.current,
|
|
82
|
+
...stateUpdate
|
|
83
|
+
}
|
|
84
|
+
targetState.current = newState
|
|
85
|
+
// If `items` property did change the component detects it at render time
|
|
86
|
+
// and updates `VirtualScroller` items immediately by calling `.setItems()`.
|
|
87
|
+
// But, since all of that happens at render time and not in an "effect",
|
|
88
|
+
// if the state update was done as usual by calling `_setStateUpdate()`,
|
|
89
|
+
// React would throw an error about updating state during render.
|
|
90
|
+
// Hence, state update in that particular case should happen "directly",
|
|
91
|
+
// without waiting for an "asynchronous" effect to trigger and call
|
|
92
|
+
// an "asyncronous" `_setStateUpdate()` function.
|
|
93
|
+
//
|
|
94
|
+
// Updating state directly in that particular case works because there
|
|
95
|
+
// already is a render ongoing, so there's no need to re-render the component
|
|
96
|
+
// again after such render-time state update.
|
|
97
|
+
//
|
|
98
|
+
// When the initial `VirtualScroller` state is being set, it contains an `.items`
|
|
99
|
+
// property too, but that initial setting is done using another function called
|
|
100
|
+
// `setInitialState()`, so using `if (stateUpdate.items)` condition here for describing
|
|
101
|
+
// just the case when `state` has been updated as a result of a `setItems()` call
|
|
102
|
+
// seems to be fine.
|
|
103
|
+
//
|
|
104
|
+
if (stateUpdate.items) {
|
|
105
|
+
// If a `stateUpdate` contains `items` then it means that there was a `setItems()` call.
|
|
106
|
+
// No need to trigger a re-render — the component got re-rendered anyway.
|
|
107
|
+
// Just update the `state` "in place".
|
|
108
|
+
state.current = newState
|
|
109
|
+
} else {
|
|
110
|
+
_setStateUpdate(newState)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import px from '../utility/px.js'
|
|
2
|
+
|
|
3
|
+
export default function useStyle({
|
|
4
|
+
tbody,
|
|
5
|
+
virtualScroller
|
|
6
|
+
}) {
|
|
7
|
+
if (tbody) {
|
|
8
|
+
return
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const {
|
|
12
|
+
beforeItemsHeight,
|
|
13
|
+
afterItemsHeight
|
|
14
|
+
} = virtualScroller.getState()
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
paddingTop: px(beforeItemsHeight),
|
|
18
|
+
paddingBottom: px(afterItemsHeight)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
|
|
3
|
+
import VirtualScroller from '../VirtualScroller.js'
|
|
4
|
+
|
|
5
|
+
// Creates a `VirtualScroller` instance.
|
|
6
|
+
export default function useVirtualScroller({
|
|
7
|
+
items,
|
|
8
|
+
estimatedItemHeight,
|
|
9
|
+
bypass,
|
|
10
|
+
// bypassBatchSize,
|
|
11
|
+
tbody,
|
|
12
|
+
onItemInitialRender,
|
|
13
|
+
// `onItemFirstRender(i)` is deprecated, use `onItemInitialRender(item)` instead.
|
|
14
|
+
onItemFirstRender,
|
|
15
|
+
initialScrollPosition,
|
|
16
|
+
onScrollPositionChange,
|
|
17
|
+
measureItemsBatchSize,
|
|
18
|
+
// `scrollableContainer` property is deprecated.
|
|
19
|
+
// Use `getScrollableContainer()` property instead.
|
|
20
|
+
scrollableContainer,
|
|
21
|
+
getScrollableContainer,
|
|
22
|
+
getColumnsCount,
|
|
23
|
+
getItemId,
|
|
24
|
+
AsComponent,
|
|
25
|
+
initialState,
|
|
26
|
+
onStateChange
|
|
27
|
+
}, {
|
|
28
|
+
container
|
|
29
|
+
}) {
|
|
30
|
+
return useMemo(() => {
|
|
31
|
+
// Create `virtual-scroller` instance.
|
|
32
|
+
return new VirtualScroller(
|
|
33
|
+
() => container.current,
|
|
34
|
+
items,
|
|
35
|
+
{
|
|
36
|
+
_useTimeoutInRenderLoop: true,
|
|
37
|
+
estimatedItemHeight,
|
|
38
|
+
bypass,
|
|
39
|
+
// bypassBatchSize,
|
|
40
|
+
tbody,
|
|
41
|
+
onItemInitialRender,
|
|
42
|
+
// `onItemFirstRender(i)` is deprecated, use `onItemInitialRender(item)` instead.
|
|
43
|
+
onItemFirstRender,
|
|
44
|
+
initialScrollPosition,
|
|
45
|
+
onScrollPositionChange,
|
|
46
|
+
measureItemsBatchSize,
|
|
47
|
+
// `scrollableContainer` property is deprecated.
|
|
48
|
+
// Use `getScrollableContainer()` property instead.
|
|
49
|
+
scrollableContainer,
|
|
50
|
+
getScrollableContainer,
|
|
51
|
+
getColumnsCount,
|
|
52
|
+
getItemId,
|
|
53
|
+
tbody: AsComponent === 'tbody',
|
|
54
|
+
state: initialState,
|
|
55
|
+
onStateChange
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
}, [])
|
|
59
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { useLayoutEffect } from 'react'
|
|
2
|
+
|
|
3
|
+
export default function useVirtualScrollerStartStop(virtualScroller) {
|
|
4
|
+
useLayoutEffect(() => {
|
|
5
|
+
// Start listening to scroll events.
|
|
6
|
+
virtualScroller.start()
|
|
7
|
+
return () => {
|
|
8
|
+
// Stop listening to scroll events.
|
|
9
|
+
virtualScroller.stop()
|
|
10
|
+
}
|
|
11
|
+
}, [])
|
|
12
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import ItemsContainer from './ItemsContainer.js'
|
|
2
|
+
import ScrollableContainer from './ScrollableContainer.js'
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
createItemsContainer(getItemsContainerElement) {
|
|
6
|
+
return new ItemsContainer(getItemsContainerElement)
|
|
7
|
+
},
|
|
8
|
+
createScrollableContainer(getScrollableContainerElement, getItemsContainerElement) {
|
|
9
|
+
return new ScrollableContainer(getScrollableContainerElement, getItemsContainerElement)
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export default class ItemsContainer {
|
|
2
|
+
/**
|
|
3
|
+
* Constructs a new "container" from an element.
|
|
4
|
+
* @param {function} getElement
|
|
5
|
+
*/
|
|
6
|
+
constructor(getElement) {
|
|
7
|
+
this.getElement = getElement
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Returns an item element's "top offset", relative to the items `container`'s top edge.
|
|
12
|
+
* @param {number} renderedElementIndex — An index of an item relative to the "first shown item index". For example, if the list is showing items from index 8 to index 12 then `renderedElementIndex = 0` would mean the item at index `8`.
|
|
13
|
+
* @return {number}
|
|
14
|
+
*/
|
|
15
|
+
getNthRenderedItemTopOffset(renderedElementIndex) {
|
|
16
|
+
const children = this.getElement().children
|
|
17
|
+
const maxWidth = this.getElement().width
|
|
18
|
+
let topOffset = this.getElement().paddingTop
|
|
19
|
+
let rowWidth
|
|
20
|
+
let rowHeight
|
|
21
|
+
let startNewRow = true
|
|
22
|
+
let i = 0
|
|
23
|
+
while (i <= renderedElementIndex) {
|
|
24
|
+
if (startNewRow || rowWidth + children[i].width > maxWidth) {
|
|
25
|
+
if (i > 0) {
|
|
26
|
+
topOffset += rowHeight
|
|
27
|
+
topOffset += children[i].marginTop
|
|
28
|
+
}
|
|
29
|
+
rowWidth = children[i].width
|
|
30
|
+
rowHeight = children[i].height
|
|
31
|
+
if (rowWidth > maxWidth) {
|
|
32
|
+
startNewRow = true
|
|
33
|
+
} else {
|
|
34
|
+
startNewRow = false
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
rowWidth += children[i].width
|
|
38
|
+
rowHeight = Math.max(rowHeight, children[i].height)
|
|
39
|
+
}
|
|
40
|
+
i++
|
|
41
|
+
}
|
|
42
|
+
return topOffset
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Returns an item element's height.
|
|
47
|
+
* @param {number} renderedElementIndex — An index of an item relative to the "first shown item index". For example, if the list is showing items from index 8 to index 12 then `renderedElementIndex = 0` would mean the item at index `8`.
|
|
48
|
+
* @return {number}
|
|
49
|
+
*/
|
|
50
|
+
getNthRenderedItemHeight(renderedElementIndex) {
|
|
51
|
+
return this.getElement().children[renderedElementIndex].height
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Returns items container height.
|
|
56
|
+
* @return {number}
|
|
57
|
+
*/
|
|
58
|
+
getHeight() {
|
|
59
|
+
const children = this.getElement().children
|
|
60
|
+
const maxWidth = this.getElement().width
|
|
61
|
+
let contentHeight = this.getElement().paddingTop
|
|
62
|
+
let i = 0
|
|
63
|
+
while (i < children.length) {
|
|
64
|
+
let rowWidth = 0
|
|
65
|
+
let rowHeight = 0
|
|
66
|
+
while (rowWidth <= maxWidth && i < children.length) {
|
|
67
|
+
if (rowWidth === 0 && i > 0) {
|
|
68
|
+
const verticalSpacing = children[i].marginTop
|
|
69
|
+
contentHeight += verticalSpacing
|
|
70
|
+
}
|
|
71
|
+
rowWidth += children[i].width
|
|
72
|
+
rowHeight = Math.max(rowHeight, children[i].height)
|
|
73
|
+
i++
|
|
74
|
+
}
|
|
75
|
+
contentHeight += rowHeight
|
|
76
|
+
}
|
|
77
|
+
contentHeight += this.getElement().paddingBottom
|
|
78
|
+
return contentHeight
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Removes all item elements of an items container.
|
|
83
|
+
*/
|
|
84
|
+
clear() {
|
|
85
|
+
this.getElement().children = []
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
export default class ScrollableContainer {
|
|
2
|
+
/**
|
|
3
|
+
* Constructs a new "scrollable container" from an element.
|
|
4
|
+
* @param {func} getElement — Returns the scrollable container element.
|
|
5
|
+
* @param {func} getItemsContainerElement — Returns items "container" element.
|
|
6
|
+
*/
|
|
7
|
+
constructor(getElement, getItemsContainerElement) {
|
|
8
|
+
this.getElement = getElement
|
|
9
|
+
this.getItemsContainerElement = getItemsContainerElement
|
|
10
|
+
this.scrollTop = 0
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Returns the current scroll position.
|
|
15
|
+
* @return {number}
|
|
16
|
+
*/
|
|
17
|
+
getScrollY() {
|
|
18
|
+
return this.scrollTop
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Scrolls to a specific position.
|
|
23
|
+
* @param {number} scrollY
|
|
24
|
+
*/
|
|
25
|
+
scrollToY(scrollY) {
|
|
26
|
+
this.scrollTop = scrollY
|
|
27
|
+
if (this.onScrollListener) {
|
|
28
|
+
this.onScrollListener()
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Returns "scrollable container" width,
|
|
34
|
+
* i.e. the available width for its content.
|
|
35
|
+
* @return {number}
|
|
36
|
+
*/
|
|
37
|
+
getWidth() {
|
|
38
|
+
return this.getElement().width
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Returns the height of the "scrollable container" itself.
|
|
43
|
+
* Not to be confused with the height of "scrollable container"'s content.
|
|
44
|
+
* @return {number}
|
|
45
|
+
*/
|
|
46
|
+
getHeight() {
|
|
47
|
+
return this.getElement().height
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Returns a "top offset" of an items container element
|
|
52
|
+
* relative to the "scrollable container"'s top edge.
|
|
53
|
+
* @return {number}
|
|
54
|
+
*/
|
|
55
|
+
getItemsContainerTopOffset() {
|
|
56
|
+
return 0
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Adds a "scroll" event listener to the "scrollable container".
|
|
61
|
+
* @param {onScroll} Should be called whenever the scroll position inside the "scrollable container" (potentially) changes.
|
|
62
|
+
* @return {function} Returns a function that stops listening.
|
|
63
|
+
*/
|
|
64
|
+
onScroll(onScroll) {
|
|
65
|
+
this.onScrollListener = onScroll
|
|
66
|
+
return () => {
|
|
67
|
+
delete this.onScrollListener
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Adds a "resize" event listener to the "scrollable container".
|
|
73
|
+
* @param {onResize} Should be called whenever the "scrollable container"'s width or height (potentially) changes.
|
|
74
|
+
* @return {function} Returns a function that stops listening.
|
|
75
|
+
*/
|
|
76
|
+
onResize(onResize) {
|
|
77
|
+
this.onResizeListener = onResize
|
|
78
|
+
return () => {
|
|
79
|
+
delete this.onResizeListener
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Returns a `Promise` because `this.onResizeListener()` is a "debounced" function.
|
|
84
|
+
// See `./utility/debounce.js` for more details.
|
|
85
|
+
_triggerResizeListener() {
|
|
86
|
+
return this.onResizeListener()
|
|
87
|
+
}
|
|
88
|
+
}
|