You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

200 lines
5.9 KiB

  1. import React, { PureComponent } from 'react';
  2. import { ScrollContainer } from 'react-router-scroll-4';
  3. import PropTypes from 'prop-types';
  4. import IntersectionObserverArticleContainer from '../containers/intersection_observer_article_container';
  5. import LoadMore from './load_more';
  6. import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper';
  7. import { throttle } from 'lodash';
  8. import { List as ImmutableList } from 'immutable';
  9. import classNames from 'classnames';
  10. import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../features/ui/util/fullscreen';
  11. export default class ScrollableList extends PureComponent {
  12. static contextTypes = {
  13. router: PropTypes.object,
  14. };
  15. static propTypes = {
  16. scrollKey: PropTypes.string.isRequired,
  17. onLoadMore: PropTypes.func,
  18. onScrollToTop: PropTypes.func,
  19. onScroll: PropTypes.func,
  20. trackScroll: PropTypes.bool,
  21. shouldUpdateScroll: PropTypes.func,
  22. isLoading: PropTypes.bool,
  23. hasMore: PropTypes.bool,
  24. prepend: PropTypes.node,
  25. alwaysPrepend: PropTypes.bool,
  26. alwaysShowScrollbar: PropTypes.bool,
  27. emptyMessage: PropTypes.node,
  28. children: PropTypes.node,
  29. };
  30. static defaultProps = {
  31. trackScroll: true,
  32. };
  33. state = {
  34. fullscreen: null,
  35. };
  36. intersectionObserverWrapper = new IntersectionObserverWrapper();
  37. handleScroll = throttle(() => {
  38. if (this.node) {
  39. const { scrollTop, scrollHeight, clientHeight } = this.node;
  40. const offset = scrollHeight - scrollTop - clientHeight;
  41. if (400 > offset && this.props.onLoadMore && !this.props.isLoading) {
  42. this.props.onLoadMore();
  43. }
  44. if (scrollTop < 100 && this.props.onScrollToTop) {
  45. this.props.onScrollToTop();
  46. } else if (this.props.onScroll) {
  47. this.props.onScroll();
  48. }
  49. }
  50. }, 150, {
  51. trailing: true,
  52. });
  53. componentDidMount () {
  54. this.attachScrollListener();
  55. this.attachIntersectionObserver();
  56. attachFullscreenListener(this.onFullScreenChange);
  57. // Handle initial scroll posiiton
  58. this.handleScroll();
  59. }
  60. getSnapshotBeforeUpdate (prevProps) {
  61. const someItemInserted = React.Children.count(prevProps.children) > 0 &&
  62. React.Children.count(prevProps.children) < React.Children.count(this.props.children) &&
  63. this.getFirstChildKey(prevProps) !== this.getFirstChildKey(this.props);
  64. if (someItemInserted && this.node.scrollTop > 0) {
  65. return this.node.scrollHeight - this.node.scrollTop;
  66. } else {
  67. return null;
  68. }
  69. }
  70. componentDidUpdate (prevProps, prevState, snapshot) {
  71. // Reset the scroll position when a new child comes in in order not to
  72. // jerk the scrollbar around if you're already scrolled down the page.
  73. if (snapshot !== null) {
  74. const newScrollTop = this.node.scrollHeight - snapshot;
  75. if (this.node.scrollTop !== newScrollTop) {
  76. this.node.scrollTop = newScrollTop;
  77. }
  78. }
  79. }
  80. componentWillUnmount () {
  81. this.detachScrollListener();
  82. this.detachIntersectionObserver();
  83. detachFullscreenListener(this.onFullScreenChange);
  84. }
  85. onFullScreenChange = () => {
  86. this.setState({ fullscreen: isFullscreen() });
  87. }
  88. attachIntersectionObserver () {
  89. this.intersectionObserverWrapper.connect({
  90. root: this.node,
  91. rootMargin: '300% 0px',
  92. });
  93. }
  94. detachIntersectionObserver () {
  95. this.intersectionObserverWrapper.disconnect();
  96. }
  97. attachScrollListener () {
  98. this.node.addEventListener('scroll', this.handleScroll);
  99. }
  100. detachScrollListener () {
  101. this.node.removeEventListener('scroll', this.handleScroll);
  102. }
  103. getFirstChildKey (props) {
  104. const { children } = props;
  105. let firstChild = children;
  106. if (children instanceof ImmutableList) {
  107. firstChild = children.get(0);
  108. } else if (Array.isArray(children)) {
  109. firstChild = children[0];
  110. }
  111. return firstChild && firstChild.key;
  112. }
  113. setRef = (c) => {
  114. this.node = c;
  115. }
  116. handleLoadMore = (e) => {
  117. e.preventDefault();
  118. this.props.onLoadMore();
  119. }
  120. render () {
  121. const { children, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, alwaysPrepend, alwaysShowScrollbar, emptyMessage, onLoadMore } = this.props;
  122. const { fullscreen } = this.state;
  123. const childrenCount = React.Children.count(children);
  124. const loadMore = (hasMore && childrenCount > 0 && onLoadMore) ? <LoadMore visible={!isLoading} onClick={this.handleLoadMore} /> : null;
  125. let scrollableArea = null;
  126. if (isLoading || childrenCount > 0 || !emptyMessage) {
  127. scrollableArea = (
  128. <div className={classNames('scrollable', { fullscreen })} ref={this.setRef}>
  129. <div role='feed' className='item-list'>
  130. {prepend}
  131. {React.Children.map(this.props.children, (child, index) => (
  132. <IntersectionObserverArticleContainer
  133. key={child.key}
  134. id={child.key}
  135. index={index}
  136. listLength={childrenCount}
  137. intersectionObserverWrapper={this.intersectionObserverWrapper}
  138. saveHeightKey={trackScroll ? `${this.context.router.route.location.key}:${scrollKey}` : null}
  139. >
  140. {child}
  141. </IntersectionObserverArticleContainer>
  142. ))}
  143. {loadMore}
  144. </div>
  145. </div>
  146. );
  147. } else {
  148. const scrollable = alwaysShowScrollbar;
  149. scrollableArea = (
  150. <div className={classNames({ scrollable, fullscreen })} ref={this.setRef} style={{ flex: '1 1 auto', display: 'flex', flexDirection: 'column' }}>
  151. {alwaysPrepend && prepend}
  152. <div className='empty-column-indicator'>
  153. {emptyMessage}
  154. </div>
  155. </div>
  156. );
  157. }
  158. if (trackScroll) {
  159. return (
  160. <ScrollContainer scrollKey={scrollKey} shouldUpdateScroll={shouldUpdateScroll}>
  161. {scrollableArea}
  162. </ScrollContainer>
  163. );
  164. } else {
  165. return scrollableArea;
  166. }
  167. }
  168. }