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';
12 export default class ScrollableList
extends PureComponent
{
14 static contextTypes
= {
15 router: PropTypes
.object
,
19 scrollKey: PropTypes
.string
.isRequired
,
20 onLoadMore: PropTypes
.func
,
21 onScrollToTop: PropTypes
.func
,
22 onScroll: PropTypes
.func
,
23 trackScroll: PropTypes
.bool
,
24 shouldUpdateScroll: PropTypes
.func
,
25 isLoading: PropTypes
.bool
,
26 hasMore: PropTypes
.bool
,
27 prepend: PropTypes
.node
,
28 emptyMessage: PropTypes
.node
,
29 children: PropTypes
.node
,
32 static defaultProps
= {
41 intersectionObserverWrapper
= new IntersectionObserverWrapper();
43 handleScroll
= throttle(() => {
45 const { scrollTop
, scrollHeight
, clientHeight
} = this.node
;
46 const offset
= scrollHeight
- scrollTop
- clientHeight
;
48 if (400 > offset
&& this.props
.onLoadMore
&& !this.props
.isLoading
) {
49 this.props
.onLoadMore();
52 if (scrollTop
< 100 && this.props
.onScrollToTop
) {
53 this.props
.onScrollToTop();
54 } else if (this.props
.onScroll
) {
55 this.props
.onScroll();
62 componentDidMount () {
63 this.attachScrollListener();
64 this.attachIntersectionObserver();
65 attachFullscreenListener(this.onFullScreenChange
);
67 // Handle initial scroll posiiton
71 getSnapshotBeforeUpdate (prevProps
) {
72 const someItemInserted
= React
.Children
.count(prevProps
.children
) > 0 &&
73 React
.Children
.count(prevProps
.children
) < React
.Children
.count(this.props
.children
) &&
74 this.getFirstChildKey(prevProps
) !== this.getFirstChildKey(this.props
);
75 if (someItemInserted
&& this.node
.scrollTop
> 0 || this.state
.mouseOver
) {
76 return this.node
.scrollHeight
- this.node
.scrollTop
;
82 componentDidUpdate (prevProps
, prevState
, snapshot
) {
83 // Reset the scroll position when a new child comes in in order not to
84 // jerk the scrollbar around if you're already scrolled down the page.
85 if (snapshot
!== null) {
86 const newScrollTop
= this.node
.scrollHeight
- snapshot
;
88 if (this.node
.scrollTop
!== newScrollTop
) {
89 this.node
.scrollTop
= newScrollTop
;
94 componentWillUnmount () {
95 this.detachScrollListener();
96 this.detachIntersectionObserver();
97 detachFullscreenListener(this.onFullScreenChange
);
100 onFullScreenChange
= () => {
101 this.setState({ fullscreen: isFullscreen() });
104 attachIntersectionObserver () {
105 this.intersectionObserverWrapper
.connect({
107 rootMargin: '300% 0px',
111 detachIntersectionObserver () {
112 this.intersectionObserverWrapper
.disconnect();
115 attachScrollListener () {
116 this.node
.addEventListener('scroll', this.handleScroll
);
119 detachScrollListener () {
120 this.node
.removeEventListener('scroll', this.handleScroll
);
123 getFirstChildKey (props
) {
124 const { children
} = props
;
125 let firstChild
= children
;
126 if (children
instanceof ImmutableList
) {
127 firstChild
= children
.get(0);
128 } else if (Array
.isArray(children
)) {
129 firstChild
= children
[0];
131 return firstChild
&& firstChild
.key
;
138 handleLoadMore
= (e
) => {
140 this.props
.onLoadMore();
143 handleMouseEnter
= () => {
144 this.setState({ mouseOver: true });
147 handleMouseLeave
= () => {
148 this.setState({ mouseOver: false });
152 const { children
, scrollKey
, trackScroll
, shouldUpdateScroll
, isLoading
, hasMore
, prepend
, emptyMessage
, onLoadMore
} = this.props
;
153 const { fullscreen
} = this.state
;
154 const childrenCount
= React
.Children
.count(children
);
156 const loadMore
= (hasMore
&& childrenCount
> 0 && onLoadMore
) ? <LoadMore visible
={!isLoading
} onClick
={this.handleLoadMore
} /> : null;
157 let scrollableArea
= null;
159 if (isLoading
|| childrenCount
> 0 || !emptyMessage
) {
161 <div className
={classNames('scrollable', { fullscreen
})} ref
={this.setRef
} onMouseEnter
={this.handleMouseEnter
} onMouseLeave
={this.handleMouseLeave
}>
162 <div role
='feed' className
='item-list'>
165 {React
.Children
.map(this.props
.children
, (child
, index
) => (
166 <IntersectionObserverArticleContainer
170 listLength
={childrenCount
}
171 intersectionObserverWrapper
={this.intersectionObserverWrapper
}
172 saveHeightKey
={trackScroll
? `${this.context.router.route.location.key}:${scrollKey}` : null}
175 </IntersectionObserverArticleContainer
>
184 <div className
='empty-column-indicator' ref
={this.setRef
}>
192 <ScrollContainer scrollKey
={scrollKey
} shouldUpdateScroll
={shouldUpdateScroll
}>
197 return scrollableArea
;