import * as React from 'react' ; import * as ReactDom from 'react-dom' ; import { on , off } from '../_utils/events' ; import scrollParent from '../_utils/scrollParent' ; import throttle from 'lodash/throttle' ; import debounce from 'lodash/debounce' ; export interface LazyLoadProps { once : boolean ; height : number | string ; offset : number | number [ ] ; overflow : boolean ; resize : boolean ; scroll : boolean ; children : React . ReactNode ; throttle : number | boolean ; debounce : number | boolean ; placeholder : React . ReactNode ; scrollContainer : string ; unmountIfInvisible : boolean ; } const defaultBoundingClientRect = { top : 0 , right : 0 , bottom : 0 , left : 0 , width : 0 , height : 0 } ; const LISTEN_FLAG = 'data-lazyload-listened' ; const listeners = [ ] ; let pending = [ ] ; let passiveEventSupported = false ; try { const opts = Object . defineProperty ( { } , 'passive' , { get ( ) { passiveEventSupported = true ; } , } ) ; window . addEventListener ( 'test' , ( ) => { } , opts ) ; } catch ( e ) { } const passiveEvent = passiveEventSupported ? { capture : false , passive : true } : false ; param { node } component param { node } parent return { bool } const checkOverflowVisible = function checkOverflowVisible ( component , parent ) { const node = ReactDom . findDOMNode ( component ) as HTMLElement ; let parentTop ; let parentHeight ; try { ( { top : parentTop , height : parentHeight } = parent . getBoundingClientRect ( ) ) ; } catch ( e ) { ( { top : parentTop , height : parentHeight } = defaultBoundingClientRect ) ; } const windowInnerHeight = window . innerHeight || document . documentElement . clientHeight ; const intersectionTop = Math . max ( parentTop , 0 ) ; const intersectionHeight = Math . min ( windowInnerHeight , parentTop + parentHeight ) - intersectionTop ; let top ; let height ; try { ( { top , height } = node . getBoundingClientRect ( ) ) ; } catch ( e ) { ( { top , height } = defaultBoundingClientRect ) ; } const offsetTop = top - intersectionTop ; const offsets = Array . isArray ( component . props . offset ) ? component . props . offset : [ component . props . offset , component . props . offset ] ; return offsetTop - offsets [ 0 ] <= intersectionHeight && offsetTop + height + offsets [ 1 ] >= 0 ; } ; param { node } component return { bool } const checkNormalVisible = function checkNormalVisible ( component ) { const node = ReactDom . findDOMNode ( component ) as HTMLElement ; if ( ! ( node . offsetWidth || node . offsetHeight || node . getClientRects ( ) . length ) ) return false ; let top ; let elementHeight ; try { ( { top , height : elementHeight } = node . getBoundingClientRect ( ) ) ; } catch ( e ) { ( { top , height : elementHeight } = defaultBoundingClientRect ) ; } const windowInnerHeight = window . innerHeight || document . documentElement . clientHeight ; const offsets = Array . isArray ( component . props . offset ) ? component . props . offset : [ component . props . offset , component . props . offset ] ; return top - offsets [ 0 ] <= windowInnerHeight && top + elementHeight + offsets [ 1 ] >= 0 ; } ; param { React } component const checkVisible = function checkVisible ( component ) { const node = ReactDom . findDOMNode ( component ) as HTMLElement ; if ( ! ( node instanceof HTMLElement ) ) { return ; } const parent = scrollParent ( node ) ; const isOverflow = component . props . overflow && parent !== node . ownerDocument && parent !== document && parent !== document . documentElement ; const visible = isOverflow ? checkOverflowVisible ( component , parent ) : checkNormalVisible ( component ) ; if ( visible ) { if ( ! component . visible ) { if ( component . props . once ) { pending . push ( component ) ; } component . visible = true ; component . forceUpdate ( ) ; } } else if ( ! ( component . props . once && component . visible ) ) { component . visible = false ; if ( component . props . unmountIfInvisible ) { component . forceUpdate ( ) ; } } } ; const purgePending = function purgePending ( ) { pending . forEach ( ( component ) => { const index = listeners . indexOf ( component ) ; if ( index !== - 1 ) { listeners . splice ( index , 1 ) ; } } ) ; pending = [ ] ; } ; const lazyLoadHandler = ( ) => { for ( let i = 0 ; i < listeners . length ; ++ i ) { const listener = listeners [ i ] ; checkVisible ( listener ) ; } purgePending ( ) ; } ; let delayType ; let finalLazyLoadHandler = null ; const isString = ( str ) => typeof str === 'string' ; class LazyLoad extends React . Component < LazyLoadProps , any > { static lazyload : ( options : LazyLoadProps ) => React . ReactNode ; static defaultProps = { once : false , offset : 0 , overflow : false , resize : false , scroll : true , unmountIfInvisible : false , } ; visible : boolean ; constructor ( props ) { super ( props ) ; this . visible = false ; } componentDidMount ( ) { let scrollport = window ; const { scrollContainer } = this . props ; if ( scrollContainer ) { if ( isString ( scrollContainer ) ) { scrollport = scrollport . document . querySelector ( scrollContainer ) as HTMLElement ; } } const needResetFinalLazyLoadHandler = ( this . props . debounce !== undefined && delayType === 'throttle' ) || ( delayType === 'debounce' && this . props . debounce === undefined ) ; if ( needResetFinalLazyLoadHandler ) { off ( scrollport , 'scroll' , finalLazyLoadHandler , passiveEvent ) ; off ( window , 'resize' , finalLazyLoadHandler , passiveEvent ) ; finalLazyLoadHandler = null ; } if ( ! finalLazyLoadHandler ) { if ( this . props . debounce !== undefined ) { finalLazyLoadHandler = debounce ( lazyLoadHandler , typeof this . props . debounce === 'number' ? this . props . debounce : 300 ) ; delayType = 'debounce' ; } else if ( this . props . throttle !== undefined ) { finalLazyLoadHandler = throttle ( lazyLoadHandler , typeof this . props . throttle === 'number' ? this . props . throttle : 300 ) ; delayType = 'throttle' ; } else { finalLazyLoadHandler = lazyLoadHandler ; } } if ( this . props . overflow ) { const parent = scrollParent ( ReactDom . findDOMNode ( this ) ) ; if ( parent && typeof parent . getAttribute === 'function' ) { const listenerCount = 1 + + parent . getAttribute ( LISTEN_FLAG ) ; if ( listenerCount === 1 ) { parent . addEventListener ( 'scroll' , finalLazyLoadHandler , passiveEvent ) ; } parent . setAttribute ( LISTEN_FLAG , listenerCount ) ; } } else if ( listeners . length === 0 || needResetFinalLazyLoadHandler ) { const { scroll , resize } = this . props ; if ( scroll ) { on ( scrollport , 'scroll' , finalLazyLoadHandler , passiveEvent ) ; } if ( resize ) { on ( window , 'resize' , finalLazyLoadHandler , passiveEvent ) ; } } listeners . push ( this ) ; checkVisible ( this ) ; } shouldComponentUpdate ( ) { return this . visible ; } componentWillUnmount ( ) { if ( this . props . overflow ) { const parent = scrollParent ( ReactDom . findDOMNode ( this ) ) ; if ( parent && typeof parent . getAttribute === 'function' ) { const listenerCount = + parent . getAttribute ( LISTEN_FLAG ) - 1 ; if ( listenerCount === 0 ) { parent . removeEventListener ( 'scroll' , finalLazyLoadHandler , passiveEvent ) ; parent . removeAttribute ( LISTEN_FLAG ) ; } else { parent . setAttribute ( LISTEN_FLAG , listenerCount ) ; } } } const index = listeners . indexOf ( this ) ; if ( index !== - 1 ) { listeners . splice ( index , 1 ) ; } if ( listeners . length === 0 && typeof window !== 'undefined' ) { off ( window , 'resize' , finalLazyLoadHandler , passiveEvent ) ; off ( window , 'scroll' , finalLazyLoadHandler , passiveEvent ) ; } } render ( ) { return this . visible ? ( this . props . children ) : this . props . placeholder ? ( this . props . placeholder ) : ( < div style = { { height : this . props . height } } className = "lazyload-placeholder" / > ) ; } } const getDisplayName = ( WrappedComponent ) => WrappedComponent . displayName || WrappedComponent . name || 'Component' ; const decorator = ( options ) => function lazyload ( WrappedComponent ) { return class LazyLoadDecorated extends React . Component { displayName = `LazyLoad${ getDisplayName ( WrappedComponent ) }` ; render ( ) { return ( < LazyLoad { ... options } > < WrappedComponent { ... this . props } / > < /LazyLoad>); } } ; } ; LazyLoad . lazyload = decorator ; export default LazyLoad ; export { lazyLoadHandler as forceCheck } ;