import React , { Component , ReactNode , CSSProperties } from 'react' ; import { throttle } from 'throttle-debounce' ; import { ThresholdUnits , parseThreshold } from './utils/threshold' ; type Fn = ( ) => any ; export interface Props { next : Fn ; hasMore : boolean ; children : ReactNode ; loader : ReactNode ; scrollThreshold ? : number | string ; endMessage ? : ReactNode ; style ? : CSSProperties ; height ? : number | string ; scrollableTarget ? : ReactNode ; hasChildren ? : boolean ; inverse ? : boolean ; pullDownToRefresh ? : boolean ; pullDownToRefreshContent ? : ReactNode ; releaseToRefreshContent ? : ReactNode ; pullDownToRefreshThreshold ? : number ; refreshFunction ? : Fn ; onScroll ? : ( e : MouseEvent ) => any ; dataLength : number ; initialScrollY ? : number ; className ? : string ; } interface State { showLoader : boolean ; pullToRefreshThresholdBreached : boolean ; prevDataLength : number | undefined ; } export default class InfiniteScroll extends Component < Props , State > { constructor ( props ) { super ( props ) ; this . state = { showLoader : false , pullToRefreshThresholdBreached : false , prevDataLength : props . dataLength , } ; this . throttledOnScrollListener = throttle ( 150 , this . onScrollListener ) . bind ( this ) ; this . onStart = this . onStart . bind ( this ) ; this . onMove = this . onMove . bind ( this ) ; this . onEnd = this . onEnd . bind ( this ) ; } private throttledOnScrollListener : ( e : MouseEvent ) => void ; private _scrollableNode : HTMLElement | undefined | null ; private el : HTMLElement | undefined | Window & typeof globalThis ; private _infScroll : HTMLDivElement | undefined ; private lastScrollTop = 0 ; private actionTriggered = false ; private _pullDown : HTMLDivElement | undefined ; private startY = 0 ; private currentY = 0 ; private dragging = false ; private maxPullDownDistance = 0 ; componentDidMount ( ) { if ( typeof this . props . dataLength === 'undefined' ) { throw new Error ( `mandatory prop "dataLength" is missing. The prop is needed` + ` when loading more content. Check README.md for usage` ) ; } this . _scrollableNode = this . getScrollableTarget ( ) ; this . el = this . props . height ? this . _infScroll : this . _scrollableNode || window ; if ( this . el ) { this . el . addEventListener ( 'scroll' , this . throttledOnScrollListener as EventListenerOrEventListenerObject ) ; } if ( typeof this . props . initialScrollY === 'number' && this . el && this . el instanceof HTMLElement && this . el . scrollHeight > this . props . initialScrollY ) { this . el . scrollTo ( 0 , this . props . initialScrollY ) ; } if ( this . props . pullDownToRefresh && this . el ) { this . el . addEventListener ( 'touchstart' , this . onStart ) ; this . el . addEventListener ( 'touchmove' , this . onMove ) ; this . el . addEventListener ( 'touchend' , this . onEnd ) ; this . el . addEventListener ( 'mousedown' , this . onStart ) ; this . el . addEventListener ( 'mousemove' , this . onMove ) ; this . el . addEventListener ( 'mouseup' , this . onEnd ) ; this . maxPullDownDistance = ( this . _pullDown && this . _pullDown . firstChild && ( this . _pullDown . firstChild as HTMLDivElement ) . getBoundingClientRect ( ) . height ) || 0 ; this . forceUpdate ( ) ; if ( typeof this . props . refreshFunction !== 'function' ) { throw new Error ( `Mandatory prop "refreshFunction" missing.
          Pull Down To Refresh functionality will not work
          as expected. Check README.md for usage'` ) ; } } } componentWillUnmount ( ) { if ( this . el ) { this . el . removeEventListener ( 'scroll' , this . throttledOnScrollListener as EventListenerOrEventListenerObject ) ; if ( this . props . pullDownToRefresh ) { this . el . removeEventListener ( 'touchstart' , this . onStart ) ; this . el . removeEventListener ( 'touchmove' , this . onMove ) ; this . el . removeEventListener ( 'touchend' , this . onEnd ) ; this . el . removeEventListener ( 'mousedown' , this . onStart ) ; this . el . removeEventListener ( 'mousemove' , this . onMove ) ; this . el . removeEventListener ( 'mouseup' , this . onEnd ) ; } } } componentDidUpdate ( prevProps ) { if ( this . props . dataLength === prevProps . dataLength ) return ; this . actionTriggered = false ; this . setState ( { showLoader : false , } ) ; } static getDerivedStateFromProps ( nextProps , prevState ) { const dataLengthChanged = nextProps . dataLength !== prevState . prevDataLength ; if ( dataLengthChanged ) { return { ... prevState , prevDataLength : nextProps . dataLength , } ; } return null ; } getScrollableTarget = ( ) => { if ( this . props . scrollableTarget instanceof HTMLElement ) return this . props . scrollableTarget ; if ( typeof this . props . scrollableTarget === 'string' ) { return document . getElementById ( this . props . scrollableTarget ) ; } if ( this . props . scrollableTarget === null ) { console . warn ( `You are trying to pass scrollableTarget but it is null. This might
        happen because the element may not have been added to DOM yet.
        See https://github.com/ankeetmaini/react-infinite-scroll-component/issues/59 for more info.
      ` ) ; } return null ; } ; onStart : EventListener = ( evt ) => { if ( this . lastScrollTop ) return ; this . dragging = true ; if ( evt instanceof MouseEvent ) { this . startY = evt . pageY ; } else if ( evt instanceof TouchEvent ) { this . startY = evt . touches [ 0 ] . pageY ; } this . currentY = this . startY ; if ( this . _infScroll ) { this . _infScroll . style . willChange = 'transform' ; this . _infScroll . style . transition = `transform 0.2s cubic-bezier(0,0,0.31,1)` ; } } ; onMove : EventListener = ( evt ) => { if ( ! this . dragging ) return ; if ( evt instanceof MouseEvent ) { this . currentY = evt . pageY ; } else if ( evt instanceof TouchEvent ) { this . currentY = evt . touches [ 0 ] . pageY ; } if ( this . currentY < this . startY ) return ; if ( this . currentY - this . startY >= Number ( this . props . pullDownToRefreshThreshold ) ) { this . setState ( { pullToRefreshThresholdBreached : true , } ) ; } if ( this . currentY - this . startY > this . maxPullDownDistance * 1.5 ) return ; if ( this . _infScroll ) { this . _infScroll . style . overflow = 'visible' ; this . _infScroll . style . transform = `translate3d(0px, ${ this . currentY - this . startY }px, 0px)` ; } } ; onEnd : EventListener = ( ) => { this . startY = 0 ; this . currentY = 0 ; this . dragging = false ; if ( this . state . pullToRefreshThresholdBreached ) { this . props . refreshFunction && this . props . refreshFunction ( ) ; this . setState ( { pullToRefreshThresholdBreached : false , } ) ; } requestAnimationFrame ( ( ) => { if ( this . _infScroll ) { this . _infScroll . style . overflow = 'auto' ; this . _infScroll . style . transform = 'none' ; this . _infScroll . style . willChange = 'unset' ; } } ) ; } ; isElementAtTop ( target , scrollThreshold = 0.8 ) { const clientHeight = target === document . body || target === document . documentElement ? window . screen . availHeight : target . clientHeight ; const threshold = parseThreshold ( scrollThreshold ) ; if ( threshold . unit === ThresholdUnits . Pixel ) { return ( target . scrollTop <= threshold . value + clientHeight - target . scrollHeight + 1 ) ; } return ( target . scrollTop <= threshold . value / 100 + clientHeight - target . scrollHeight + 1 ) ; } isElementAtBottom ( target , scrollThreshold = 0.8 ) { const clientHeight = target === document . body || target === document . documentElement ? window . screen . availHeight : target . clientHeight ; const threshold = parseThreshold ( scrollThreshold ) ; if ( threshold . unit === ThresholdUnits . Pixel ) { return ( target . scrollTop + clientHeight >= target . scrollHeight - threshold . value ) ; } return ( target . scrollTop + clientHeight >= ( threshold . value / 100 ) * target . scrollHeight ) ; } onScrollListener = ( event ) => { if ( typeof this . props . onScroll === 'function' ) { setTimeout ( ( ) => this . props . onScroll && this . props . onScroll ( event ) , 0 ) ; } const target = this . props . height || this . _scrollableNode ? ( event . target as HTMLElement ) : document . documentElement . scrollTop ? document . documentElement : document . body ; if ( this . actionTriggered ) return ; const atBottom = this . props . inverse ? this . isElementAtTop ( target , this . props . scrollThreshold ) : this . isElementAtBottom ( target , this . props . scrollThreshold ) ; if ( atBottom && this . props . hasMore ) { this . actionTriggered = true ; this . setState ( { showLoader : true } ) ; this . props . next && this . props . next ( ) ; } this . lastScrollTop = target . scrollTop ; } ; render ( ) { const style = { height : this . props . height || 'auto' , overflow : 'auto' , WebkitOverflowScrolling : 'touch' , ... this . props . style , } as CSSProperties ; const hasChildren = this . props . hasChildren || ! ! ( this . props . children && this . props . children instanceof Array && this . props . children . length ) ; const outerDivStyle = this . props . pullDownToRefresh && this . props . height ? { overflow : 'auto' } : { } ; return ( < div style = { outerDivStyle } className = "infinite-scroll-component__outerdiv" > < div className = { `infinite-scroll-component ${ this . props . className || '' }` } ref = { ( infScroll ) => ( this . _infScroll = infScroll ) } style = { style } > { this . props . pullDownToRefresh && ( < div style = { { position : 'relative' } } ref = { ( pullDown ) => ( this . _pullDown = pullDown ) } > < div style = { { position : 'absolute' , left : 0 , right : 0 , top : - 1 * this . maxPullDownDistance , } } > { this . state . pullToRefreshThresholdBreached ? this . props . releaseToRefreshContent : this . props . pullDownToRefreshContent } < /div> < /div>)} { this . props . children } { ! this . state . showLoader && ! hasChildren && this . props . hasMore && this . props . loader } { this . state . showLoader && this . props . hasMore && this . props . loader } { ! this . props . hasMore && this . props . endMessage } < /div> < /div>); } }