import { AfterContentInit , ContentChildren , Directive , ElementRef , EventEmitter , Input , OnChanges , OnDestroy , OnInit , Output , QueryList , SimpleChanges } from '@angular/core' ; import { BehaviorSubject , from , fromEvent , Observable , of , Subject , Subscription } from 'rxjs' ; import { auditTime , combineLatest , filter as filterOperator , first , takeUntil } from 'rxjs/operators' ; import { InfiniteScrollLoadButtonDirective } from './infinite-scroll-load-button.directive' ; import { InfiniteScrollLoadingDirective } from './infinite-scroll-loading.directive' ; @ Directive ( { selector : '[uxInfiniteScroll]' , exportAs : 'uxInfiniteScroll' } ) export class InfiniteScrollDirective < T = any > implements OnInit , AfterContentInit , OnChanges , OnDestroy { @ Input ( 'uxInfiniteScroll' ) load : InfiniteScrollLoadFunction < T > ; @ Input ( 'collection' ) _collection : T [ ] = [ ] ; get collection ( ) { return this . _collection ; } set collection ( value ) { this . collectionChange . emit ( value ) ; this . _collection = value ; } @ Input ( ) set scrollElement ( element ) { this . _scrollElement = element instanceof ElementRef ? element : new ElementRef ( element ) ; } @ Input ( ) enabled : boolean = true ; @ Input ( ) filter : any ; @ Input ( ) loadOnInit : boolean = true ; @ Input ( ) loadOnScroll : boolean = true ; @ Input ( ) pageSize : number = 20 ; @ Output ( ) collectionChange = new EventEmitter < T [ ] > ( ) ; @ Output ( 'loading' ) loadingEvent = new EventEmitter < InfiniteScrollLoadingEvent > ( ) ; @ Output ( 'loaded' ) loadedEvent = new EventEmitter < InfiniteScrollLoadedEvent > ( ) ; @ Output ( 'loadError' ) loadErrorEvent = new EventEmitter < InfiniteScrollLoadErrorEvent > ( ) ; @ ContentChildren ( InfiniteScrollLoadButtonDirective ) private _loadButtonQuery : QueryList < InfiniteScrollLoadButtonDirective > ; @ ContentChildren ( InfiniteScrollLoadingDirective ) private _loadingIndicatorQuery : QueryList < InfiniteScrollLoadingDirective > ; private _pages : T [ ] [ ] ; private _nextPageNum = 0 ; private _domObserver : MutationObserver ; private _scrollEventSub : Subscription ; private _updateRequests = new Subject < InfiniteScrollRequest < T > > ( ) ; private _isLoading = new BehaviorSubject < boolean > ( false ) ; private _isExhausted = new BehaviorSubject < boolean > ( false ) ; private _loadButtonEnabled = new BehaviorSubject < boolean > ( false ) ; private _canLoadManually : Observable < boolean > ; private _scrollElement : ElementRef ; private _subscriptions : Subscription [ ] = [ ] ; private _loadButtonSubscriptions : Subscription [ ] = [ ] ; private _onDestroy = new Subject < void > ( ) ; constructor ( private _element ) { this . _canLoadManually = this . _isLoading . pipe ( combineLatest ( this . _isExhausted , this . _loadButtonEnabled , ( isLoading , isExhausted , loadButtonEnabled ) => { return ! isLoading && ! isExhausted && loadButtonEnabled ; } ) ) ; } ngOnInit ( ) { if ( ! this . _scrollElement ) { this . _scrollElement = this . _element ; } this . _loadButtonEnabled . next ( ! this . loadOnScroll ) ; } ngAfterContentInit ( ) { this . _updateRequests . pipe ( filterOperator ( request => request . check ) , auditTime ( 200 ) , takeUntil ( this . _onDestroy ) ) . subscribe ( this . doRequest . bind ( this ) ) ; this . _updateRequests . pipe ( filterOperator ( request => ! request . check ) , takeUntil ( this . _onDestroy ) ) . subscribe ( this . doRequest . bind ( this ) ) ; if ( this . enabled ) { this . attachEventHandlers ( ) ; } this . _canLoadManually . pipe ( takeUntil ( this . _onDestroy ) ) . subscribe ( canLoad => { this . _loadButtonQuery . forEach ( loadButton => { loadButton . visible = canLoad ; } ) ; } ) ; this . _isLoading . pipe ( takeUntil ( this . _onDestroy ) ) . subscribe ( isLoading => { this . _loadingIndicatorQuery . forEach ( loading => { loading . visible = isLoading ; } ) ; } ) ; this . attachLoadButtonEvents ( ) ; this . _loadButtonQuery . changes . pipe ( takeUntil ( this . _onDestroy ) ) . subscribe ( ( ) => { this . attachLoadButtonEvents ( ) ; } ) ; if ( this . loadOnInit ) { this . loadNextPage ( ) ; } } ngOnChanges ( changes ) { let check = true ; if ( changes . enabled && changes . enabled . currentValue !== changes . enabled . previousValue ) { if ( changes . enabled . currentValue ) { this . attachEventHandlers ( ) ; this . reset ( ) ; check = false ; } else { this . detachEventHandlers ( ) ; } } if ( this . enabled ) { if ( changes . filter && this . coerceFilter ( changes . filter . currentValue ) !== this . coerceFilter ( changes . filter . previousValue ) ) { this . reset ( ) ; check = false ; } if ( changes . loadOnScroll ) { this . _loadButtonEnabled . next ( ! changes . loadOnScroll . currentValue ) ; } if ( changes . pageSize && changes . pageSize . currentValue !== changes . pageSize . previousValue ) { this . reset ( ) ; check = false ; } this . _updateRequests . next ( { check , pageNumber : this . _nextPageNum , pageSize : this . pageSize , filter : this . coerceFilter ( this . filter ) as T } ) ; } } ngOnDestroy ( ) { this . detachEventHandlers ( ) ; this . _onDestroy . next ( ) ; this . _onDestroy . complete ( ) ; } /**
     * Request an additional page of data.
     */ loadNextPage ( ) { if ( ! this . enabled ) { return ; } this . _updateRequests . next ( { check : false , pageNumber : this . _nextPageNum , pageSize : this . pageSize , filter : this . coerceFilter ( this . filter ) as T } ) ; } /**
     * Request a check for whether an additional page of data is required. This is throttled.
     */ check ( ) { if ( ! this . enabled ) { return ; } this . _updateRequests . next ( { check : true , pageNumber : this . _nextPageNum , pageSize : this . pageSize , filter : this . coerceFilter ( this . filter ) as T } ) ; } /**
     * Clear the collection. Future requests will load from page 0.
     */ reset ( clearSubscriptions = true ) { if ( ! this . enabled ) { return ; } this . _nextPageNum = 0 ; if ( this . collection ) { this . collection . length = 0 ; } if ( this . _subscriptions && clearSubscriptions ) { this . _pages = [ ] ; this . _isExhausted . next ( false ) ; this . _subscriptions . forEach ( request => request . unsubscribe ( ) ) ; } } /**
     * Reload the data without clearing the view.
     */ reload ( ) { this . _pages ?. forEach ( ( page , i ) => this . reloadPage ( i ) ) ; } param pageNum reloadPage ( pageNum ) { if ( ! this . enabled ) { return ; } this . _updateRequests . next ( { check : false , pageNumber : pageNum , pageSize : this . pageSize , filter : this . coerceFilter ( this . filter ) as T , reload : true } ) ; } /** A filter value of null or undefined should be considered the same as an empty string */ private coerceFilter ( value ) { return value === undefined || value === null ? '' : value ; } /**
     * Attach scroll event handler and DOM observer.
     */ private attachEventHandlers ( ) { const target = this . _scrollElement . nativeElement instanceof HTMLHtmlElement ? document : this . _scrollElement . nativeElement ; this . _scrollEventSub = fromEvent ( target , 'scroll' ) . subscribe ( this . check . bind ( this ) ) ; this . _domObserver = new MutationObserver ( this . check . bind ( this ) ) ; this . _domObserver . observe ( this . _scrollElement . nativeElement , { childList : true , subtree : true } ) ; } /**
     * Detach scroll event handler and DOM observer.
     */ private detachEventHandlers ( ) { if ( this . _scrollEventSub ) { this . _scrollEventSub . unsubscribe ( ) ; this . _scrollEventSub = null ; } if ( this . _domObserver ) { this . _domObserver . disconnect ( ) ; this . _domObserver = null ; } } /**
     * Remove any existing event subscriptions for the load button `loading` event, then attach
     * subscriptions
     * for any in the query.
     */ private attachLoadButtonEvents ( ) { this . _loadButtonSubscriptions . forEach ( s => s . unsubscribe ( ) ) ; this . _loadButtonSubscriptions = this . _loadButtonQuery . map ( loadButton => loadButton . loading . subscribe ( this . loadNextPage . bind ( this ) ) ) ; } /**
     * Conditionally loads a page into the collection based on directive state and request parameters.
     */ private doRequest ( request ) { if ( this . needsData ( request ) && this . beginLoading ( request ) ) { const loadResult = this . load ( request . pageNumber , request . pageSize , request . filter ) ; if ( loadResult === null ) { return ; } const observable = Array . isArray ( loadResult ) ? of ( loadResult ) : from ( loadResult ) ; let completed = false ; const subscription = observable . pipe ( first ( ) ) . subscribe ( items => { if ( request . filter === this . coerceFilter ( this . filter ) && request . pageSize === this . pageSize ) { if ( items && items . length ) { this . setPageItems ( request . pageNumber , items ) ; } this . endLoading ( request , items ) ; } } , ( reason ) => { this . endLoadingWithError ( request , reason ) ; } , ( ) => { this . _subscriptions = this . _subscriptions . filter ( s => s !== subscription ) ; completed = true ; } ) ; if ( ! completed ) { this . _subscriptions . push ( subscription ) ; } } } /**
     * Returns true if the request should be fulfilled.
     */ private needsData ( request ) { if ( ! this . enabled ) { return false ; } if ( ! request . check ) { return true ; } if ( this . _isExhausted . getValue ( ) || this . _isLoading . getValue ( ) ) { return false ; } if ( this . _scrollElement && this . loadOnScroll ) { const element = this . _scrollElement . nativeElement ; const remainingScroll = element . scrollHeight - ( element . scrollTop + element . clientHeight ) ; const isVisible = element . scrollHeight > 0 ; return isVisible && remainingScroll <= element . clientHeight ; } return false ; } /**
     * Updates state for the beginning of a load. Returns false if the `loading` event was cancelled.
     */ private beginLoading ( request ) { const event = new InfiniteScrollLoadingEvent ( request . pageNumber , request . pageSize , request . filter ) ; this . loadingEvent . emit ( event ) ; this . _isLoading . next ( ! event . defaultPrevented ( ) ) ; return ! event . defaultPrevented ( ) ; } private setPageItems ( pageNum , items ) { this . _pages [ pageNum ] = items ; this . collection = this . _pages . reduce ( ( previous , current ) => previous . concat ( current ) , [ ] ) ; } /**
     * Updates state from a successful load. Raises the `loaded` event.
     */ private endLoading ( request , data ? ) { this . _isLoading . next ( false ) ; const isExhausted = ! ! ( data && data . length < this . pageSize ) ; this . _isExhausted . next ( isExhausted ) ; this . loadedEvent . emit ( new InfiniteScrollLoadedEvent ( request . pageNumber , request . pageSize , request . filter , data , isExhausted ) ) ; if ( ! request . reload ) { this . _nextPageNum += 1 ; } } /**
     * Updates state from a failed load. Raises the `loadError` event.
     */ private endLoadingWithError ( request , error ) { this . _isLoading . next ( false ) ; this . loadErrorEvent . emit ( new InfiniteScrollLoadErrorEvent < T > ( request . pageNumber , request . pageSize , request . filter , error ) ) ; } } /**
 * The internal data associated with a load/check request.
 */ class InfiniteScrollRequest < S = any > { check : boolean ; pageNumber : number ; pageSize : number ; filter : S ; reload ? : boolean ; } export type InfiniteScrollLoadFunction < T = any , S = any > = ( pageNum : number , pageSize : number , filter : S ) => T [ ] | Promise < T [ ] > ; /**
 * Event raised before the `loading` function is called.
 */ export class InfiniteScrollLoadingEvent < S = any > { private _defaultPrevented = false ; constructor ( /**
     * The index of the requested page, starting from 0.
     */ public pageNumber , /**
     * The number of items requested.
     */ public pageSize , /**
     * The filter details as provided via the `filter` binding.
     */ public filter ) { } /**
     * Prevents the default behaviour of the `loading` event (loading function will not be called).
     */ preventDefault ( ) { this . _defaultPrevented = true ; } defaultPrevented ( ) { return this . _defaultPrevented ; } } /**
 * Event raised when the loading function result has been resolved and added to the collection.
 */ export class InfiniteScrollLoadedEvent < T = any , S = any > { constructor ( /**
     * The index of the requested page, starting from 0.
     */ public pageNumber , /**
     * The number of items requested.
     */ public pageSize , /**
     * The filter details as provided via the `filter` binding.
     */ public filter , /**
     * The result of the promise returned from the loading function.
     */ public data , /**
     * True if the data is considered exhausted (number of items returned less than `pageSize`).
     */ public exhausted ) { } } /**
 * Event raised if the loading function returns a rejected promise.
 */ export class InfiniteScrollLoadErrorEvent < S = any > { constructor ( /**
     * The index of the requested page, starting from 0.
     */ public pageNumber , /**
     * The number of items requested.
     */ public pageSize , /**
     * The filter details as provided via the `filter` binding.
     */ public filter , /**
     * The object provided when rejecting the promise.
     */ public error ) { } }