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.
Files changed (154) hide show
  1. package/.gitlab-ci.yml +1 -1
  2. package/CHANGELOG.md +24 -1
  3. package/README.md +139 -33
  4. package/babel.config.js +25 -0
  5. package/babel.js +5 -0
  6. package/bundle/index-bypass.html +1 -1
  7. package/bundle/index-dom.html +1 -1
  8. package/bundle/index-grid.html +1 -2
  9. package/bundle/index-scrollableContainer.html +1 -1
  10. package/bundle/index-tbody-scrollableContainer.html +2 -0
  11. package/bundle/index-tbody.html +2 -0
  12. package/bundle/virtual-scroller-dom.js +1 -1
  13. package/bundle/virtual-scroller-dom.js.map +1 -1
  14. package/bundle/virtual-scroller-react.js +1 -1
  15. package/bundle/virtual-scroller-react.js.map +1 -1
  16. package/bundle/virtual-scroller.js +1 -1
  17. package/bundle/virtual-scroller.js.map +1 -1
  18. package/commonjs/BeforeResize.js +319 -0
  19. package/commonjs/BeforeResize.js.map +1 -0
  20. package/commonjs/DOM/Engine.js +46 -0
  21. package/commonjs/DOM/Engine.js.map +1 -0
  22. package/commonjs/DOM/ItemsContainer.js +78 -0
  23. package/commonjs/DOM/ItemsContainer.js.map +1 -0
  24. package/commonjs/DOM/{WaitForStylesToLoad.js → ListTopOffsetWatcher.js} +56 -35
  25. package/commonjs/DOM/ListTopOffsetWatcher.js.map +1 -0
  26. package/commonjs/DOM/ScrollableContainer.js +56 -81
  27. package/commonjs/DOM/ScrollableContainer.js.map +1 -1
  28. package/commonjs/DOM/VirtualScroller.js +20 -15
  29. package/commonjs/DOM/VirtualScroller.js.map +1 -1
  30. package/commonjs/DOM/tbody.js +2 -2
  31. package/commonjs/ItemHeights.js +22 -29
  32. package/commonjs/ItemHeights.js.map +1 -1
  33. package/commonjs/Layout.js +588 -215
  34. package/commonjs/Layout.js.map +1 -1
  35. package/commonjs/Layout.test.js +191 -0
  36. package/commonjs/Layout.test.js.map +1 -0
  37. package/commonjs/ListHeightChangeWatcher.js +126 -0
  38. package/commonjs/ListHeightChangeWatcher.js.map +1 -0
  39. package/commonjs/Resize.js +22 -21
  40. package/commonjs/Resize.js.map +1 -1
  41. package/commonjs/Scroll.js +148 -88
  42. package/commonjs/Scroll.js.map +1 -1
  43. package/commonjs/VirtualScroller.js +1269 -390
  44. package/commonjs/VirtualScroller.js.map +1 -1
  45. package/commonjs/getItemCoordinates.js.map +1 -1
  46. package/commonjs/getItemsDiff.js.map +1 -1
  47. package/commonjs/getVerticalSpacing.js +8 -8
  48. package/commonjs/getVerticalSpacing.js.map +1 -1
  49. package/commonjs/react/VirtualScroller.js +31 -37
  50. package/commonjs/react/VirtualScroller.js.map +1 -1
  51. package/commonjs/utility/debounce.js +26 -4
  52. package/commonjs/utility/debounce.js.map +1 -1
  53. package/commonjs/utility/debug.js +51 -12
  54. package/commonjs/utility/debug.js.map +1 -1
  55. package/commonjs/utility/getStateSnapshot.js +50 -0
  56. package/commonjs/utility/getStateSnapshot.js.map +1 -0
  57. package/commonjs/utility/px.js +1 -1
  58. package/commonjs/utility/px.js.map +1 -1
  59. package/commonjs/utility/px.test.js +14 -0
  60. package/commonjs/utility/px.test.js.map +1 -0
  61. package/commonjs/utility/shallowEqual.js +1 -1
  62. package/commonjs/utility/shallowEqual.js.map +1 -1
  63. package/commonjs/utility/throttle.js.map +1 -1
  64. package/dom/index.d.ts +23 -0
  65. package/index.d.ts +84 -0
  66. package/modules/BeforeResize.js +310 -0
  67. package/modules/BeforeResize.js.map +1 -0
  68. package/modules/DOM/Engine.js +27 -0
  69. package/modules/DOM/Engine.js.map +1 -0
  70. package/modules/DOM/ItemsContainer.js +71 -0
  71. package/modules/DOM/ItemsContainer.js.map +1 -0
  72. package/modules/DOM/{WaitForStylesToLoad.js → ListTopOffsetWatcher.js} +57 -35
  73. package/modules/DOM/ListTopOffsetWatcher.js.map +1 -0
  74. package/modules/DOM/ScrollableContainer.js +55 -80
  75. package/modules/DOM/ScrollableContainer.js.map +1 -1
  76. package/modules/DOM/VirtualScroller.js +15 -14
  77. package/modules/DOM/VirtualScroller.js.map +1 -1
  78. package/modules/ItemHeights.js +17 -28
  79. package/modules/ItemHeights.js.map +1 -1
  80. package/modules/Layout.js +582 -213
  81. package/modules/Layout.js.map +1 -1
  82. package/modules/Layout.test.js +185 -0
  83. package/modules/Layout.test.js.map +1 -0
  84. package/modules/ListHeightChangeWatcher.js +119 -0
  85. package/modules/ListHeightChangeWatcher.js.map +1 -0
  86. package/modules/Resize.js +21 -20
  87. package/modules/Resize.js.map +1 -1
  88. package/modules/Scroll.js +148 -87
  89. package/modules/Scroll.js.map +1 -1
  90. package/modules/VirtualScroller.js +1263 -390
  91. package/modules/VirtualScroller.js.map +1 -1
  92. package/modules/getItemCoordinates.js.map +1 -1
  93. package/modules/getItemsDiff.js.map +1 -1
  94. package/modules/getVerticalSpacing.js +8 -8
  95. package/modules/getVerticalSpacing.js.map +1 -1
  96. package/modules/react/VirtualScroller.js +31 -37
  97. package/modules/react/VirtualScroller.js.map +1 -1
  98. package/modules/utility/debounce.js +26 -4
  99. package/modules/utility/debounce.js.map +1 -1
  100. package/modules/utility/debug.js +47 -10
  101. package/modules/utility/debug.js.map +1 -1
  102. package/modules/utility/getStateSnapshot.js +43 -0
  103. package/modules/utility/getStateSnapshot.js.map +1 -0
  104. package/modules/utility/px.js +1 -1
  105. package/modules/utility/px.js.map +1 -1
  106. package/modules/utility/px.test.js +9 -0
  107. package/modules/utility/px.test.js.map +1 -0
  108. package/modules/utility/shallowEqual.js +1 -1
  109. package/modules/utility/shallowEqual.js.map +1 -1
  110. package/modules/utility/throttle.js.map +1 -1
  111. package/package.json +24 -22
  112. package/react/index.d.ts +27 -0
  113. package/source/BeforeResize.js +317 -0
  114. package/source/DOM/Engine.js +32 -0
  115. package/source/DOM/ItemsContainer.js +48 -0
  116. package/source/DOM/{WaitForStylesToLoad.js → ListTopOffsetWatcher.js} +48 -22
  117. package/source/DOM/ScrollableContainer.js +39 -56
  118. package/source/DOM/VirtualScroller.js +6 -7
  119. package/source/ItemHeights.js +19 -24
  120. package/source/Layout.js +626 -252
  121. package/source/Layout.test.js +171 -0
  122. package/source/ListHeightChangeWatcher.js +94 -0
  123. package/source/Resize.js +23 -15
  124. package/source/Scroll.js +139 -78
  125. package/source/VirtualScroller.js +1243 -286
  126. package/source/getVerticalSpacing.js +7 -7
  127. package/source/react/VirtualScroller.js +2 -18
  128. package/source/utility/debounce.js +20 -3
  129. package/source/utility/debug.js +34 -3
  130. package/source/utility/getStateSnapshot.js +36 -0
  131. package/source/utility/px.js +1 -1
  132. package/source/utility/px.test.js +9 -0
  133. package/website/index-bypass.html +195 -0
  134. package/website/index-grid.html +0 -1
  135. package/website/index-scrollableContainer.html +208 -0
  136. package/website/index-tbody-scrollableContainer.html +68 -0
  137. package/website/index-tbody.html +55 -0
  138. package/commonjs/DOM/RenderingEngine.js +0 -33
  139. package/commonjs/DOM/RenderingEngine.js.map +0 -1
  140. package/commonjs/DOM/Screen.js +0 -87
  141. package/commonjs/DOM/Screen.js.map +0 -1
  142. package/commonjs/DOM/WaitForStylesToLoad.js.map +0 -1
  143. package/commonjs/RestoreScroll.js +0 -118
  144. package/commonjs/RestoreScroll.js.map +0 -1
  145. package/modules/DOM/RenderingEngine.js +0 -19
  146. package/modules/DOM/RenderingEngine.js.map +0 -1
  147. package/modules/DOM/Screen.js +0 -80
  148. package/modules/DOM/Screen.js.map +0 -1
  149. package/modules/DOM/WaitForStylesToLoad.js.map +0 -1
  150. package/modules/RestoreScroll.js +0 -111
  151. package/modules/RestoreScroll.js.map +0 -1
  152. package/source/DOM/RenderingEngine.js +0 -22
  153. package/source/DOM/Screen.js +0 -51
  154. package/source/RestoreScroll.js +0 -86
