import { ALIGNMENT } from './constants' ; export type ItemSizeGetter = ( index : number ) => number ; export type ItemSize = number | number [ ] | ItemSizeGetter ; export interface SizeAndPosition { size : number ; offset : number ; } interface SizeAndPositionData { [ id ] : SizeAndPosition ; } export interface Options { itemCount : number ; itemSizeGetter : ItemSizeGetter ; estimatedItemSize : number ; } export default class SizeAndPositionManager { private itemSizeGetter : ItemSizeGetter ; private itemCount : number ; private estimatedItemSize : number ; private lastMeasuredIndex : number ; private itemSizeAndPositionData : SizeAndPositionData ; constructor ( { itemCount , itemSizeGetter , estimatedItemSize } ) { this . itemSizeGetter = itemSizeGetter ; this . itemCount = itemCount ; this . estimatedItemSize = estimatedItemSize ; this . itemSizeAndPositionData = { } ; this . lastMeasuredIndex = - 1 ; } updateConfig ( { itemCount , itemSizeGetter , estimatedItemSize } ) { if ( itemCount != null ) { this . itemCount = itemCount ; } if ( estimatedItemSize != null ) { this . estimatedItemSize = estimatedItemSize ; } if ( itemSizeGetter != null ) { this . itemSizeGetter = itemSizeGetter ; } } getLastMeasuredIndex ( ) { return this . lastMeasuredIndex ; } /**
     * This method returns the size and position for the item at the specified index.
     * It just-in-time calculates (or used cached values) for items leading up to the index.
     */ getSizeAndPositionForIndex ( index ) { if ( index < 0 || index >= this . itemCount ) { throw Error ( `Requested index ${ index } is outside of range 0..${ this . itemCount }` ) ; } if ( index > this . lastMeasuredIndex ) { const lastMeasuredSizeAndPosition = this . getSizeAndPositionOfLastMeasuredItem ( ) ; let offset = lastMeasuredSizeAndPosition . offset + lastMeasuredSizeAndPosition . size ; for ( let i = this . lastMeasuredIndex + 1 ; i <= index ; i ++ ) { const size = this . itemSizeGetter ( i ) ; if ( size == null || isNaN ( size ) ) { throw Error ( `Invalid size returned for index ${ i } of value ${ size }` ) ; } this . itemSizeAndPositionData [ i ] = { offset , size , } ; offset += size ; } this . lastMeasuredIndex = index ; } return this . itemSizeAndPositionData [ index ] ; } getSizeAndPositionOfLastMeasuredItem ( ) { return this . lastMeasuredIndex >= 0 ? this . itemSizeAndPositionData [ this . lastMeasuredIndex ] : { offset : 0 , size : 0 } ; } /**
     * Total size of all items being measured.
     * This value will be completedly estimated initially.
     * As items as measured the estimate will be updated.
     */ getTotalSize ( ) { const lastMeasuredSizeAndPosition = this . getSizeAndPositionOfLastMeasuredItem ( ) ; return ( lastMeasuredSizeAndPosition . offset + lastMeasuredSizeAndPosition . size + ( this . itemCount - this . lastMeasuredIndex - 1 ) * this . estimatedItemSize ) ; } param align param containerSize return getUpdatedOffsetForIndex ( { align = ALIGNMENT . START , containerSize , currentOffset , targetIndex , } ) { if ( containerSize <= 0 ) { return 0 ; } const datum = this . getSizeAndPositionForIndex ( targetIndex ) ; const maxOffset = datum . offset ; const minOffset = maxOffset - containerSize + datum . size ; let idealOffset ; switch ( align ) { case ALIGNMENT . END : idealOffset = minOffset ; break ; case ALIGNMENT . CENTER : idealOffset = maxOffset - ( containerSize - datum . size ) / 2 ; break ; case ALIGNMENT . START : idealOffset = maxOffset ; break ; default : idealOffset = Math . max ( minOffset , Math . min ( maxOffset , currentOffset ) ) ; } const totalSize = this . getTotalSize ( ) ; return Math . max ( 0 , Math . min ( totalSize - containerSize , idealOffset ) ) ; } getVisibleRange ( { containerSize , offset , overscanCount , } ) { const totalSize = this . getTotalSize ( ) ; if ( totalSize === 0 ) { return { } ; } const maxOffset = offset + containerSize ; let start = this . findNearestItem ( offset ) ; if ( typeof start === 'undefined' ) { throw Error ( `Invalid offset ${ offset } specified` ) ; } const datum = this . getSizeAndPositionForIndex ( start ) ; offset = datum . offset + datum . size ; let stop = start ; while ( offset < maxOffset && stop < this . itemCount - 1 ) { stop ++ ; offset += this . getSizeAndPositionForIndex ( stop ) . size ; } if ( overscanCount ) { start = Math . max ( 0 , start - overscanCount ) ; stop = Math . min ( stop + overscanCount , this . itemCount - 1 ) ; } return { start , stop , } ; } /**
     * Clear all cached values for items after the specified index.
     * This method should be called for any item that has changed its size.
     * It will not immediately perform any calculations; they'll be performed the next time getSizeAndPositionForIndex() is called.
     */ resetItem ( index ) { this . lastMeasuredIndex = Math . min ( this . lastMeasuredIndex , index - 1 ) ; } /**
     * Searches for the item (index) nearest the specified offset.
     *
     * If no exact match is found the next lowest item index will be returned.
     * This allows partially visible items (with offsets just before/above the fold) to be visible.
     */ findNearestItem ( offset ) { if ( isNaN ( offset ) ) { throw Error ( `Invalid offset ${ offset } specified` ) ; } offset = Math . max ( 0 , offset ) ; const lastMeasuredSizeAndPosition = this . getSizeAndPositionOfLastMeasuredItem ( ) ; const lastMeasuredIndex = Math . max ( 0 , this . lastMeasuredIndex ) ; if ( lastMeasuredSizeAndPosition . offset >= offset ) { return this . binarySearch ( { high : lastMeasuredIndex , low : 0 , offset , } ) ; } else { return this . exponentialSearch ( { index : lastMeasuredIndex , offset , } ) ; } } private binarySearch ( { low , high , offset } ) { let middle = 0 ; let currentOffset = 0 ; while ( low <= high ) { middle = low + Math . floor ( ( high - low ) / 2 ) ; currentOffset = this . getSizeAndPositionForIndex ( middle ) . offset ; if ( currentOffset === offset ) { return middle ; } else if ( currentOffset < offset ) { low = middle + 1 ; } else if ( currentOffset > offset ) { high = middle - 1 ; } } if ( low > 0 ) { return low - 1 ; } return 0 ; } private exponentialSearch ( { index , offset } ) { let interval = 1 ; while ( index < this . itemCount && this . getSizeAndPositionForIndex ( index ) . offset < offset ) { index += interval ; interval *= 2 ; } return this . binarySearch ( { high : Math . min ( index , this . itemCount - 1 ) , low : Math . floor ( index / 2 ) , offset , } ) ; } }