import { Vue , Component , Prop , Watch } from 'vue-property-decorator' ; import _debounce from './debounce' ; @ Component export default class VirtualList extends Vue { @ Prop ( { type : Number , required : true } ) size : number ; @ Prop ( { type : Number , required : true } ) remain : number ; @ Prop ( { type : String , default : '' } ) wclass : string ; @ Prop ( { type : Boolean , default : false } ) pagemode : boolean ; @ Prop ( { type : Number , default : 0 } ) start : number ; @ Prop ( { type : Number , default : 0 } ) offset : number ; @ Prop ( { type : Function || Boolean , default : ( idx ) => false } ) variable : Function | boolean ; @ Prop ( { type : Number , default : 0 } ) bench : number ; @ Prop ( { type : Number , default : 0 } ) debounce : number ; @ Prop ( { type : Function || Boolean , default : ( ) => false } ) toTop : Function ; @ Prop ( { type : Function || Boolean , default : ( ) => false } ) tobottom : Function ; @ Prop ( { type : Function || Boolean , default : ( ) => false } ) onscroll : Function ; @ Prop ( { type : Object || null , default : ( ) => null } ) item : object | null ; @ Prop ( { type : Number , default : 0 } ) itemcount : number ; @ Prop ( { type : Function , default : ( ) => { } } ) itemprops : Function ; delta : { [ x ] : any ; } = this . initDelta ( ) ; changeProp : string = '' ; @ Watch ( 'size' ) onSizeChange ( ) { this . changeProp = 'size' ; } @ Watch ( 'remain' ) onRemainChange ( ) { this . changeProp = 'remain' ; } @ Watch ( 'bench' ) onBenchChange ( ) { this . changeProp = 'bench' ; this . itemModeForceRender ( ) ; } @ Watch ( 'start' ) onStartChange ( ) { this . changeProp = 'start' ; this . itemModeForceRender ( ) ; } @ Watch ( 'offset' ) onOffsetChange ( ) { this . changeProp = 'offset' ; this . itemModeForceRender ( ) ; } @ Watch ( 'itemcount' ) onItemcountChange ( ) { this . changeProp = 'itemcount' ; this . itemModeForceRender ( ) ; } private onScroll ( event ) { const delta = this . delta , vsl = this . $refs . vsl , offset = this . pagemode ? window . pageYOffset : ( ( vsl as any ) . $el || vsl ) . scrollTop || 0 ; delta . direction = offset > delta . scrollTop ? 'D' : 'U' ; delta . scrollTop = offset ; if ( delta . total > delta . keeps ) { this . updateZone ( offset ) ; } else { delta . end = delta . total - 1 ; } const offsetAll = delta . offsetAll ; if ( this . onscroll && typeof this . onscroll === 'function' ) this . onscroll ( event , { offset , offsetAll , start : delta . start , end : delta . end } ) ; if ( ! offset && delta . total ) this . fireEvent ( 'totop' ) ; if ( offset >= offsetAll ) this . fireEvent ( 'tobottom' ) ; } private updateZone ( offset ) { let delta = this . delta , overs = this . variable ? this . getVarOvers ( offset ) : Math . floor ( offset / this . size ) ; if ( delta . direction === 'U' ) overs = overs - this . remain + 1 ; const zone = this . getZone ( overs ) , bench = this . bench || this . remain ; const shouldRenderNextZone = Math . abs ( overs - delta . start - bench ) === 1 ; if ( ! shouldRenderNextZone && ( overs - delta . start <= bench ) && ! zone . isLast && ( overs > delta . start ) ) return ; if ( shouldRenderNextZone || zone . start !== delta . start || zone . end !== delta . end ) { delta . end = zone . end ; delta . start = zone . start ; this . forceRender ( ) ; } } private getZone ( index ) { let start , end ; const delta = this . delta ; index = parseInt ( `${ index }` , 10 ) ; index = Math . max ( 0 , index ) ; const lastStart = delta . total - delta . keeps ; const isLast = ( index <= delta . total && index >= lastStart ) || ( index > delta . total ) ; if ( isLast ) { end = delta . total - 1 ; start = Math . max ( 0 , lastStart ) ; } else { start = index ; end = start + delta . keeps - 1 ; } return { end , start , isLast } ; } private forceRender ( ) { window . requestAnimationFrame ( ( ) => { this . $forceUpdate ( ) ; } ) ; } private getVarOvers ( offset ) { let low = 0 ; let middle = 0 ; let middleOffset = 0 ; let delta = this . delta ; let high = delta . total ; while ( low <= high ) { middle = low + Math . floor ( ( high - low ) / 2 ) ; middleOffset = this . getVarOffset ( middle , null ) ; if ( ! delta . varAverSize ) delta . varAverSize = Math . floor ( middleOffset / middle ) ; if ( middleOffset === offset ) return middle ; if ( middleOffset < offset ) { low = middle + 1 ; } else if ( middleOffset > offset ) { high = middle - 1 ; } } return low > 0 ? -- low : 0 ; } private getVarOffset ( index , nocache ) { let delta = this . delta ; const cache = delta . varCache [ index ] ; if ( ! nocache && cache ) { return cache . offset ; } let offset = 0 ; for ( let i = 0 ; i < index ; i ++ ) { const size = this . getVarSize ( i , nocache ) ; delta . varCache [ i ] = { size : size , offset : offset } ; offset += size ; } delta . varLastCalcIndex = Math . max ( delta . varLastCalcIndex , index - 1 ) ; delta . varLastCalcIndex = Math . min ( delta . varLastCalcIndex , delta . total - 1 ) ; return offset ; } private getVarSize ( index , nocache ) { const cache = this . delta . varCache [ index ] ; if ( ! nocache && cache ) { return cache . size ; } if ( typeof this . variable === 'function' ) { return this . variable ( index ) || 0 ; } else { const slot = this . item ? ( this . $children [ index ] ? this . $children [ index ] . $vnode : null ) : ( this . $slots as any ) . default [ index ] ; const style = slot && slot . data && slot . data . style ; if ( style && style . height ) { const shm = style . height . match ( /^(.*)px$/ ) ; return ( shm && + shm [ 1 ] ) || 0 ; } } return 0 ; } private getVarPaddingTop ( ) { return this . getVarOffset ( this . delta . start , null ) ; } private getVarPaddingBottom ( ) { const delta = this . delta ; const last = delta . total - 1 ; if ( delta . total - delta . end <= delta . keeps || delta . varLastCalcIndex === last ) { return this . getVarOffset ( last , null ) - this . getVarOffset ( delta . end , null ) ; } else { return ( delta . total - delta . end ) * ( delta . varAverSize || this . size ) ; } } private getVarAllHeight ( ) { const delta = this . delta ; if ( delta . total - delta . end <= delta . keeps || delta . varLastCalcIndex === delta . total - 1 ) { return this . getVarOffset ( delta . total , null ) ; } else { return this . getVarOffset ( delta . start , null ) + ( delta . total - delta . end ) * ( delta . varAverSize || this . size ) ; } } private updateVariable ( index ) { this . getVarOffset ( index , true ) ; } private fireEvent ( event ) { if ( ( this as any ) [ event ] ) ( this as any ) [ event ] ( ) ; } private setScrollTop ( scrollTop ) { if ( this . pagemode ) { window . scrollTo ( 0 , scrollTop ) ; } else { let vsl = this . $refs . vsl ; if ( vsl ) { ( ( vsl as any ) . $el || vsl ) . scrollTop = scrollTop ; } } } private filter ( ) { let delta = this . delta ; const slots = this . $slots . default || [ ] ; if ( this . item ) { delta . total = this . itemcount ; if ( delta . keeps > delta . total ) { delta . end = delta . total - 1 ; } } else { if ( ! slots . length ) { delta . start = 0 ; } delta . total = slots . length ; } let paddingTop , paddingBottom , allHeight ; const hasPadding = delta . total > delta . keeps ; if ( this . variable ) { allHeight = this . getVarAllHeight ( ) ; paddingTop = hasPadding ? this . getVarPaddingTop ( ) : 0 ; paddingBottom = hasPadding ? this . getVarPaddingBottom ( ) : 0 ; } else { allHeight = this . size * delta . total ; paddingTop = this . size * ( hasPadding ? delta . start : 0 ) ; paddingBottom = this . size * ( hasPadding ? delta . total - delta . keeps : 0 ) - paddingTop ; } if ( paddingBottom < this . size ) { paddingBottom = 0 ; } if ( this . pagemode && this . $el && this . $el . parentElement ) { let bodyRect = document . body . getBoundingClientRect ( ) ; let elemRect = this . $el . parentElement . getBoundingClientRect ( ) ; let offset = elemRect . top - bodyRect . top ; paddingTop -= offset ; if ( paddingTop < 0 ) paddingTop = 0 ; } delta . paddingTop = paddingTop ; delta . paddingBottom = paddingBottom ; delta . offsetAll = allHeight - this . size * this . remain ; let renders = [ ] ; for ( let i = delta . start ; i < delta . total && i <= Math . ceil ( delta . end ) ; i ++ ) { let slot = null ; if ( this . item ) { const CustomRender = this . item ; slot = < CustomRender { ... this . itemprops ( i ) } / > ; } else { slot = slots [ i ] ; } renders . push ( slot ) ; } return renders ; } private itemModeForceRender ( ) { if ( this . item ) this . forceRender ( ) ; } private initDelta ( ) { const start = this . start >= this . remain ? this . start : 0 , keeps = this . remain + ( this . bench || this . remain ) ; return { direction : '' , scrollTop : 0 , start : start , end : start + keeps - 1 , keeps : keeps , total : 0 , offsetAll : 0 , paddingTop : 0 , paddingBottom : 0 , varCache : { } , varAverSize : 0 , varLastCalcIndex : 0 } ; } protected mounted ( ) { if ( this . pagemode ) { window . addEventListener ( 'scroll' , this . debounce ? _debounce ( this . onScroll . bind ( this ) , this . debounce , false ) : this . onScroll , false ) ; } if ( this . start ) { const start = this . getZone ( this . start ) . start ; this . setScrollTop ( this . variable ? this . getVarOffset ( start , null ) : start * this . size ) ; } else if ( this . offset ) { this . setScrollTop ( this . offset ) ; } } protected beforeDestroy ( ) { if ( this . pagemode ) { window . removeEventListener ( 'scroll' , this . debounce ? _debounce ( this . onScroll . bind ( this ) , this . debounce , false ) : this . onScroll , false ) ; } } protected beforeUpdate ( ) { let delta = this . delta ; delta . keeps = this . remain + ( this . bench || this . remain ) ; const calcstart = this . changeProp === 'start' ? this . start : delta . start ; const zone = this . getZone ( calcstart ) ; if ( this . changeProp && [ 'start' , 'size' , 'offset' ] . includes ( this . changeProp ) ) { const scrollTop = this . changeProp === 'offset' ? this . offset : this . variable ? this . getVarOffset ( zone . isLast ? delta . total : zone . start , null ) : zone . isLast && ( delta . total - calcstart <= this . remain ) ? delta . total * this . size : calcstart * this . size ; this . $nextTick ( this . setScrollTop . bind ( this , scrollTop ) ) ; } if ( this . changeProp || delta . end !== zone . end || calcstart !== zone . start ) { this . changeProp = '' ; delta . end = zone . end ; delta . start = zone . start ; this . forceRender ( ) ; } } protected render ( ) { const dbc = this . debounce , { paddingTop , paddingBottom } = this . delta , renderList = < div class = { this . wclass } role = "group" style = { { display : 'block' , paddingTop : paddingTop + 'px' , paddingBottom : paddingBottom + 'px' } } > { this . filter ( ) } < /div>; if ( this . pagemode ) return renderList ; return < div ref = "vsl" onScroll = { dbc ? _debounce ( this . onScroll . bind ( this ) , dbc , false ) : this . onScroll } style = { { display : 'block' , overflowY : 'auto' , height : `${ this . size * this . remain }px` } } > { renderList } < /div>; } }