import BinarySearch from "../utils/BinarySearch" ; import { Dimension } from "./dependencies/LayoutProvider" ; import { Layout } from "./layoutmanager/LayoutManager" ; /***
 * Given an offset this utility can compute visible items. Also tracks previously visible items to compute items which get hidden or visible
 * Virtual renderer uses callbacks from this utility to main recycle pool and the render stack.
 * The utility optimizes finding visible indexes by using the last visible items. However, that can be slow if scrollToOffset is explicitly called.
 * We use binary search to optimize in most cases like while finding first visible item or initial offset. In future we'll also be using BS to speed up
 * scroll to offset.
 */ export interface Range { start : number ; end : number ; } export interface WindowCorrection { windowShift : number ; startCorrection : number ; endCorrection : number ; } export type TOnItemStatusChanged = ( ( all : number [ ] , now : number [ ] , notNow : number [ ] ) => void ) ; export default class ViewabilityTracker { public onVisibleRowsChanged : TOnItemStatusChanged | null ; public onEngagedRowsChanged : TOnItemStatusChanged | null ; private _currentOffset : number ; private _maxOffset : number ; private _renderAheadOffset : number ; private _visibleWindow : Range ; private _engagedWindow : Range ; private _relevantDim : Range ; private _isHorizontal : boolean ; private _windowBound : number ; private _visibleIndexes : number [ ] ; private _engagedIndexes : number [ ] ; private _layouts : Layout [ ] = [ ] ; private _actualOffset : number ; private _defaultCorrection : WindowCorrection ; constructor ( renderAheadOffset , initialOffset ) { this . _currentOffset = Math . max ( 0 , initialOffset ) ; this . _maxOffset = 0 ; this . _actualOffset = 0 ; this . _renderAheadOffset = renderAheadOffset ; this . _visibleWindow = { start : 0 , end : 0 } ; this . _engagedWindow = { start : 0 , end : 0 } ; this . _isHorizontal = false ; this . _windowBound = 0 ; this . _visibleIndexes = [ ] ; this . _engagedIndexes = [ ] ; this . onVisibleRowsChanged = null ; this . onEngagedRowsChanged = null ; this . _relevantDim = { start : 0 , end : 0 } ; this . _defaultCorrection = { startCorrection : 0 , endCorrection : 0 , windowShift : 0 } ; } public init ( windowCorrection ) { this . _doInitialFit ( this . _currentOffset , windowCorrection ) ; } public setLayouts ( layouts , maxOffset ) { this . _layouts = layouts ; this . _maxOffset = maxOffset ; } public setDimensions ( dimension , isHorizontal ) { this . _isHorizontal = isHorizontal ; this . _windowBound = isHorizontal ? dimension . width : dimension . height ; } public forceRefresh ( ) { const shouldForceScroll = this . _currentOffset >= ( this . _maxOffset - this . _windowBound ) ; this . forceRefreshWithOffset ( this . _currentOffset ) ; return shouldForceScroll ; } public forceRefreshWithOffset ( offset ) { this . _currentOffset = - 1 ; this . updateOffset ( offset , false , this . _defaultCorrection ) ; } public updateOffset ( offset , isActual , windowCorrection ) { let correctedOffset = offset ; if ( isActual ) { this . _actualOffset = offset ; correctedOffset = Math . min ( this . _maxOffset , Math . max ( 0 , offset + ( windowCorrection . windowShift + windowCorrection . startCorrection ) ) ) ; } if ( this . _currentOffset !== correctedOffset ) { this . _currentOffset = correctedOffset ; this . _updateTrackingWindows ( offset , windowCorrection ) ; let startIndex = 0 ; if ( this . _visibleIndexes . length > 0 ) { startIndex = this . _visibleIndexes [ 0 ] ; } this . _fitAndUpdate ( startIndex ) ; } } public getLastOffset ( ) { return this . _currentOffset ; } public getLastActualOffset ( ) { return this . _actualOffset ; } public getEngagedIndexes ( ) { return this . _engagedIndexes ; } public findFirstLogicallyVisibleIndex ( ) { const relevantIndex = this . _findFirstVisibleIndexUsingBS ( 0.001 ) ; let result = relevantIndex ; for ( let i = relevantIndex - 1 ; i >= 0 ; i -- ) { if ( this . _isHorizontal ) { if ( this . _layouts [ relevantIndex ] . x !== this . _layouts [ i ] . x ) { break ; } else { result = i ; } } else { if ( this . _layouts [ relevantIndex ] . y !== this . _layouts [ i ] . y ) { break ; } else { result = i ; } } } return result ; } public updateRenderAheadOffset ( renderAheadOffset ) { this . _renderAheadOffset = Math . max ( 0 , renderAheadOffset ) ; this . forceRefreshWithOffset ( this . _currentOffset ) ; } public getCurrentRenderAheadOffset ( ) { return this . _renderAheadOffset ; } public setActualOffset ( actualOffset ) { this . _actualOffset = actualOffset ; } private _findFirstVisibleIndexOptimally ( ) { let firstVisibleIndex = 0 ; if ( this . _currentOffset > 5000 ) { firstVisibleIndex = this . _findFirstVisibleIndexUsingBS ( ) ; } else if ( this . _currentOffset > 0 ) { firstVisibleIndex = this . _findFirstVisibleIndexLinearly ( ) ; } return firstVisibleIndex ; } private _fitAndUpdate ( startIndex ) { const newVisibleItems = [ ] ; const newEngagedItems = [ ] ; this . _fitIndexes ( newVisibleItems , newEngagedItems , startIndex , true ) ; this . _fitIndexes ( newVisibleItems , newEngagedItems , startIndex + 1 , false ) ; this . _diffUpdateOriginalIndexesAndRaiseEvents ( newVisibleItems , newEngagedItems ) ; } private _doInitialFit ( offset , windowCorrection ) { offset = Math . min ( this . _maxOffset , Math . max ( 0 , offset ) ) ; this . _updateTrackingWindows ( offset , windowCorrection ) ; const firstVisibleIndex = this . _findFirstVisibleIndexOptimally ( ) ; this . _fitAndUpdate ( firstVisibleIndex ) ; } private _findFirstVisibleIndexLinearly ( ) { const count = this . _layouts . length ; let itemRect = null ; const relevantDim = { start : 0 , end : 0 } ; for ( let i = 0 ; i < count ; i ++ ) { itemRect = this . _layouts [ i ] ; this . _setRelevantBounds ( itemRect , relevantDim ) ; if ( this . _itemIntersectsVisibleWindow ( relevantDim . start , relevantDim . end ) ) { return i ; } } return 0 ; } private _findFirstVisibleIndexUsingBS ( bias = 0 ) { const count = this . _layouts . length ; return BinarySearch . findClosestHigherValueIndex ( count , this . _visibleWindow . start + bias , this . _valueExtractorForBinarySearch ) ; } private _valueExtractorForBinarySearch = ( index ) : number => { const itemRect = this . _layouts [ index ] ; this . _setRelevantBounds ( itemRect , this . _relevantDim ) ; return this . _relevantDim . end ; } ; private _fitIndexes ( newVisibleIndexes , newEngagedIndexes , startIndex , isReverse ) { const count = this . _layouts . length ; const relevantDim = { start : 0 , end : 0 } ; let i = 0 ; let atLeastOneLocated = false ; if ( startIndex < count ) { if ( ! isReverse ) { for ( i = startIndex ; i < count ; i ++ ) { if ( this . _checkIntersectionAndReport ( i , false , relevantDim , newVisibleIndexes , newEngagedIndexes ) ) { atLeastOneLocated = true ; } else { if ( atLeastOneLocated ) { break ; } } } } else { for ( i = startIndex ; i >= 0 ; i -- ) { if ( this . _checkIntersectionAndReport ( i , true , relevantDim , newVisibleIndexes , newEngagedIndexes ) ) { atLeastOneLocated = true ; } else { if ( atLeastOneLocated ) { break ; } } } } } } private _checkIntersectionAndReport ( index , insertOnTop , relevantDim , newVisibleIndexes , newEngagedIndexes ) { const itemRect = this . _layouts [ index ] ; let isFound = false ; this . _setRelevantBounds ( itemRect , relevantDim ) ; if ( this . _itemIntersectsVisibleWindow ( relevantDim . start , relevantDim . end ) ) { if ( insertOnTop ) { newVisibleIndexes . splice ( 0 , 0 , index ) ; newEngagedIndexes . splice ( 0 , 0 , index ) ; } else { newVisibleIndexes . push ( index ) ; newEngagedIndexes . push ( index ) ; } isFound = true ; } else if ( this . _itemIntersectsEngagedWindow ( relevantDim . start , relevantDim . end ) ) { if ( insertOnTop ) { newEngagedIndexes . splice ( 0 , 0 , index ) ; } else { newEngagedIndexes . push ( index ) ; } isFound = true ; } return isFound ; } private _setRelevantBounds ( itemRect , relevantDim ) { if ( this . _isHorizontal ) { relevantDim . end = itemRect . x + itemRect . width ; relevantDim . start = itemRect . x ; } else { relevantDim . end = itemRect . y + itemRect . height ; relevantDim . start = itemRect . y ; } } private _isItemInBounds ( window , itemBound ) { return ( window . start < itemBound && window . end > itemBound ) ; } private _isItemBoundsBeyondWindow ( window , startBound , endBound ) { return ( window . start >= startBound && window . end <= endBound ) ; } private _isZeroHeightEdgeElement ( window , startBound , endBound ) { return startBound - endBound === 0 && ( window . start === startBound || window . end === endBound ) ; } private _itemIntersectsWindow ( window , startBound , endBound ) { return this . _isItemInBounds ( window , startBound ) || this . _isItemInBounds ( window , endBound ) || this . _isItemBoundsBeyondWindow ( window , startBound , endBound ) || this . _isZeroHeightEdgeElement ( window , startBound , endBound ) ; } private _itemIntersectsEngagedWindow ( startBound , endBound ) { return this . _itemIntersectsWindow ( this . _engagedWindow , startBound , endBound ) ; } private _itemIntersectsVisibleWindow ( startBound , endBound ) { return this . _itemIntersectsWindow ( this . _visibleWindow , startBound , endBound ) ; } private _updateTrackingWindows ( offset , correction ) { const startCorrection = correction . windowShift + correction . startCorrection ; const bottomCorrection = correction . windowShift + correction . endCorrection ; const startOffset = offset + startCorrection ; const endOffset = ( offset + this . _windowBound ) + bottomCorrection ; this . _engagedWindow . start = Math . max ( 0 , startOffset - this . _renderAheadOffset ) ; this . _engagedWindow . end = endOffset + this . _renderAheadOffset ; this . _visibleWindow . start = startOffset ; this . _visibleWindow . end = endOffset ; } private _diffUpdateOriginalIndexesAndRaiseEvents ( newVisibleItems , newEngagedItems ) { this . _diffArraysAndCallFunc ( newVisibleItems , this . _visibleIndexes , this . onVisibleRowsChanged ) ; this . _diffArraysAndCallFunc ( newEngagedItems , this . _engagedIndexes , this . onEngagedRowsChanged ) ; this . _visibleIndexes = newVisibleItems ; this . _engagedIndexes = newEngagedItems ; } private _diffArraysAndCallFunc ( newItems , oldItems , func ) { if ( func ) { const now = this . _calculateArrayDiff ( newItems , oldItems ) ; const notNow = this . _calculateArrayDiff ( oldItems , newItems ) ; if ( now . length > 0 || notNow . length > 0 ) { func ( [ ... newItems ] , now , notNow ) ; } } } private _calculateArrayDiff ( arr1 , arr2 ) { const len = arr1 . length ; const diffArr = [ ] ; for ( let i = 0 ; i < len ; i ++ ) { if ( BinarySearch . findIndexOf ( arr2 , arr1 [ i ] ) === - 1 ) { diffArr . push ( arr1 [ i ] ) ; } } return diffArr ; } }