virtual-scroller 1.7.7 → 1.8.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.
- package/.gitlab-ci.yml +1 -1
- package/CHANGELOG.md +24 -1
- package/README.md +139 -33
- package/babel.config.js +25 -0
- package/babel.js +5 -0
- package/bundle/index-bypass.html +1 -1
- package/bundle/index-dom.html +1 -1
- package/bundle/index-grid.html +1 -2
- package/bundle/index-scrollableContainer.html +1 -1
- package/bundle/index-tbody-scrollableContainer.html +2 -0
- package/bundle/index-tbody.html +2 -0
- 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 +319 -0
- package/commonjs/BeforeResize.js.map +1 -0
- package/commonjs/DOM/Engine.js +46 -0
- package/commonjs/DOM/Engine.js.map +1 -0
- package/commonjs/DOM/ItemsContainer.js +78 -0
- package/commonjs/DOM/ItemsContainer.js.map +1 -0
- package/commonjs/DOM/{WaitForStylesToLoad.js → ListTopOffsetWatcher.js} +56 -35
- package/commonjs/DOM/ListTopOffsetWatcher.js.map +1 -0
- package/commonjs/DOM/ScrollableContainer.js +56 -81
- package/commonjs/DOM/ScrollableContainer.js.map +1 -1
- package/commonjs/DOM/VirtualScroller.js +20 -15
- package/commonjs/DOM/VirtualScroller.js.map +1 -1
- package/commonjs/DOM/tbody.js +2 -2
- package/commonjs/ItemHeights.js +22 -29
- package/commonjs/ItemHeights.js.map +1 -1
- package/commonjs/Layout.js +588 -215
- package/commonjs/Layout.js.map +1 -1
- package/commonjs/Layout.test.js +191 -0
- package/commonjs/Layout.test.js.map +1 -0
- package/commonjs/ListHeightChangeWatcher.js +126 -0
- package/commonjs/ListHeightChangeWatcher.js.map +1 -0
- package/commonjs/Resize.js +22 -21
- package/commonjs/Resize.js.map +1 -1
- package/commonjs/Scroll.js +148 -88
- package/commonjs/Scroll.js.map +1 -1
- package/commonjs/VirtualScroller.js +1269 -390
- package/commonjs/VirtualScroller.js.map +1 -1
- package/commonjs/getItemCoordinates.js.map +1 -1
- package/commonjs/getItemsDiff.js.map +1 -1
- package/commonjs/getVerticalSpacing.js +8 -8
- package/commonjs/getVerticalSpacing.js.map +1 -1
- package/commonjs/react/VirtualScroller.js +31 -37
- package/commonjs/react/VirtualScroller.js.map +1 -1
- package/commonjs/utility/debounce.js +26 -4
- package/commonjs/utility/debounce.js.map +1 -1
- package/commonjs/utility/debug.js +51 -12
- package/commonjs/utility/debug.js.map +1 -1
- package/commonjs/utility/getStateSnapshot.js +50 -0
- package/commonjs/utility/getStateSnapshot.js.map +1 -0
- package/commonjs/utility/px.js +1 -1
- package/commonjs/utility/px.js.map +1 -1
- package/commonjs/utility/px.test.js +14 -0
- package/commonjs/utility/px.test.js.map +1 -0
- 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.d.ts +23 -0
- package/index.d.ts +84 -0
- package/modules/BeforeResize.js +310 -0
- package/modules/BeforeResize.js.map +1 -0
- package/modules/DOM/Engine.js +27 -0
- package/modules/DOM/Engine.js.map +1 -0
- package/modules/DOM/ItemsContainer.js +71 -0
- package/modules/DOM/ItemsContainer.js.map +1 -0
- package/modules/DOM/{WaitForStylesToLoad.js → ListTopOffsetWatcher.js} +57 -35
- package/modules/DOM/ListTopOffsetWatcher.js.map +1 -0
- package/modules/DOM/ScrollableContainer.js +55 -80
- package/modules/DOM/ScrollableContainer.js.map +1 -1
- package/modules/DOM/VirtualScroller.js +15 -14
- package/modules/DOM/VirtualScroller.js.map +1 -1
- package/modules/ItemHeights.js +17 -28
- package/modules/ItemHeights.js.map +1 -1
- package/modules/Layout.js +582 -213
- package/modules/Layout.js.map +1 -1
- package/modules/Layout.test.js +185 -0
- package/modules/Layout.test.js.map +1 -0
- package/modules/ListHeightChangeWatcher.js +119 -0
- package/modules/ListHeightChangeWatcher.js.map +1 -0
- package/modules/Resize.js +21 -20
- package/modules/Resize.js.map +1 -1
- package/modules/Scroll.js +148 -87
- package/modules/Scroll.js.map +1 -1
- package/modules/VirtualScroller.js +1263 -390
- package/modules/VirtualScroller.js.map +1 -1
- package/modules/getItemCoordinates.js.map +1 -1
- package/modules/getItemsDiff.js.map +1 -1
- package/modules/getVerticalSpacing.js +8 -8
- package/modules/getVerticalSpacing.js.map +1 -1
- package/modules/react/VirtualScroller.js +31 -37
- package/modules/react/VirtualScroller.js.map +1 -1
- package/modules/utility/debounce.js +26 -4
- package/modules/utility/debounce.js.map +1 -1
- package/modules/utility/debug.js +47 -10
- package/modules/utility/debug.js.map +1 -1
- package/modules/utility/getStateSnapshot.js +43 -0
- package/modules/utility/getStateSnapshot.js.map +1 -0
- package/modules/utility/px.js +1 -1
- package/modules/utility/px.js.map +1 -1
- package/modules/utility/px.test.js +9 -0
- package/modules/utility/px.test.js.map +1 -0
- 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 +24 -22
- package/react/index.d.ts +27 -0
- package/source/BeforeResize.js +317 -0
- package/source/DOM/Engine.js +32 -0
- package/source/DOM/ItemsContainer.js +48 -0
- package/source/DOM/{WaitForStylesToLoad.js → ListTopOffsetWatcher.js} +48 -22
- package/source/DOM/ScrollableContainer.js +39 -56
- package/source/DOM/VirtualScroller.js +6 -7
- package/source/ItemHeights.js +19 -24
- package/source/Layout.js +626 -252
- package/source/Layout.test.js +171 -0
- package/source/ListHeightChangeWatcher.js +94 -0
- package/source/Resize.js +23 -15
- package/source/Scroll.js +139 -78
- package/source/VirtualScroller.js +1243 -286
- package/source/getVerticalSpacing.js +7 -7
- package/source/react/VirtualScroller.js +2 -18
- package/source/utility/debounce.js +20 -3
- package/source/utility/debug.js +34 -3
- package/source/utility/getStateSnapshot.js +36 -0
- package/source/utility/px.js +1 -1
- package/source/utility/px.test.js +9 -0
- package/website/index-bypass.html +195 -0
- package/website/index-grid.html +0 -1
- package/website/index-scrollableContainer.html +208 -0
- package/website/index-tbody-scrollableContainer.html +68 -0
- package/website/index-tbody.html +55 -0
- package/commonjs/DOM/RenderingEngine.js +0 -33
- package/commonjs/DOM/RenderingEngine.js.map +0 -1
- package/commonjs/DOM/Screen.js +0 -87
- package/commonjs/DOM/Screen.js.map +0 -1
- package/commonjs/DOM/WaitForStylesToLoad.js.map +0 -1
- package/commonjs/RestoreScroll.js +0 -118
- package/commonjs/RestoreScroll.js.map +0 -1
- package/modules/DOM/RenderingEngine.js +0 -19
- package/modules/DOM/RenderingEngine.js.map +0 -1
- package/modules/DOM/Screen.js +0 -80
- package/modules/DOM/Screen.js.map +0 -1
- package/modules/DOM/WaitForStylesToLoad.js.map +0 -1
- package/modules/RestoreScroll.js +0 -111
- package/modules/RestoreScroll.js.map +0 -1
- package/source/DOM/RenderingEngine.js +0 -22
- package/source/DOM/Screen.js +0 -51
- package/source/RestoreScroll.js +0 -86
|
@@ -4,47 +4,72 @@
|
|
|
4
4
|
// https://github.com/bvaughn/react-virtualized/issues/722
|
|
5
5
|
import { setTimeout, clearTimeout } from 'request-animation-frame-timeout'
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
// Refreshing two times every seconds seems reasonable.
|
|
8
|
+
const WATCH_LIST_TOP_OFFSET_INTERVAL = 500
|
|
9
|
+
|
|
10
|
+
// Refreshing for 3 seconds after the initial page load seems reasonable.
|
|
11
|
+
const WATCH_LIST_TOP_OFFSET_MAX_DURATION = 3000
|
|
8
12
|
|
|
9
13
|
// `VirtualScroller` calls `this.layout.layOut()` on mount,
|
|
10
14
|
// but if the page styles are applied after `VirtualScroller` mounts
|
|
11
15
|
// (for example, if styles are applied via javascript, like Webpack does)
|
|
12
|
-
// then the list might not render correctly and will only show the first item.
|
|
13
|
-
// The reason
|
|
14
|
-
//
|
|
16
|
+
// then the list might not render correctly and it will only show the first item.
|
|
17
|
+
// The reason is that in that case calling `.getListTopOffset()` on mount
|
|
18
|
+
// returns "incorrect" `top` position because the styles haven't been applied yet.
|
|
19
|
+
//
|
|
15
20
|
// For example, consider a page:
|
|
16
21
|
// <div class="page">
|
|
17
22
|
// <nav class="sidebar">...</nav>
|
|
18
23
|
// <main>...</main>
|
|
19
24
|
// </div>
|
|
25
|
+
//
|
|
20
26
|
// The sidebar is styled as `position: fixed`, but until
|
|
21
27
|
// the page styles have been applied it's gonna be a regular `<div/>`
|
|
22
28
|
// meaning that `<main/>` will be rendered below the sidebar
|
|
23
29
|
// and will appear offscreen and so it will only render the first item.
|
|
30
|
+
//
|
|
24
31
|
// Then, the page styles are loaded and applied and the sidebar
|
|
25
32
|
// is now `position: fixed` so `<main/>` is now rendered at the top of the page
|
|
26
33
|
// but `VirtualScroller`'s `.render()` has already been called
|
|
27
34
|
// and it won't re-render until the user scrolls or the window is resized.
|
|
28
|
-
//
|
|
35
|
+
//
|
|
36
|
+
// This type of a bug doesn't seem to occur in production, but it can appear
|
|
29
37
|
// in development mode when using Webpack. The workaround `VirtualScroller`
|
|
30
|
-
// implements for such cases is calling `.
|
|
38
|
+
// implements for such cases is calling `.getListTopOffset()`
|
|
31
39
|
// on the list container DOM element periodically (every second) to check
|
|
32
40
|
// if the `top` coordinate has changed as a result of CSS being applied:
|
|
33
41
|
// if it has then it recalculates the shown item indexes.
|
|
34
|
-
|
|
42
|
+
//
|
|
43
|
+
// Maybe this bug could occur in production when using Webpack chunks.
|
|
44
|
+
// That depends on how a style of a chunk is added to the page:
|
|
45
|
+
// if it's added via `javascript` after the page has been rendered
|
|
46
|
+
// then this workaround will also work for that case.
|
|
47
|
+
//
|
|
48
|
+
// Another example would be a page having a really tall expanded "accordion"
|
|
49
|
+
// section, below which a `VirtualScroller` list resides. If the user un-expands
|
|
50
|
+
// such expanded "accordion" section, the list would become visible but
|
|
51
|
+
// it wouldn't get re-rendered because no `scroll` event has occured,
|
|
52
|
+
// and the list only re-renders automatically on `scroll` events.
|
|
53
|
+
// To work around such cases, call `virtualScroller.updateLayout()` method manually.
|
|
54
|
+
// The workaround below could be extended to refresh the list's top coordinate
|
|
55
|
+
// indefinitely and at higher intervals, but why waste CPU time on that.
|
|
56
|
+
// There doesn't seem to be any DOM API for tracking an element's top position.
|
|
57
|
+
// There is `IntersectionObserver` API but it doesn't exactly do that.
|
|
58
|
+
//
|
|
59
|
+
export default class ListTopOffsetWatcher {
|
|
35
60
|
constructor({
|
|
36
|
-
|
|
37
|
-
|
|
61
|
+
getListTopOffset,
|
|
62
|
+
onListTopOffsetChange
|
|
38
63
|
}) {
|
|
39
|
-
this.
|
|
40
|
-
this.
|
|
64
|
+
this.getListTopOffset = getListTopOffset
|
|
65
|
+
this.onListTopOffsetChange = onListTopOffsetChange
|
|
41
66
|
}
|
|
42
67
|
|
|
43
|
-
|
|
68
|
+
onListTopOffset(listTopOffset) {
|
|
44
69
|
if (this.listTopOffsetInsideScrollableContainer === undefined) {
|
|
45
70
|
// Start periodical checks of the list's top offset
|
|
46
71
|
// in order to perform a re-layout in case it changes.
|
|
47
|
-
// See the comments in `
|
|
72
|
+
// See the comments in `ListTopOffsetWatcher.js` file
|
|
48
73
|
// on why can the list's top offset change, and in which circumstances.
|
|
49
74
|
this.start()
|
|
50
75
|
}
|
|
@@ -58,7 +83,11 @@ export default class WaitForStylesToLoad {
|
|
|
58
83
|
|
|
59
84
|
stop() {
|
|
60
85
|
this.isRendered = false
|
|
61
|
-
|
|
86
|
+
|
|
87
|
+
if (this.watchListTopOffsetTimer) {
|
|
88
|
+
clearTimeout(this.watchListTopOffsetTimer)
|
|
89
|
+
this.watchListTopOffsetTimer = undefined
|
|
90
|
+
}
|
|
62
91
|
}
|
|
63
92
|
|
|
64
93
|
watchListTopOffset() {
|
|
@@ -72,11 +101,11 @@ export default class WaitForStylesToLoad {
|
|
|
72
101
|
// Skip comparing `top` coordinate of the list
|
|
73
102
|
// when this function is called for the first time.
|
|
74
103
|
if (this.listTopOffsetInsideScrollableContainer !== undefined) {
|
|
75
|
-
// Calling `this.
|
|
76
|
-
//
|
|
104
|
+
// Calling `this.getListTopOffset()` on an element
|
|
105
|
+
// runs about 0.003 milliseconds on a modern desktop CPU,
|
|
77
106
|
// so I guess it's fine calling it twice a second.
|
|
78
|
-
if (this.
|
|
79
|
-
this.
|
|
107
|
+
if (this.getListTopOffset() !== this.listTopOffsetInsideScrollableContainer) {
|
|
108
|
+
this.onListTopOffsetChange()
|
|
80
109
|
}
|
|
81
110
|
}
|
|
82
111
|
// Compare `top` coordinate of the list twice a second
|
|
@@ -93,7 +122,4 @@ export default class WaitForStylesToLoad {
|
|
|
93
122
|
// Run the cycle.
|
|
94
123
|
check()
|
|
95
124
|
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const WATCH_LIST_TOP_OFFSET_INTERVAL = 500
|
|
99
|
-
const WATCH_LIST_TOP_OFFSET_MAX_DURATION = 3000
|
|
125
|
+
}
|
|
@@ -2,9 +2,11 @@ export default class ScrollableContainer {
|
|
|
2
2
|
/**
|
|
3
3
|
* Constructs a new "scrollable container" from an element.
|
|
4
4
|
* @param {Element} scrollableContainer
|
|
5
|
+
* @param {func} getItemsContainerElement — Returns items "container" element.
|
|
5
6
|
*/
|
|
6
|
-
constructor(element) {
|
|
7
|
+
constructor(element, getItemsContainerElement) {
|
|
7
8
|
this.element = element
|
|
9
|
+
this.getItemsContainerElement = getItemsContainerElement
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
/**
|
|
@@ -20,7 +22,14 @@ export default class ScrollableContainer {
|
|
|
20
22
|
* @param {number} scrollY
|
|
21
23
|
*/
|
|
22
24
|
scrollToY(scrollY) {
|
|
23
|
-
|
|
25
|
+
// IE 11 doesn't seem to have a `.scrollTo()` method.
|
|
26
|
+
// https://gitlab.com/catamphetamine/virtual-scroller/-/issues/10
|
|
27
|
+
// https://stackoverflow.com/questions/39908825/window-scrollto-is-not-working-in-internet-explorer-11
|
|
28
|
+
if (this.element.scrollTo) {
|
|
29
|
+
this.element.scrollTo(0, scrollY)
|
|
30
|
+
} else {
|
|
31
|
+
this.element.scrollTop = scrollY
|
|
32
|
+
}
|
|
24
33
|
}
|
|
25
34
|
|
|
26
35
|
/**
|
|
@@ -45,30 +54,15 @@ export default class ScrollableContainer {
|
|
|
45
54
|
}
|
|
46
55
|
|
|
47
56
|
/**
|
|
48
|
-
* Returns
|
|
49
|
-
* For example, a scrollable container can have a height of 500px,
|
|
50
|
-
* but the content in it could have a height of 5000px,
|
|
51
|
-
* in which case a vertical scrollbar is rendered, and only
|
|
52
|
-
* one-tenth of all the items are shown at any given moment.
|
|
53
|
-
* This function is currently only used when using the
|
|
54
|
-
* `preserveScrollPositionOfTheBottomOfTheListOnMount` feature.
|
|
55
|
-
* @return {number}
|
|
56
|
-
*/
|
|
57
|
-
getContentHeight() {
|
|
58
|
-
return this.element.scrollHeight
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Returns a "top offset" of an element
|
|
57
|
+
* Returns a "top offset" of an items container element
|
|
63
58
|
* relative to the "scrollable container"'s top edge.
|
|
64
|
-
* @param {Element} element
|
|
65
59
|
* @return {number}
|
|
66
60
|
*/
|
|
67
|
-
|
|
61
|
+
getItemsContainerTopOffset() {
|
|
68
62
|
const scrollableContainerTop = this.element.getBoundingClientRect().top
|
|
69
63
|
const scrollableContainerBorderTopWidth = this.element.clientTop
|
|
70
|
-
const
|
|
71
|
-
return (
|
|
64
|
+
const itemsContainerTop = this.getItemsContainerElement().getBoundingClientRect().top
|
|
65
|
+
return (itemsContainerTop - scrollableContainerTop) + this.getScrollY() - scrollableContainerBorderTopWidth
|
|
72
66
|
}
|
|
73
67
|
|
|
74
68
|
// isVisible() {
|
|
@@ -81,7 +75,7 @@ export default class ScrollableContainer {
|
|
|
81
75
|
* @param {onScroll} Should be called whenever the scroll position inside the "scrollable container" (potentially) changes.
|
|
82
76
|
* @return {function} Returns a function that stops listening.
|
|
83
77
|
*/
|
|
84
|
-
|
|
78
|
+
onScroll(onScroll) {
|
|
85
79
|
this.element.addEventListener('scroll', onScroll)
|
|
86
80
|
return () => this.element.removeEventListener('scroll', onScroll)
|
|
87
81
|
}
|
|
@@ -89,13 +83,10 @@ export default class ScrollableContainer {
|
|
|
89
83
|
/**
|
|
90
84
|
* Adds a "resize" event listener to the "scrollable container".
|
|
91
85
|
* @param {onResize} Should be called whenever the "scrollable container"'s width or height (potentially) changes.
|
|
92
|
-
|
|
93
|
-
* @return {function} Returns a function that stops listening.
|
|
86
|
+
* @return {function} Returns a function that stops listening.
|
|
94
87
|
*/
|
|
95
|
-
onResize(onResize
|
|
96
|
-
//
|
|
97
|
-
// For now, `scrollableContainer` is supposed to have constant width and height.
|
|
98
|
-
// (unless window is resized).
|
|
88
|
+
onResize(onResize) {
|
|
89
|
+
// Watches "scrollable container"'s dimensions via a `ResizeObserver`.
|
|
99
90
|
// https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver
|
|
100
91
|
// https://web.dev/resize-observer/
|
|
101
92
|
let unobserve
|
|
@@ -122,7 +113,9 @@ export default class ScrollableContainer {
|
|
|
122
113
|
// hasn't changed since the previous time `onResize()` has been called,
|
|
123
114
|
// then `onResize()` doesn't do anything, so, I guess, there shouldn't be
|
|
124
115
|
// any "performance implications" of running the listener twice in such case.
|
|
125
|
-
const unlistenGlobalResize = addGlobalResizeListener(onResize, {
|
|
116
|
+
const unlistenGlobalResize = addGlobalResizeListener(onResize, {
|
|
117
|
+
itemsContainerElement: this.getItemsContainerElement()
|
|
118
|
+
})
|
|
126
119
|
return () => {
|
|
127
120
|
if (unobserve) {
|
|
128
121
|
unobserve()
|
|
@@ -133,8 +126,12 @@ export default class ScrollableContainer {
|
|
|
133
126
|
}
|
|
134
127
|
|
|
135
128
|
export class ScrollableWindowContainer extends ScrollableContainer {
|
|
136
|
-
|
|
137
|
-
|
|
129
|
+
/**
|
|
130
|
+
* Constructs a new window "scrollable container".
|
|
131
|
+
* @param {func} getItemsContainerElement — Returns items "container" element.
|
|
132
|
+
*/
|
|
133
|
+
constructor(getItemsContainerElement) {
|
|
134
|
+
super(window, getItemsContainerElement)
|
|
138
135
|
}
|
|
139
136
|
|
|
140
137
|
/**
|
|
@@ -182,38 +179,24 @@ export class ScrollableWindowContainer extends ScrollableContainer {
|
|
|
182
179
|
}
|
|
183
180
|
|
|
184
181
|
/**
|
|
185
|
-
* Returns
|
|
186
|
-
* For example, a scrollable container can have a height of 500px,
|
|
187
|
-
* but the content in it could have a height of 5000px,
|
|
188
|
-
* in which case a vertical scrollbar is rendered, and only
|
|
189
|
-
* one-tenth of all the items are shown at any given moment.
|
|
190
|
-
* This function is currently only used when using the
|
|
191
|
-
* `preserveScrollPositionOfTheBottomOfTheListOnMount` feature.
|
|
192
|
-
* @return {number}
|
|
193
|
-
*/
|
|
194
|
-
getContentHeight() {
|
|
195
|
-
return document.documentElement.scrollHeight
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Returns a "top offset" of an element
|
|
182
|
+
* Returns a "top offset" of an items container element
|
|
200
183
|
* relative to the "scrollable container"'s top edge.
|
|
201
|
-
* @param {Element} element
|
|
202
184
|
* @return {number}
|
|
203
185
|
*/
|
|
204
|
-
|
|
186
|
+
getItemsContainerTopOffset() {
|
|
205
187
|
const borderTopWidth = document.clientTop || document.body.clientTop || 0
|
|
206
|
-
return
|
|
188
|
+
return this.getItemsContainerElement().getBoundingClientRect().top + this.getScrollY() - borderTopWidth
|
|
207
189
|
}
|
|
208
190
|
|
|
209
191
|
/**
|
|
210
192
|
* Adds a "resize" event listener to the "scrollable container".
|
|
211
193
|
* @param {onScroll} Should be called whenever the "scrollable container"'s width or height (potentially) changes.
|
|
212
|
-
* @param {Element} options.container — The result of the `getContainerElement()` function that was passed in `VirtualScroller` constructor. For example, DOM renderer uses it to filter-out unrelated "resize" events.
|
|
213
194
|
* @return {function} Returns a function that stops listening.
|
|
214
195
|
*/
|
|
215
|
-
onResize(onResize
|
|
216
|
-
return addGlobalResizeListener(onResize, {
|
|
196
|
+
onResize(onResize) {
|
|
197
|
+
return addGlobalResizeListener(onResize, {
|
|
198
|
+
itemsContainerElement: this.getItemsContainerElement()
|
|
199
|
+
})
|
|
217
200
|
}
|
|
218
201
|
|
|
219
202
|
// isVisible() {
|
|
@@ -223,11 +206,11 @@ export class ScrollableWindowContainer extends ScrollableContainer {
|
|
|
223
206
|
|
|
224
207
|
/**
|
|
225
208
|
* Adds a "resize" event listener to the `window`.
|
|
226
|
-
* @param {onResize} Should be called whenever the "container"'s width or height (potentially) changes.
|
|
227
|
-
* @param {Element} options.
|
|
209
|
+
* @param {onResize} Should be called whenever the "scrollable container"'s width or height (potentially) changes.
|
|
210
|
+
* @param {Element} options.itemsContainerElement — The items "container" element, which is not the same as the "scrollable container" element. For example, "scrollable container" could be resized while the list element retaining its size. One such example is a user entering fullscreen mode on an HTML5 `<video/>` element: in that case, a "resize" event is triggered on a window, and window dimensions change to the user's screen size, but such "resize" event can be ignored because the list isn't visible until the user exits fullscreen mode.
|
|
228
211
|
* @return {function} Returns a function that stops listening.
|
|
229
212
|
*/
|
|
230
|
-
function addGlobalResizeListener(onResize, {
|
|
213
|
+
function addGlobalResizeListener(onResize, { itemsContainerElement }) {
|
|
231
214
|
const onResizeListener = () => {
|
|
232
215
|
// By default, `VirtualScroller` always performs a re-layout
|
|
233
216
|
// on window `resize` event. But browsers (Chrome, Firefox)
|
|
@@ -258,7 +241,7 @@ function addGlobalResizeListener(onResize, { container }) {
|
|
|
258
241
|
// the layout wouldn't be affected too, so such `resize` event should also be
|
|
259
242
|
// ignored: when `document.fullscreenElement` is inside the `container`.
|
|
260
243
|
//
|
|
261
|
-
if (document.fullscreenElement.contains(
|
|
244
|
+
if (document.fullscreenElement.contains(itemsContainerElement)) {
|
|
262
245
|
// The element is either the `container`'s ancestor,
|
|
263
246
|
// Or is the `container` itself.
|
|
264
247
|
// (`a.contains(b)` includes the `a === b` case).
|
|
@@ -4,8 +4,8 @@ import log, { warn } from '../utility/debug'
|
|
|
4
4
|
import px from '../utility/px'
|
|
5
5
|
|
|
6
6
|
export default class VirtualScroller {
|
|
7
|
-
constructor(
|
|
8
|
-
this.container =
|
|
7
|
+
constructor(itemsContainerElement, items, renderItem, options = {}) {
|
|
8
|
+
this.container = itemsContainerElement
|
|
9
9
|
this.renderItem = renderItem
|
|
10
10
|
const {
|
|
11
11
|
onMount,
|
|
@@ -55,7 +55,6 @@ export default class VirtualScroller {
|
|
|
55
55
|
const diffRender = prevState && items === prevState.items && items.length > 0
|
|
56
56
|
// Remove no longer visible items from the DOM.
|
|
57
57
|
if (diffRender) {
|
|
58
|
-
log('Incremental rerender')
|
|
59
58
|
// Decrement instead of increment here because
|
|
60
59
|
// `this.container.removeChild()` changes indexes.
|
|
61
60
|
let i = prevState.lastShownItemIndex
|
|
@@ -63,14 +62,14 @@ export default class VirtualScroller {
|
|
|
63
62
|
if (i >= firstShownItemIndex && i <= lastShownItemIndex) {
|
|
64
63
|
// The item is still visible.
|
|
65
64
|
} else {
|
|
66
|
-
log('Remove item index', i)
|
|
65
|
+
log('DOM: Remove element for item index', i)
|
|
67
66
|
// The item is no longer visible. Remove it.
|
|
68
67
|
this.unmountItem(this.container.childNodes[i - prevState.firstShownItemIndex])
|
|
69
68
|
}
|
|
70
69
|
i--
|
|
71
70
|
}
|
|
72
71
|
} else {
|
|
73
|
-
log('Rerender from scratch')
|
|
72
|
+
log('DOM: Rerender the list from scratch')
|
|
74
73
|
while (this.container.firstChild) {
|
|
75
74
|
this.unmountItem(this.container.firstChild)
|
|
76
75
|
}
|
|
@@ -89,11 +88,11 @@ export default class VirtualScroller {
|
|
|
89
88
|
} else {
|
|
90
89
|
const item = this.renderItem(items[i])
|
|
91
90
|
if (shouldPrependItems) {
|
|
92
|
-
log('Prepend item index', i)
|
|
91
|
+
log('DOM: Prepend element for item index', i)
|
|
93
92
|
// Append `item` to `this.container` before the retained items.
|
|
94
93
|
this.container.insertBefore(item, prependBeforeItemElement)
|
|
95
94
|
} else {
|
|
96
|
-
log('Append item index', i)
|
|
95
|
+
log('DOM: Append element for item index', i)
|
|
97
96
|
// Append `item` to `this.container`.
|
|
98
97
|
this.container.appendChild(item)
|
|
99
98
|
}
|
package/source/ItemHeights.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import log, { warn, isDebug, reportError } from './utility/debug'
|
|
2
2
|
|
|
3
3
|
export default class ItemHeights {
|
|
4
|
-
constructor(
|
|
5
|
-
this.
|
|
6
|
-
this.getContainerElement = getContainerElement
|
|
4
|
+
constructor(container, getItemHeight, setItemHeight) {
|
|
5
|
+
this.container = container
|
|
7
6
|
this._get = getItemHeight
|
|
8
7
|
this._set = setItemHeight
|
|
9
8
|
this.reset()
|
|
@@ -64,13 +63,7 @@ export default class ItemHeights {
|
|
|
64
63
|
// }
|
|
65
64
|
|
|
66
65
|
_measureItemHeight(i, firstShownItemIndex) {
|
|
67
|
-
|
|
68
|
-
if (container) {
|
|
69
|
-
const elementIndex = i - firstShownItemIndex
|
|
70
|
-
if (elementIndex >= 0 && elementIndex < this.screen.getChildElementsCount(container)) {
|
|
71
|
-
return this.screen.getChildElementHeight(container, elementIndex)
|
|
72
|
-
}
|
|
73
|
-
}
|
|
66
|
+
return this.container.getNthRenderedItemHeight(i - firstShownItemIndex)
|
|
74
67
|
}
|
|
75
68
|
|
|
76
69
|
/**
|
|
@@ -92,6 +85,7 @@ export default class ItemHeights {
|
|
|
92
85
|
* @return {number[]} The indexes of the items that have not previously been measured and have been measured now.
|
|
93
86
|
*/
|
|
94
87
|
measureItemHeights(firstShownItemIndex, lastShownItemIndex) {
|
|
88
|
+
log('~ Measure item heights ~')
|
|
95
89
|
// If no items are rendered, don't measure anything.
|
|
96
90
|
if (firstShownItemIndex === undefined) {
|
|
97
91
|
return
|
|
@@ -102,8 +96,10 @@ export default class ItemHeights {
|
|
|
102
96
|
// then reset `this.measuredItemsHeight` and "first measured"/"last measured" item indexes.
|
|
103
97
|
// For example, this could happen when `.setItems()` prepends a lot of new items.
|
|
104
98
|
if (this.firstMeasuredItemIndex !== undefined) {
|
|
105
|
-
if (
|
|
106
|
-
|
|
99
|
+
if (
|
|
100
|
+
firstShownItemIndex > this.lastMeasuredItemIndex + 1 ||
|
|
101
|
+
lastShownItemIndex < this.firstMeasuredItemIndex - 1
|
|
102
|
+
) {
|
|
107
103
|
// Reset.
|
|
108
104
|
log('Non-measured items gap detected. Reset first and last measured item indexes.')
|
|
109
105
|
this.reset()
|
|
@@ -115,20 +111,16 @@ export default class ItemHeights {
|
|
|
115
111
|
let firstMeasuredItemIndexHasBeenUpdated = false
|
|
116
112
|
let i = firstShownItemIndex
|
|
117
113
|
while (i <= lastShownItemIndex) {
|
|
114
|
+
// Measure item heights that haven't been measured previously.
|
|
118
115
|
// Don't re-measure item heights that have been measured previously.
|
|
119
116
|
// The rationale is that developers are supposed to manually call
|
|
120
117
|
// `.onItemHeightChange()` every time an item's height changes.
|
|
121
|
-
// If developers
|
|
118
|
+
// If developers don't neglect that rule, item heights won't
|
|
122
119
|
// change unexpectedly.
|
|
123
|
-
// // Re-measure all shown items' heights, because an item's height
|
|
124
|
-
// // might have changed since it has been measured initially.
|
|
125
|
-
// // For example, if an item is a long comment with a "Show more" button,
|
|
126
|
-
// // then the user might have clicked that "Show more" button.
|
|
127
120
|
if (this._get(i) === undefined) {
|
|
128
121
|
nonPreviouslyMeasuredItemIndexes.push(i)
|
|
129
|
-
log('Item', i, 'hasn\'t been previously measured')
|
|
130
122
|
const height = this._measureItemHeight(i, firstShownItemIndex)
|
|
131
|
-
log('
|
|
123
|
+
log('Item index', i, 'height', height)
|
|
132
124
|
this._set(i, height)
|
|
133
125
|
// Update average item height calculation variables
|
|
134
126
|
// related to the previously measured items
|
|
@@ -164,14 +156,17 @@ export default class ItemHeights {
|
|
|
164
156
|
this.lastMeasuredItemIndex = i
|
|
165
157
|
}
|
|
166
158
|
} else {
|
|
167
|
-
// Validate the item's height
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
159
|
+
// Validate that the item's height didn't change since it was last measured.
|
|
160
|
+
// If it did, then display a warning and update the item's height
|
|
161
|
+
// as an attempt to fix things.
|
|
162
|
+
// If an item's height changes unexpectedly then it means that there'll
|
|
163
|
+
// likely be "content jumping".
|
|
171
164
|
const previousHeight = this._get(i)
|
|
172
165
|
const height = this._measureItemHeight(i, firstShownItemIndex)
|
|
173
166
|
if (previousHeight !== height) {
|
|
174
|
-
warn('Item', i, 'height was', previousHeight, 'before it
|
|
167
|
+
warn('Item index', i, 'height changed unexpectedly: it was', previousHeight, 'before, but now it is', height, '. An item\'s height is allowed to change only in two cases: when the item\'s "state" changes and the developer calls `onItemStateChange(i, newState)`, or when the item\'s height changes for some other reason and the developer calls `onItemHeightChange(i)`. Perhaps you forgot to persist the item\'s "state" by calling `onItemStateChange(i, newState)` when it changed, and that "state" got lost when the item element was unmounted, which resulted in a different height when the item was shown again having its "state" reset.')
|
|
168
|
+
// Update the item's height as an attempt to fix things.
|
|
169
|
+
this._set(i, height)
|
|
175
170
|
}
|
|
176
171
|
}
|
|
177
172
|
i++
|