]> cat aescling's git repositories - mastodon.git/blob - app/javascript/mastodon/components/scrollable_list.js
Prevent timeline from moving when cursor is hovering over it (fixes #7278) (#7327)
[mastodon.git] / app / javascript / mastodon / components / scrollable_list.js
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
12 export default class ScrollableList extends PureComponent {
13
14 static contextTypes = {
15 router: PropTypes.object,
16 };
17
18 static propTypes = {
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,
30 };
31
32 static defaultProps = {
33 trackScroll: true,
34 };
35
36 state = {
37 fullscreen: null,
38 mouseOver: false,
39 };
40
41 intersectionObserverWrapper = new IntersectionObserverWrapper();
42
43 handleScroll = throttle(() => {
44 if (this.node) {
45 const { scrollTop, scrollHeight, clientHeight } = this.node;
46 const offset = scrollHeight - scrollTop - clientHeight;
47
48 if (400 > offset && this.props.onLoadMore && !this.props.isLoading) {
49 this.props.onLoadMore();
50 }
51
52 if (scrollTop < 100 && this.props.onScrollToTop) {
53 this.props.onScrollToTop();
54 } else if (this.props.onScroll) {
55 this.props.onScroll();
56 }
57 }
58 }, 150, {
59 trailing: true,
60 });
61
62 componentDidMount () {
63 this.attachScrollListener();
64 this.attachIntersectionObserver();
65 attachFullscreenListener(this.onFullScreenChange);
66
67 // Handle initial scroll posiiton
68 this.handleScroll();
69 }
70
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;
77 } else {
78 return null;
79 }
80 }
81
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;
87
88 if (this.node.scrollTop !== newScrollTop) {
89 this.node.scrollTop = newScrollTop;
90 }
91 }
92 }
93
94 componentWillUnmount () {
95 this.detachScrollListener();
96 this.detachIntersectionObserver();
97 detachFullscreenListener(this.onFullScreenChange);
98 }
99
100 onFullScreenChange = () => {
101 this.setState({ fullscreen: isFullscreen() });
102 }
103
104 attachIntersectionObserver () {
105 this.intersectionObserverWrapper.connect({
106 root: this.node,
107 rootMargin: '300% 0px',
108 });
109 }
110
111 detachIntersectionObserver () {
112 this.intersectionObserverWrapper.disconnect();
113 }
114
115 attachScrollListener () {
116 this.node.addEventListener('scroll', this.handleScroll);
117 }
118
119 detachScrollListener () {
120 this.node.removeEventListener('scroll', this.handleScroll);
121 }
122
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];
130 }
131 return firstChild && firstChild.key;
132 }
133
134 setRef = (c) => {
135 this.node = c;
136 }
137
138 handleLoadMore = (e) => {
139 e.preventDefault();
140 this.props.onLoadMore();
141 }
142
143 handleMouseEnter = () => {
144 this.setState({ mouseOver: true });
145 }
146
147 handleMouseLeave = () => {
148 this.setState({ mouseOver: false });
149 }
150
151 render () {
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);
155
156 const loadMore = (hasMore && childrenCount > 0 && onLoadMore) ? <LoadMore visible={!isLoading} onClick={this.handleLoadMore} /> : null;
157 let scrollableArea = null;
158
159 if (isLoading || childrenCount > 0 || !emptyMessage) {
160 scrollableArea = (
161 <div className={classNames('scrollable', { fullscreen })} ref={this.setRef} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
162 <div role='feed' className='item-list'>
163 {prepend}
164
165 {React.Children.map(this.props.children, (child, index) => (
166 <IntersectionObserverArticleContainer
167 key={child.key}
168 id={child.key}
169 index={index}
170 listLength={childrenCount}
171 intersectionObserverWrapper={this.intersectionObserverWrapper}
172 saveHeightKey={trackScroll ? `${this.context.router.route.location.key}:${scrollKey}` : null}
173 >
174 {child}
175 </IntersectionObserverArticleContainer>
176 ))}
177
178 {loadMore}
179 </div>
180 </div>
181 );
182 } else {
183 scrollableArea = (
184 <div className='empty-column-indicator' ref={this.setRef}>
185 {emptyMessage}
186 </div>
187 );
188 }
189
190 if (trackScroll) {
191 return (
192 <ScrollContainer scrollKey={scrollKey} shouldUpdateScroll={shouldUpdateScroll}>
193 {scrollableArea}
194 </ScrollContainer>
195 );
196 } else {
197 return scrollableArea;
198 }
199 }
200
201 }
This page took 0.140367 seconds and 4 git commands to generate.