1 import React
, { PureComponent
} from 'react';
2 import { ScrollContainer
} from 'react-router-scroll';
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';
10 export default class ScrollableList
extends PureComponent
{
12 static contextTypes
= {
13 router: PropTypes
.object
,
17 scrollKey: PropTypes
.string
.isRequired
,
18 onScrollToBottom: PropTypes
.func
,
19 onScrollToTop: PropTypes
.func
,
20 onScroll: PropTypes
.func
,
21 trackScroll: PropTypes
.bool
,
22 shouldUpdateScroll: PropTypes
.func
,
23 isLoading: PropTypes
.bool
,
24 hasMore: PropTypes
.bool
,
25 prepend: PropTypes
.node
,
26 emptyMessage: PropTypes
.node
,
27 children: PropTypes
.node
,
30 static defaultProps
= {
38 intersectionObserverWrapper
= new IntersectionObserverWrapper();
40 handleScroll
= throttle(() => {
42 const { scrollTop
, scrollHeight
, clientHeight
} = this.node
;
43 const offset
= scrollHeight
- scrollTop
- clientHeight
;
44 this._oldScrollPosition
= scrollHeight
- scrollTop
;
46 if (400 > offset
&& this.props
.onScrollToBottom
&& !this.props
.isLoading
) {
47 this.props
.onScrollToBottom();
48 } else if (scrollTop
< 100 && this.props
.onScrollToTop
) {
49 this.props
.onScrollToTop();
50 } else if (this.props
.onScroll
) {
51 this.props
.onScroll();
58 handleMouseMove
= throttle(() => {
59 this._lastMouseMove
= new Date();
62 handleMouseLeave
= () => {
63 this._lastMouseMove
= null;
66 componentDidMount () {
67 this.attachScrollListener();
68 this.attachIntersectionObserver();
70 // Handle initial scroll posiiton
74 componentDidUpdate (prevProps
) {
75 const someItemInserted
= React
.Children
.count(prevProps
.children
) > 0 &&
76 React
.Children
.count(prevProps
.children
) < React
.Children
.count(this.props
.children
) &&
77 this.getFirstChildKey(prevProps
) !== this.getFirstChildKey(this.props
);
79 // Reset the scroll position when a new child comes in in order not to
80 // jerk the scrollbar around if you're already scrolled down the page.
81 if (someItemInserted
&& this._oldScrollPosition
&& this.node
.scrollTop
> 0) {
82 const newScrollTop
= this.node
.scrollHeight
- this._oldScrollPosition
;
84 if (this.node
.scrollTop
!== newScrollTop
) {
85 this.node
.scrollTop
= newScrollTop
;
88 this._oldScrollPosition
= this.node
.scrollHeight
- this.node
.scrollTop
;
92 componentWillUnmount () {
93 this.detachScrollListener();
94 this.detachIntersectionObserver();
97 attachIntersectionObserver () {
98 this.intersectionObserverWrapper
.connect({
100 rootMargin: '300% 0px',
104 detachIntersectionObserver () {
105 this.intersectionObserverWrapper
.disconnect();
108 attachScrollListener () {
109 this.node
.addEventListener('scroll', this.handleScroll
);
112 detachScrollListener () {
113 this.node
.removeEventListener('scroll', this.handleScroll
);
116 getFirstChildKey (props
) {
117 const { children
} = props
;
118 let firstChild
= children
;
119 if (children
instanceof ImmutableList
) {
120 firstChild
= children
.get(0);
121 } else if (Array
.isArray(children
)) {
122 firstChild
= children
[0];
124 return firstChild
&& firstChild
.key
;
131 handleLoadMore
= (e
) => {
133 this.props
.onScrollToBottom();
137 return this._lastMouseMove
!== null && ((new Date()) - this._lastMouseMove
< 600);
140 handleKeyDown
= (e
) => {
141 if (['PageDown', 'PageUp'].includes(e
.key
) || (e
.ctrlKey
&& ['End', 'Home'].includes(e
.key
))) {
142 const article
= (() => {
145 return e
.target
.nodeName
=== 'ARTICLE' && e
.target
.nextElementSibling
;
147 return e
.target
.nodeName
=== 'ARTICLE' && e
.target
.previousElementSibling
;
149 return this.node
.querySelector('[role="feed"] > article:last-of-type');
151 return this.node
.querySelector('[role="feed"] > article:first-of-type');
161 article
.scrollIntoView();
167 const { children
, scrollKey
, trackScroll
, shouldUpdateScroll
, isLoading
, hasMore
, prepend
, emptyMessage
} = this.props
;
168 const childrenCount
= React
.Children
.count(children
);
170 const loadMore
= (hasMore
&& childrenCount
> 0) ? <LoadMore visible
={!isLoading
} onClick
={this.handleLoadMore
} /> : null;
171 let scrollableArea
= null;
173 if (isLoading
|| childrenCount
> 0 || !emptyMessage
) {
175 <div className
='scrollable' ref
={this.setRef
} onMouseMove
={this.handleMouseMove
} onMouseLeave
={this.handleMouseLeave
}>
176 <div role
='feed' className
='item-list' onKeyDown
={this.handleKeyDown
}>
179 {React
.Children
.map(this.props
.children
, (child
, index
) => (
180 <IntersectionObserverArticleContainer
184 listLength
={childrenCount
}
185 intersectionObserverWrapper
={this.intersectionObserverWrapper
}
186 saveHeightKey
={trackScroll
? `${this.context.router.route.location.key}:${scrollKey}` : null}
189 </IntersectionObserverArticleContainer
>
198 <div className
='empty-column-indicator' ref
={this.setRef
}>
206 <ScrollContainer scrollKey
={scrollKey
} shouldUpdateScroll
={shouldUpdateScroll
}>
211 return scrollableArea
;