import { inBrowser , remove , _ , throttle , supportWebp , getDPR , scrollParent , getBestSelectionFromSrcset , assign , isObject , hasIntersectionObserver , modeType , ImageCache } from './util' ; import LazyContainerMananger from './lazy-container' ; import { VueReactiveListener , VueLazyloadOptions } from '../types/index' ; import ReactiveListener from './listener' ; import { nextTick , VNode , DirectiveBinding } from 'vue' ; const DEFAULT_URL = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7' ; const DEFAULT_EVENTS = [ 'scroll' , 'wheel' , 'mousewheel' , 'resize' , 'animationend' , 'transitionend' , 'touchmove' ] ; const DEFAULT_OBSERVER_OPTIONS = { rootMargin : '0px' , threshold : 0 } ; type TeventType = 'loading' | 'loaded' | 'error' ; type Tlistener = { [ key ] : any ; state : { loading ? : boolean ; loaded ? : boolean ; error ? : boolean ; attempt ? : number ; rendered ? : boolean ; } ; el : any ; rect : object ; checkInView : ( ) => boolean ; load : ( ) => void ; } ; class Lazy { version : string ; mode : string ; ListenerQueue : Array < Tlistener > ; TargetIndex : number ; TargetQueue : Array < any > ; options : VueLazyloadOptions ; lazyLoadHandler : ( ) => void ; _imageCache : any ; _observer ? : IntersectionObserver | null ; lazyContainerMananger : LazyContainerMananger | null ; Event ! : { listeners : { loading : Array < any > ; loaded : Array < any > ; error : Array < any > ; } ; } ; $on ! : ( event : TeventType , func : Function ) => void ; $once ! : ( event : TeventType , func : Function ) => void ; $off ! : ( event : TeventType , func : Function ) => void ; $emit ! : ( event : TeventType , context : any , inCache : boolean ) => void ; constructor ( { preLoad , error , throttleWait , preLoadTop , dispatchEvent , loading , attempt , silent = true , scale , listenEvents , filter , adapter , observer , observerOptions } ) { this . version = '__VUE_LAZYLOAD_NEXT_VERSION__' ; this . lazyContainerMananger = null ; this . mode = modeType . event ; this . ListenerQueue = [ ] ; this . TargetIndex = 0 ; this . TargetQueue = [ ] ; this . options = { silent : silent , dispatchEvent : ! ! dispatchEvent , throttleWait : throttleWait || 200 , preLoad : preLoad || 1.3 , preLoadTop : preLoadTop || 0 , error : error || DEFAULT_URL , loading : loading || DEFAULT_URL , attempt : attempt || 3 , scale : scale || getDPR ( scale ) , listenEvents : listenEvents || DEFAULT_EVENTS , supportWebp : supportWebp ( ) , filter : filter || { } , adapter : adapter || { } , observer : ! ! observer , observerOptions : observerOptions || DEFAULT_OBSERVER_OPTIONS } ; this . _initEvent ( ) ; this . _imageCache = new ImageCache ( 200 ) ; this . lazyLoadHandler = throttle ( this . _lazyLoadHandler . bind ( this ) , this . options . throttleWait ! ) ; this . setMode ( this . options . observer ? modeType . observer : modeType . event ) ; } param { Object } config return config ( options = { } ) { assign ( this . options , options ) ; } return { Array } performance ( ) { const list = [ ] ; this . ListenerQueue . map ( item => list . push ( item . performance ( ) ) ) ; return list ; } addLazyBox ( vm ) { this . ListenerQueue . push ( vm ) ; if ( inBrowser ) { this . _addListenerTarget ( window ) ; this . _observer && this . _observer . observe ( vm . el ) ; if ( vm . $el && vm . $el . parentNode ) { this . _addListenerTarget ( vm . $el . parentNode ) ; } } } add ( el , binding , vnode ) { if ( this . ListenerQueue . some ( item => item . el === el ) ) { this . update ( el , binding ) ; return nextTick ( this . lazyLoadHandler ) ; } let { src , loading , error , cors } = this . _valueFormatter ( binding . value ) ; nextTick ( ( ) => { src = getBestSelectionFromSrcset ( el , this . options . scale as number ) || src ; this . _observer && this . _observer . observe ( el ) ; const container = Object . keys ( binding . modifiers ) [ 0 ] ; let $parent ; if ( container ) { $parent = binding . instance ! . $refs [ container ] ; $parent = $parent ? $parent . el || $parent : document . getElementById ( container ) ; } if ( ! $parent ) { $parent = scrollParent ( el ) ; } const newListener = new ReactiveListener ( el , src , error ! , loading ! , binding . arg ! , $parent , this . options , cors ! , this . _elRenderer . bind ( this ) , this . _imageCache ) ; this . ListenerQueue . push ( newListener ) ; if ( inBrowser ) { this . _addListenerTarget ( window ) ; this . _addListenerTarget ( $parent ) ; } nextTick ( ( ) => this . lazyLoadHandler ( ) ) ; } ) ; } param { DOM } el param { object } vue return update ( el , binding , vnode ? ) { let { src , loading , error } = this . _valueFormatter ( binding . value ) ; src = getBestSelectionFromSrcset ( el , this . options . scale ! ) || src ; const exist = this . ListenerQueue . find ( item => item . el === el ) ; if ( ! exist ) { this . add ( el , binding , vnode ! ) ; } else { exist . update ( { src , loading , error } ) ; } if ( this . _observer ) { this . _observer . unobserve ( el ) ; this . _observer . observe ( el ) ; } nextTick ( ( ) => this . lazyLoadHandler ( ) ) ; } param { DOM } el return remove ( el ) { if ( ! el ) return ; this . _observer && this . _observer . unobserve ( el ) ; const existItem = this . ListenerQueue . find ( item => item . el === el ) ; if ( existItem ) { this . _removeListenerTarget ( existItem . $parent ) ; this . _removeListenerTarget ( window ) ; remove ( this . ListenerQueue , existItem ) ; existItem . $destroy && existItem . $destroy ( ) ; } } removeComponent ( vm ) { if ( ! vm ) return ; remove ( this . ListenerQueue , vm ) ; this . _observer && this . _observer . unobserve ( vm . el ) ; if ( vm . $parent && vm . $el . parentNode ) { this . _removeListenerTarget ( vm . $el . parentNode ) ; } this . _removeListenerTarget ( window ) ; } setMode ( mode ) { if ( ! hasIntersectionObserver && mode === modeType . observer ) { mode = modeType . event ; } this . mode = mode ; if ( mode === modeType . event ) { if ( this . _observer ) { this . ListenerQueue . forEach ( listener => { this . _observer ! . unobserve ( listener . el as Element ) ; } ) ; this . _observer = null ; } this . TargetQueue . forEach ( target => { this . _initListen ( target . el , true ) ; } ) ; } else { this . TargetQueue . forEach ( target => { this . _initListen ( target . el , false ) ; } ) ; this . _initIntersectionObserver ( ) ; } } _addListenerTarget ( el ) { if ( ! el ) return ; let target = this . TargetQueue . find ( target => target . el === el ) ; if ( ! target ) { target = { el : el , id : ++ this . TargetIndex , childrenCount : 1 , listened : true } ; this . mode === modeType . event && this . _initListen ( target . el , true ) ; this . TargetQueue . push ( target ) ; } else { target . childrenCount ++ ; } return this . TargetIndex ; } _removeListenerTarget ( el ) { this . TargetQueue . forEach ( ( target , index ) => { if ( target . el === el ) { target . childrenCount -- ; if ( ! target . childrenCount ) { this . _initListen ( target . el , false ) ; this . TargetQueue . splice ( index , 1 ) ; target = null ; } } } ) ; } _initListen ( el , start ) { this . options . listenEvents ! . forEach ( ( evt ) => _ [ start ? 'on' : 'off' ] ( el , evt , this . lazyLoadHandler ) ) ; } _initEvent ( ) { this . Event = { listeners : { loading : [ ] , loaded : [ ] , error : [ ] } } ; this . $on = ( event , func ) => { if ( ! this . Event . listeners [ event ] ) this . Event . listeners [ event ] = [ ] ; this . Event . listeners [ event ] . push ( func ) ; } ; this . $once = ( event , func ) => { const vm = this ; function on ( ) { vm . $off ( event , on ) ; func . apply ( vm , arguments ) ; } this . $on ( event , on ) ; } ; this . $off = ( event , func ) => { if ( ! func ) { if ( ! this . Event . listeners [ event ] ) return ; this . Event . listeners [ event ] . length = 0 ; return ; } remove ( this . Event . listeners [ event ] , func ) ; } ; this . $emit = ( event , context , inCache ) => { if ( ! this . Event . listeners [ event ] ) return ; this . Event . listeners [ event ] . forEach ( func => func ( context , inCache ) ) ; } ; } return _lazyLoadHandler ( ) { const freeList = [ ] ; this . ListenerQueue . forEach ( ( listener , index ) => { if ( ! listener . el || ! listener . el . parentNode || listener . state . loaded ) { freeList . push ( listener ) ; } const catIn = listener . checkInView ( ) ; if ( ! catIn ) return ; if ( ! listener . state . loaded ) listener . load ( ) ; } ) ; freeList . forEach ( item => { remove ( this . ListenerQueue , item ) ; item . $destroy && item . $destroy ( ) ; } ) ; } return _initIntersectionObserver ( ) { if ( ! hasIntersectionObserver ) return ; this . _observer = new IntersectionObserver ( this . _observerHandler . bind ( this ) , this . options . observerOptions ) ; if ( this . ListenerQueue . length ) { this . ListenerQueue . forEach ( listener => { this . _observer ! . observe ( listener . el as Element ) ; } ) ; } } param { Array < IntersectionObserverEntry > } entries return _observerHandler ( entries ) { entries . forEach ( entry => { if ( entry . isIntersecting ) { this . ListenerQueue . forEach ( listener => { if ( listener . el === entry . target ) { if ( listener . state . loaded ) return this . _observer ! . unobserve ( listener . el as Element ) ; listener . load ( ) ; } } ) ; } } ) ; } param { ReactiveListener } lazyload param { TeventType } state param { bool } inCache return _elRenderer ( listener , state , cache ) { if ( ! listener . el ) return ; const { el , bindType } = listener ; let src ; switch ( state ) { case 'loading' : src = listener . loading ; break ; case 'error' : src = listener . error ; break ; default : src = listener . src ; break ; } if ( bindType ) { el . style [ bindType ] = 'url("' + src + '")' ; } else if ( el . getAttribute ( 'src' ) !== src ) { el . setAttribute ( 'src' , src ! ) ; } el . setAttribute ( 'lazy' , state ) ; this . $emit ( state , listener , cache ) ; this . options . adapter [ state ] && this . options . adapter [ state ] ( listener , this . options ) ; if ( this . options . dispatchEvent ) { const event = new CustomEvent ( state , { detail : listener } ) ; el . dispatchEvent ( event ) ; } } param { string } image return { object } _valueFormatter ( value ) { let src = value ; let loading = this . options . loading ; let error = this . options . error ; let cors = this . options . cors ; if ( isObject ( value ) ) { if ( ! value . src && ! this . options . silent ) console . error ( 'Vue Lazyload Next warning: miss src with ' + value ) ; src = value . src ; loading = value . loading || this . options . loading ; error = value . error || this . options . error ; } return { src , loading , error , cors } ; } } export default Lazy ;