@@ -1,11 +1,11 @@
1
- export default function getVerticalSpacing({ container, screen }) {
2
- if (screen.getChildElementsCount(container) > 1) {
3
- const firstShownRowTopOffset = screen.getChildElementTopOffset(container, 0)
4
- let firstShownRowHeight = screen.getChildElementHeight(container, 0)
1
+ export default function getVerticalSpacing({ itemsContainer, renderedItemsCount }) {
2
+ if (renderedItemsCount > 1) {
3
+ const firstShownRowTopOffset = itemsContainer.getNthRenderedItemTopOffset(0)
4
+ let firstShownRowHeight = itemsContainer.getNthRenderedItemHeight(0)
5
5
  let i = 1
6
- while (i < screen.getChildElementsCount(container)) {
7
- const itemTopOffset = screen.getChildElementTopOffset(container, i)
8
- const itemHeight = screen.getChildElementHeight(container, i)
6
+ while (i < renderedItemsCount) {
7
+ const itemTopOffset = itemsContainer.getNthRenderedItemTopOffset(i)
8
+ const itemHeight = itemsContainer.getNthRenderedItemHeight(i)
9
9
  // If next row is detected.
10
10
  if (itemTopOffset !== firstShownRowTopOffset) {
11
11
  // Measure inter-row spacing.
@@ -17,7 +17,7 @@ const elementType = PropTypes.elementType || PropTypes.oneOfType([
17
17
  export default class VirtualScroller extends React.Component {
18
18
  static propTypes = {
19
19
  as: elementType,
20
- items: PropTypes.arrayOf(PropTypes.object).isRequired,
20
+ items: PropTypes.arrayOf(PropTypes.any).isRequired,
21
21
  itemComponent: elementType.isRequired,
22
22
  itemComponentProps: PropTypes.object,
23
23
  estimatedItemHeight: PropTypes.number,
@@ -27,10 +27,6 @@ export default class VirtualScroller extends React.Component {
27
27
  // `preserveScrollPosition` property name is deprecated,
28
28
  // use `preserveScrollPositionOnPrependItems` instead.
29
29
  preserveScrollPosition: PropTypes.bool,
30
- preserveScrollPositionOfTheBottomOfTheListOnMount: PropTypes.bool,
31
- // `preserveScrollPositionAtBottomOnMount` property name is deprecated,
32
- // use `preserveScrollPositionOfTheBottomOfTheListOnMount` property instead.
33
- preserveScrollPositionAtBottomOnMount: PropTypes.bool,
34
30
  measureItemsBatchSize: PropTypes.number,
35
31
  scrollableContainer: PropTypes.any,
36
32
  // `getScrollableContainer` property is deprecated.
@@ -109,10 +105,6 @@ export default class VirtualScroller extends React.Component {
109
105
  initialCustomState,
110
106
  onStateChange,
111
107
  estimatedItemHeight,
112
- preserveScrollPositionOfTheBottomOfTheListOnMount,
113
- // `preserveScrollPositionAtBottomOnMount` property name is deprecated,
114
- // use `preserveScrollPositionOfTheBottomOfTheListOnMount` property instead.
115
- preserveScrollPositionAtBottomOnMount,
116
108
  initialScrollPosition,
117
109
  onScrollPositionChange,
118
110
  measureItemsBatchSize,
@@ -137,10 +129,6 @@ export default class VirtualScroller extends React.Component {
137
129
  onItemInitialRender: this.onItemInitialRender,
138
130
  // `onItemFirstRender(i)` is deprecated, use `onItemInitialRender(item)` instead.
139
131
  onItemFirstRender: this.onItemFirstRender,
140
- preserveScrollPositionOfTheBottomOfTheListOnMount,
141
- // `preserveScrollPositionAtBottomOnMount` property name is deprecated,
142
- // use `preserveScrollPositionOfTheBottomOfTheListOnMount` property instead.
143
- preserveScrollPositionAtBottomOnMount,
144
132
  initialScrollPosition,
145
133
  onScrollPositionChange,
146
134
  shouldUpdateLayoutOnScreenResize: this.shouldUpdateLayoutOnScreenResize,
@@ -451,10 +439,6 @@ export default class VirtualScroller extends React.Component {
451
439
  // `preserveScrollPosition` property name is deprecated,
452
440
  // use `preserveScrollPositionOnPrependItems` instead.
453
441
  preserveScrollPosition,
454
- preserveScrollPositionOfTheBottomOfTheListOnMount,
455
- // `preserveScrollPositionAtBottomOnMount` property name is deprecated,
456
- // use `preserveScrollPositionOfTheBottomOfTheListOnMount` property instead.
457
- preserveScrollPositionAtBottomOnMount,
458
442
  initialScrollPosition,
459
443
  onScrollPositionChange,
460
444
  measureItemsBatchSize,
@@ -558,7 +542,7 @@ export default class VirtualScroller extends React.Component {
558
542
  if (prependedItemsCount > 0) {
559
543
  if (preserveScrollPositionOnPrependItems || preserveScrollPosition) {
560
544
  if (firstShownItemIndex === 0) {
561
- this.virtualScroller.restoreScroll.captureScroll({
545
+ this.virtualScroller.listHeightChangeWatcher.snapshot({
562
546
  previousItems,
563
547
  newItems,
564
548
  prependedItemsCount
@@ -8,12 +8,29 @@ import { setTimeout, clearTimeout } from 'request-animation-frame-timeout'
8
8
  * Same as `lodash`'s `debounce()` for functions with no arguments.
9
9
  * @param {function} func
10
10
  * @param {number} interval
11
+ * @param {function} [options.onStart]
12
+ * @param {function} [options.onStop]
11
13
  * @return {function}
12
14
  */
13
- export default function debounce(func, interval) {
15
+ export default function debounce(func, interval, { onStart, onStop } = {}) {
14
16
  let timeout
15
17
  return function(...args) {
16
- clearTimeout(timeout)
17
- timeout = setTimeout(() => func.apply(this, args), interval)
18
+ return new Promise((resolve) => {
19
+ if (timeout) {
20
+ clearTimeout(timeout)
21
+ } else {
22
+ if (onStart) {
23
+ onStart()
24
+ }
25
+ }
26
+ timeout = setTimeout(() => {
27
+ timeout = undefined
28
+ if (onStop) {
29
+ onStop()
30
+ }
31
+ func.apply(this, args)
32
+ resolve()
33
+ }, interval)
34
+ })
18
35
  }
19
36
  }
@@ -5,7 +5,12 @@ export default function log(...args) {
5
5
  }
6
6
 
7
7
  export function warn(...args) {
8
- console.warn(...['[virtual-scroller]'].concat(args))
8
+ if (isWarn()) {
9
+ if (warningsAreErrors()) {
10
+ return reportError.apply(this, args)
11
+ }
12
+ console.warn(...['[virtual-scroller]'].concat(args))
13
+ }
9
14
  }
10
15
 
11
16
  export function reportError(...args) {
@@ -30,8 +35,34 @@ export function reportError(...args) {
30
35
  }
31
36
 
32
37
  export function isDebug() {
38
+ const debug = getDebug()
39
+ if (debug !== undefined) {
40
+ return debug === true || debug === 'debug'
41
+ }
42
+ }
43
+
44
+ export function isWarn() {
45
+ // const debug = getDebug()
46
+ // return debug === undefined
47
+ // || debug === true
48
+ // || debug === 'debug'
49
+ // || debug === 'warn'
50
+ //
51
+ return true
52
+ }
53
+
54
+ function getDebug() {
55
+ return getGlobalVariable('VirtualScrollerDebug')
56
+ }
57
+
58
+ function warningsAreErrors() {
59
+ return getGlobalVariable('VirtualScrollerWarningsAreErrors')
60
+ }
61
+
62
+ function getGlobalVariable(name) {
33
63
  if (typeof window !== 'undefined') {
34
- return window.VirtualScrollerDebug === true
35
- || window.VirtualScrollerDebug === 'debug'
64
+ return window[name]
65
+ } else if (typeof global !== 'undefined') {
66
+ return global[name]
36
67
  }
37
68
  }
@@ -0,0 +1,36 @@
1
+ // Creates a snapshot of a `state` or a partial update of a `state`.
2
+ // Is only used for logging state snapshots for later debug.
3
+ //
4
+ // When `state` is output to the browser console via `console.log()`,
5
+ // it is explorable in real time. That also means that if that `state`
6
+ // is modified later, the user will see the modified state, not the
7
+ // original one. In the current implementation, `state` is not strictly
8
+ // "immutable": things like individual item heights (including "before resize" ones)
9
+ // or states are updated in-place — `state.itemHeights[i] = newItemHeight` or
10
+ // `state.itemStates[i] = newItemState`. That's because those `state` properties
11
+ // are the ones that don’t affect the presentation, so there's no need to re-render
12
+ // the list when those do change — updating those properties is just an effect of
13
+ // some change rather than cause for one.
14
+ //
15
+ // So, when outputting `state` via `console.log()` for debug, it makes sense to
16
+ // snapshot it so that the developer, while debugging later, sees the correct
17
+ // item heights or item states.
18
+ //
19
+ export default function getStateSnapshot(state) {
20
+ let stateSnapshot = {
21
+ ...state
22
+ }
23
+ if (state.itemHeights) {
24
+ stateSnapshot.itemHeights = state.itemHeights.slice()
25
+ }
26
+ if (state.itemStates) {
27
+ stateSnapshot.itemStates = state.itemStates.slice()
28
+ }
29
+ if (state.beforeResize) {
30
+ stateSnapshot.beforeResize = {
31
+ ...state.beforeResize
32
+ }
33
+ stateSnapshot.beforeResize.itemHeights = state.beforeResize.itemHeights.slice()
34
+ }
35
+ return stateSnapshot
36
+ }
@@ -7,5 +7,5 @@
7
7
  */
8
8
  export default function px(number) {
9
9
  // Fractional pixels are used on "retina" screens.
10
- return number.toFixed(2) + 'px'
10
+ return (number % 1 === 0 ? number : number.toFixed(2)) + 'px'
11
11
  }
@@ -0,0 +1,9 @@
1
+ import px from './px'
2
+
3
+ describe('utility/px', function() {
4
+ it('should truncate px values', function() {
5
+ px(0).should.equal('0px')
6
+ px(1).should.equal('1px')
7
+ px(1.2345).should.equal('1.23px')
8
+ })
9
+ })
@@ -0,0 +1,195 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <!-- Fix encoding. -->
5
+ <meta charset="utf-8">
6
+ <!-- Fix document width for mobile devices. -->
7
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
8
+
9
+ <title>React VirtualScroller Demo</title>
10
+
11
+ <script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
12
+ <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
13
+ <script crossorigin src="https://cdnjs.cloudflare.com/ajax/libs/prop-types/15.7.2/prop-types.min.js"></script>
14
+ <script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>
15
+
16
+ <script src="./virtual-scroller-react.js"></script>
17
+ <script src="./on-scroll-to-react.js"></script>
18
+ <script src="./messages.js"></script>
19
+
20
+ <link rel="stylesheet" href="./style.css"/>
21
+
22
+ <!--
23
+ <style>
24
+ #messages {
25
+ border-top: 2px solid rgb(230, 236, 240);
26
+ display: grid;
27
+ grid-gap: 2em;
28
+ }
29
+ </style>
30
+ -->
31
+ </head>
32
+
33
+ <body>
34
+ <!-- http://tholman.com/github-corners/ -->
35
+ <a title="Go to GitHub repo" href="https://github.com/catamphetamine/virtual-scroller" class="github-corner" aria-label="View source on GitHub"><svg width="80" height="80" viewBox="0 0 250 250" style="fill:#151513; color:#fff; position: absolute; top: 0; border: 0; right: 0;" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a><style>.github-corner:hover .octo-arm{animation:octocat-wave 560ms ease-in-out}@keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@media (max-width:500px){.github-corner:hover .octo-arm{animation:none}.github-corner .octo-arm{animation:octocat-wave 560ms ease-in-out}}</style>
36
+
37
+ <div id="root"></div>
38
+
39
+ <script>
40
+ // Enable debug output to console.
41
+ window.VirtualScrollerDebug = true
42
+ // Whether "dynamically loaded list" mode is enabled.
43
+ window.dynamic = new URL(window.location).searchParams.get('dynamic')
44
+ </script>
45
+
46
+ <script type="text/babel">
47
+ const BATCH_SIZE = 6
48
+
49
+ class FeedMessages extends React.Component {
50
+ constructor(props) {
51
+ super(props)
52
+ const { messages } = this.props
53
+ if (window.dynamic) {
54
+ const fromIndex = Math.floor(messages.length / 2 - BATCH_SIZE / 2)
55
+ const toIndex = fromIndex + BATCH_SIZE - 1
56
+ this.state = {
57
+ fromIndex,
58
+ toIndex,
59
+ messages: messages.slice(fromIndex, toIndex + 1)
60
+ }
61
+ } else {
62
+ this.state = {
63
+ fromIndex: 0,
64
+ toIndex: messages.length,
65
+ messages
66
+ }
67
+ }
68
+ }
69
+
70
+ onShowPrevious = () => {
71
+ const { messages } = this.props
72
+ let { fromIndex } = this.state
73
+ const { toIndex } = this.state
74
+ fromIndex = Math.max(fromIndex - BATCH_SIZE, 0)
75
+ this.setState({
76
+ fromIndex,
77
+ messages: messages.slice(fromIndex, toIndex + 1)
78
+ })
79
+ }
80
+
81
+ onShowNext = () => {
82
+ const { messages } = this.props
83
+ const { fromIndex } = this.state
84
+ let { toIndex } = this.state
85
+ toIndex = Math.min(toIndex + BATCH_SIZE, messages.length - 1)
86
+ this.setState({
87
+ toIndex,
88
+ messages: messages.slice(fromIndex, toIndex + 1)
89
+ })
90
+ }
91
+
92
+ render() {
93
+ const {
94
+ fromIndex,
95
+ toIndex,
96
+ messages
97
+ } = this.state
98
+ return (
99
+ <React.Fragment>
100
+ {window.dynamic && fromIndex > 0 &&
101
+ <button
102
+ type="button"
103
+ onClick={this.onShowPrevious}
104
+ className="load-items-button">
105
+ Show previous
106
+ </button>
107
+ }
108
+ <VirtualScroller
109
+ bypass
110
+ id="messages"
111
+ items={messages}
112
+ itemComponent={Message}
113
+ preserveScrollPositionOnPrependItems/>
114
+ {window.dynamic && toIndex < this.props.messages.length - 1 &&
115
+ <button
116
+ type="button"
117
+ onClick={this.onShowNext}
118
+ className="load-items-button">
119
+ Show next
120
+ </button>
121
+ }
122
+ </React.Fragment>
123
+ )
124
+ }
125
+ }
126
+
127
+ const message = PropTypes.shape({
128
+ username: PropTypes.string.isRequired,
129
+ date: PropTypes.instanceOf(Date).isRequired,
130
+ text: PropTypes.string.isRequired
131
+ })
132
+
133
+ FeedMessages.propTypes = {
134
+ messages: PropTypes.arrayOf(message).isRequired
135
+ }
136
+
137
+ function Message(props) {
138
+ const { children: message } = props
139
+ const {
140
+ username,
141
+ date,
142
+ text
143
+ } = message
144
+ return (
145
+ <article className="feed-message">
146
+ <a target="_blank" href={`https://twitter.com/${username}`}>
147
+ @{username}
148
+ </a>
149
+ <time dateTime={date.toISOString()}>
150
+ {date.getMonth() + 1}/{date.getDate()}/{date.getFullYear()}
151
+ </time>
152
+ <p>
153
+ {text}
154
+ </p>
155
+ </article>
156
+ )
157
+ }
158
+
159
+ Message.propTypes = {
160
+ children: message.isRequired
161
+ }
162
+
163
+ class Feed extends React.Component {
164
+ render() {
165
+ return (
166
+ <section className="container">
167
+ <h1>
168
+ <TwitterLogo/>
169
+ Latest Tweets on #politics
170
+ </h1>
171
+ <FeedMessages
172
+ messages={messages}/>
173
+ <footer>
174
+ © Twitter Inc., 2019
175
+ </footer>
176
+ </section>
177
+ )
178
+ }
179
+ }
180
+
181
+ function TwitterLogo() {
182
+ return (
183
+ <svg className="twitter-logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
184
+ <path fill="#00AAEC" fillRule="evenodd" d="M128 23.294a51.28 51.28 0 0 1-15.079 4.237c5.424-3.328 9.587-8.606 11.548-14.892a51.718 51.718 0 0 1-16.687 6.526c-4.778-5.231-11.608-8.498-19.166-8.498-14.493 0-26.251 12.057-26.251 26.927 0 2.111.225 4.16.676 6.133-21.824-1.126-41.17-11.835-54.131-28.145a27.422 27.422 0 0 0-3.554 13.552c0 9.338 4.636 17.581 11.683 22.412-4.297-.131-8.355-1.356-11.901-3.359v.331c0 13.051 9.053 23.937 21.074 26.403-2.201.632-4.523.948-6.92.948-1.69 0-3.343-.162-4.944-.478 3.343 10.694 13.035 18.483 24.53 18.691-8.986 7.227-20.315 11.533-32.614 11.533-2.119 0-4.215-.123-6.266-.37 11.623 7.627 25.432 12.088 40.255 12.088 48.309 0 74.717-41.026 74.717-76.612a89.39 89.39 0 0 0-.068-3.49A53.862 53.862 0 0 0 128 23.294" clipRule="evenodd"/>
185
+ </svg>
186
+ )
187
+ }
188
+
189
+ ReactDOM.render(
190
+ <Feed/>,
191
+ document.getElementById('root')
192
+ )
193
+ </script>
194
+ </body>
195
+ </html>
@@ -61,7 +61,6 @@
61
61
  const COLUMNS_COUNT = 3
62
62
 
63
63
  function getColumnsCount(container) {
64
- console.log('Container width', container.getWidth())
65
64
  if (container.getWidth() > 1280) {
66
65
  return COLUMNS_COUNT
67
66
  }
@@ -0,0 +1,208 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <!-- Fix encoding. -->
5
+ <meta charset="utf-8">
6
+ <!-- Fix document width for mobile devices. -->
7
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
8
+
9
+ <title>React VirtualScroller Demo (Scrollable Container)</title>
10
+
11
+ <script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
12
+ <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
13
+ <script crossorigin src="https://cdnjs.cloudflare.com/ajax/libs/prop-types/15.7.2/prop-types.min.js"></script>
14
+ <script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>
15
+
16
+ <script src="./virtual-scroller-react.js"></script>
17
+ <script src="./on-scroll-to-react.js"></script>
18
+ <script src="./messages.js"></script>
19
+
20
+ <link rel="stylesheet" href="./style.css"/>
21
+ </head>
22
+
23
+ <body>
24
+ <!-- http://tholman.com/github-corners/ -->
25
+ <a title="Go to GitHub repo" href="https://github.com/catamphetamine/virtual-scroller" class="github-corner" aria-label="View source on GitHub"><svg width="80" height="80" viewBox="0 0 250 250" style="fill:#151513; color:#fff; position: absolute; top: 0; border: 0; right: 0;" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a><style>.github-corner:hover .octo-arm{animation:octocat-wave 560ms ease-in-out}@keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@media (max-width:500px){.github-corner:hover .octo-arm{animation:none}.github-corner .octo-arm{animation:octocat-wave 560ms ease-in-out}}</style>
26
+
27
+ <div id="root"></div>
28
+
29
+ <script>
30
+ // Enable debug output to console.
31
+ window.VirtualScrollerDebug = true
32
+ // Whether "dynamically loaded list" mode is enabled.
33
+ window.dynamic = new URL(window.location).searchParams.get('dynamic')
34
+ </script>
35
+
36
+ <script type="text/babel">
37
+ const BATCH_SIZE = 6
38
+
39
+ const SCROLLABLE_CONTAINER_STYLE = {
40
+ marginTop: '20vh',
41
+ maxHeight: '60vh',
42
+ overflow: 'auto'
43
+ }
44
+
45
+ class FeedMessages extends React.Component {
46
+ constructor(props) {
47
+ super(props)
48
+ const { messages } = this.props
49
+ if (window.dynamic) {
50
+ const fromIndex = Math.floor(messages.length / 2 - BATCH_SIZE / 2)
51
+ const toIndex = fromIndex + BATCH_SIZE - 1
52
+ this.state = {
53
+ fromIndex,
54
+ toIndex,
55
+ messages: messages.slice(fromIndex, toIndex + 1)
56
+ }
57
+ } else {
58
+ this.state = {
59
+ fromIndex: 0,
60
+ toIndex: messages.length,
61
+ messages
62
+ }
63
+ }
64
+ }
65
+
66
+ onShowPrevious = () => {
67
+ const { messages } = this.props
68
+ let { fromIndex } = this.state
69
+ const { toIndex } = this.state
70
+ fromIndex = Math.max(fromIndex - BATCH_SIZE, 0)
71
+ this.setState({
72
+ fromIndex,
73
+ messages: messages.slice(fromIndex, toIndex + 1)
74
+ })
75
+ }
76
+
77
+ onShowNext = () => {
78
+ const { messages } = this.props
79
+ const { fromIndex } = this.state
80
+ let { toIndex } = this.state
81
+ toIndex = Math.min(toIndex + BATCH_SIZE, messages.length - 1)
82
+ this.setState({
83
+ toIndex,
84
+ messages: messages.slice(fromIndex, toIndex + 1)
85
+ })
86
+ }
87
+
88
+ getScrollableContainer = () => {
89
+ return this.scrollableContainer
90
+ }
91
+
92
+ setScrollableContainer = (node) => {
93
+ this.scrollableContainer = node
94
+ }
95
+
96
+ componentDidMount() {
97
+ this.setState({ isMounted: true })
98
+ }
99
+
100
+ render() {
101
+ const {
102
+ fromIndex,
103
+ toIndex,
104
+ messages,
105
+ isMounted
106
+ } = this.state
107
+ return (
108
+ <div
109
+ ref={this.setScrollableContainer}
110
+ style={SCROLLABLE_CONTAINER_STYLE}>
111
+ {window.dynamic && fromIndex > 0 &&
112
+ <button
113
+ type="button"
114
+ onClick={this.onShowPrevious}
115
+ className="load-items-button">
116
+ Show previous
117
+ </button>
118
+ }
119
+ {isMounted &&
120
+ <VirtualScroller
121
+ id="messages"
122
+ items={messages}
123
+ itemComponent={Message}
124
+ preserveScrollPosition
125
+ scrollableContainer={this.scrollableContainer}/>
126
+ }
127
+ {window.dynamic && toIndex < this.props.messages.length - 1 &&
128
+ <button
129
+ type="button"
130
+ onClick={this.onShowNext}
131
+ className="load-items-button">
132
+ Show next
133
+ </button>
134
+ }
135
+ </div>
136
+ )
137
+ }
138
+ }
139
+
140
+ const message = PropTypes.shape({
141
+ username: PropTypes.string.isRequired,
142
+ date: PropTypes.instanceOf(Date).isRequired,
143
+ text: PropTypes.string.isRequired
144
+ })
145
+
146
+ FeedMessages.propTypes = {
147
+ messages: PropTypes.arrayOf(message).isRequired
148
+ }
149
+
150
+ function Message(props) {
151
+ const { children: message } = props
152
+ const {
153
+ username,
154
+ date,
155
+ text
156
+ } = message
157
+ return (
158
+ <article className="feed-message">
159
+ <a target="_blank" href={`https://twitter.com/${username}`}>
160
+ @{username}
161
+ </a>
162
+ <time dateTime={date.toISOString()}>
163
+ {date.getMonth() + 1}/{date.getDate()}/{date.getFullYear()}
164
+ </time>
165
+ <p>
166
+ {text}
167
+ </p>
168
+ </article>
169
+ )
170
+ }
171
+
172
+ Message.propTypes = {
173
+ children: message.isRequired
174
+ }
175
+
176
+ class Feed extends React.Component {
177
+ render() {
178
+ return (
179
+ <section className="container">
180
+ <h1>
181
+ <TwitterLogo/>
182
+ Latest Tweets on #politics
183
+ </h1>
184
+ <FeedMessages
185
+ messages={messages}/>
186
+ <footer>
187
+ © Twitter Inc., 2019
188
+ </footer>
189
+ </section>
190
+ )
191
+ }
192
+ }
193
+
194
+ function TwitterLogo() {
195
+ return (
196
+ <svg className="twitter-logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
197
+ <path fill="#00AAEC" fillRule="evenodd" d="M128 23.294a51.28 51.28 0 0 1-15.079 4.237c5.424-3.328 9.587-8.606 11.548-14.892a51.718 51.718 0 0 1-16.687 6.526c-4.778-5.231-11.608-8.498-19.166-8.498-14.493 0-26.251 12.057-26.251 26.927 0 2.111.225 4.16.676 6.133-21.824-1.126-41.17-11.835-54.131-28.145a27.422 27.422 0 0 0-3.554 13.552c0 9.338 4.636 17.581 11.683 22.412-4.297-.131-8.355-1.356-11.901-3.359v.331c0 13.051 9.053 23.937 21.074 26.403-2.201.632-4.523.948-6.92.948-1.69 0-3.343-.162-4.944-.478 3.343 10.694 13.035 18.483 24.53 18.691-8.986 7.227-20.315 11.533-32.614 11.533-2.119 0-4.215-.123-6.266-.37 11.623 7.627 25.432 12.088 40.255 12.088 48.309 0 74.717-41.026 74.717-76.612a89.39 89.39 0 0 0-.068-3.49A53.862 53.862 0 0 0 128 23.294" clipRule="evenodd"/>
198
+ </svg>
199
+ )
200
+ }
201
+
202
+ ReactDOM.render(
203
+ <Feed/>,
204
+ document.getElementById('root')
205
+ )
206
+ </script>
207
+ </body>
208
+ </html>