|
|
@ -10,6 +10,8 @@ import classNames from 'classnames'; |
|
|
|
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from 'flavours/glitch/util/fullscreen'; |
|
|
|
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from 'flavours/glitch/util/fullscreen'; |
|
|
|
import LoadingIndicator from './loading_indicator'; |
|
|
|
import LoadingIndicator from './loading_indicator'; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const MOUSE_IDLE_DELAY = 300; |
|
|
|
|
|
|
|
|
|
|
|
export default class ScrollableList extends PureComponent { |
|
|
|
export default class ScrollableList extends PureComponent { |
|
|
|
|
|
|
|
|
|
|
|
static contextTypes = { |
|
|
|
static contextTypes = { |
|
|
@ -38,6 +40,8 @@ export default class ScrollableList extends PureComponent { |
|
|
|
|
|
|
|
|
|
|
|
state = { |
|
|
|
state = { |
|
|
|
fullscreen: null, |
|
|
|
fullscreen: null, |
|
|
|
|
|
|
|
mouseMovedRecently: false, |
|
|
|
|
|
|
|
scrollToTopOnMouseIdle: false, |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
intersectionObserverWrapper = new IntersectionObserverWrapper(); |
|
|
|
intersectionObserverWrapper = new IntersectionObserverWrapper(); |
|
|
@ -61,6 +65,47 @@ export default class ScrollableList extends PureComponent { |
|
|
|
trailing: true, |
|
|
|
trailing: true, |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
mouseIdleTimer = null; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
clearMouseIdleTimer = () => { |
|
|
|
|
|
|
|
if (this.mouseIdleTimer === null) { |
|
|
|
|
|
|
|
return; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
clearTimeout(this.mouseIdleTimer); |
|
|
|
|
|
|
|
this.mouseIdleTimer = null; |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
handleMouseMove = throttle(() => { |
|
|
|
|
|
|
|
// As long as the mouse keeps moving, clear and restart the idle timer.
|
|
|
|
|
|
|
|
this.clearMouseIdleTimer(); |
|
|
|
|
|
|
|
this.mouseIdleTimer = |
|
|
|
|
|
|
|
setTimeout(this.handleMouseIdle, MOUSE_IDLE_DELAY); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.setState(({ |
|
|
|
|
|
|
|
mouseMovedRecently, |
|
|
|
|
|
|
|
scrollToTopOnMouseIdle, |
|
|
|
|
|
|
|
}) => ({ |
|
|
|
|
|
|
|
mouseMovedRecently: true, |
|
|
|
|
|
|
|
// Only set scrollToTopOnMouseIdle if we just started moving and were
|
|
|
|
|
|
|
|
// scrolled to the top. Otherwise, just retain the previous state.
|
|
|
|
|
|
|
|
scrollToTopOnMouseIdle: |
|
|
|
|
|
|
|
mouseMovedRecently |
|
|
|
|
|
|
|
? scrollToTopOnMouseIdle |
|
|
|
|
|
|
|
: (this.node.scrollTop === 0), |
|
|
|
|
|
|
|
})); |
|
|
|
|
|
|
|
}, MOUSE_IDLE_DELAY / 2); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
handleMouseIdle = () => { |
|
|
|
|
|
|
|
if (this.state.scrollToTopOnMouseIdle) { |
|
|
|
|
|
|
|
this.node.scrollTop = 0; |
|
|
|
|
|
|
|
this.props.onScrollToTop(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
this.setState({ |
|
|
|
|
|
|
|
mouseMovedRecently: false, |
|
|
|
|
|
|
|
scrollToTopOnMouseIdle: false, |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
componentDidMount () { |
|
|
|
componentDidMount () { |
|
|
|
this.attachScrollListener(); |
|
|
|
this.attachScrollListener(); |
|
|
|
this.attachIntersectionObserver(); |
|
|
|
this.attachIntersectionObserver(); |
|
|
@ -90,7 +135,7 @@ export default class ScrollableList extends PureComponent { |
|
|
|
const someItemInserted = React.Children.count(prevProps.children) > 0 && |
|
|
|
const someItemInserted = React.Children.count(prevProps.children) > 0 && |
|
|
|
React.Children.count(prevProps.children) < React.Children.count(this.props.children) && |
|
|
|
React.Children.count(prevProps.children) < React.Children.count(this.props.children) && |
|
|
|
this.getFirstChildKey(prevProps) !== this.getFirstChildKey(this.props); |
|
|
|
this.getFirstChildKey(prevProps) !== this.getFirstChildKey(this.props); |
|
|
|
if (someItemInserted && this.node.scrollTop > 0) { |
|
|
|
if ((someItemInserted && this.node.scrollTop > 0) || this.state.mouseMovedRecently) { |
|
|
|
return this.node.scrollHeight - this.node.scrollTop; |
|
|
|
return this.node.scrollHeight - this.node.scrollTop; |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
return null; |
|
|
|
return null; |
|
|
@ -104,6 +149,7 @@ export default class ScrollableList extends PureComponent { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
componentWillUnmount () { |
|
|
|
componentWillUnmount () { |
|
|
|
|
|
|
|
this.clearMouseIdleTimer(); |
|
|
|
this.detachScrollListener(); |
|
|
|
this.detachScrollListener(); |
|
|
|
this.detachIntersectionObserver(); |
|
|
|
this.detachIntersectionObserver(); |
|
|
|
detachFullscreenListener(this.onFullScreenChange); |
|
|
|
detachFullscreenListener(this.onFullScreenChange); |
|
|
@ -181,7 +227,7 @@ export default class ScrollableList extends PureComponent { |
|
|
|
); |
|
|
|
); |
|
|
|
} else if (isLoading || childrenCount > 0 || hasMore || !emptyMessage) { |
|
|
|
} else if (isLoading || childrenCount > 0 || hasMore || !emptyMessage) { |
|
|
|
scrollableArea = ( |
|
|
|
scrollableArea = ( |
|
|
|
<div className={classNames('scrollable', { fullscreen })} ref={this.setRef}> |
|
|
|
<div className={classNames('scrollable', { fullscreen })} ref={this.setRef} onMouseMove={this.handleMouseMove}> |
|
|
|
<div role='feed' className='item-list'> |
|
|
|
<div role='feed' className='item-list'> |
|
|
|
{prepend} |
|
|
|
{prepend} |
|
|
|
|
|
|
|
|
|
|
|