import { END , HOME , PAGE_DOWN , PAGE_UP } from '@angular/cdk/keycodes' ; import { AfterViewInit , Component , ContentChild , ElementRef , HostListener , Input , OnDestroy , Optional , Self } from '@angular/core' ; import { Subject } from 'rxjs' ; import { takeUntil } from 'rxjs/operators' ; import { TabbableListService } from '../../../directives/accessibility/index' ; import { VirtualForDirective } from './virtual-for.directive' ; import { VirtualForRange , VirtualForService } from './virtual-for.service' ; @ Component ( { selector : '[uxVirtualForContainer]' , templateUrl : './virtual-for.component.html' , providers : [ VirtualForService ] , host : { '[style.position]' : '"relative"' } } ) export class VirtualForContainerComponent < T > implements AfterViewInit , OnDestroy { /** Define the height of each virtual item */ @ Input ( ) set itemSize ( itemSize ) { this . _virtualScroll . itemSize = itemSize ; if ( this . _initialized ) { requestAnimationFrame ( ( ) => { this . updateContainer ( ) ; this . virtualFor . updateContexts ( ) ; } ) ; } } get itemSize ( ) : number { return this . _virtualScroll . itemSize ; } /** Store the container height */ _totalHeight : number ; /** Keep a local reference of the dataset */ private _dataset : T [ ] = [ ] ; /** Store the current visible range */ private _range : VirtualForRange ; /** Indicate if the component has finished initialising */ private _initialized : boolean = false ; /** Unsubscribe from all observables */ private _onDestroy = new Subject < void > ( ) ; /** Determine if this is a table */ get _isTable ( ) : boolean { return this . _elementRef . nativeElement . tagName === 'TABLE' || this . _elementRef . nativeElement . tagName === 'TBODY' ; } /** Determine if this is a list */ get _isList ( ) : boolean { return this . _elementRef . nativeElement . tagName === 'OL' || this . _elementRef . nativeElement . tagName === 'UL' ; } /** Access the uxVirtualFor child directive */ @ ContentChild ( VirtualForDirective , { static : false } ) virtualFor : VirtualForDirective < T > ; constructor ( /** Get the ElementRef of the container element */ private _elementRef , /** A service to share values between the container and child elements */ private _virtualScroll , /** Handle key presses if there is a tabbable list */ @ Self ( ) @ Optional ( ) private _tabbableList ) { } ngAfterViewInit ( ) { this . _virtualScroll . dataset . pipe ( takeUntil ( this . _onDestroy ) ) . subscribe ( dataset => { this . _dataset = dataset ; requestAnimationFrame ( ( ) => { this . updateContainer ( ) ; this . _initialized = true ; } ) ; } ) ; } ngOnDestroy ( ) { this . _onDestroy . next ( ) ; this . _onDestroy . complete ( ) ; } @ HostListener ( 'scroll' ) updateContainer ( ) { if ( this . itemSize === 0 && this . _dataset . length > 0 ) { this . itemSize = this . virtualFor . getHeight ( this . _dataset [ 0 ] , this . _dataset . length ) ; } this . _totalHeight = this . _dataset . length * this . itemSize ; const height = this . getContainerHeight ( ) ; const itemCount = Math . ceil ( ( height / this . itemSize ) * 2 ) ; /** Determine the number of items we have as a top buffer */ const topBufferCount = Math . ceil ( ( height / this . itemSize ) * 0.5 ) ; const scrollOffset = this . getScrollOffset ( ) ; const startIdx = Math . max ( Math . floor ( scrollOffset / this . itemSize ) - Math . floor ( topBufferCount ) , 0 ) ; const endIdx = Math . min ( startIdx + itemCount , this . _dataset . length ) ; this . _range = { start : startIdx , end : endIdx } ; this . _virtualScroll . range . next ( this . _range ) ; } /** If cells are automatically getting their height detected you may want to update the size */ recalculateCellSize ( ) { this . itemSize = 0 ; } @ HostListener ( 'keydown' , [ '$event' , '$event.keyCode' ] ) onKeydown ( event , keyCode ) { if ( ! this . _tabbableList ) { return ; } switch ( keyCode ) { case PAGE_UP : this . _tabbableList . focusKeyManager . setFirstItemActive ( ) ; event . preventDefault ( ) ; break ; case PAGE_DOWN : this . _tabbableList . focusKeyManager . setLastItemActive ( ) ; event . preventDefault ( ) ; break ; case HOME : this . _tabbableList . shouldFocusOnChange = false ; this . _elementRef . nativeElement . scrollTop = 0 ; requestAnimationFrame ( ( ) => { this . _tabbableList . focusKeyManager . setFirstItemActive ( ) ; this . _tabbableList . shouldFocusOnChange = true ; } ) ; event . preventDefault ( ) ; break ; case END : this . _tabbableList . shouldFocusOnChange = false ; this . _elementRef . nativeElement . scrollTop = this . _elementRef . nativeElement . scrollHeight ; requestAnimationFrame ( ( ) => { this . _tabbableList . focusKeyManager . setLastItemActive ( ) ; this . _tabbableList . shouldFocusOnChange = true ; } ) ; event . preventDefault ( ) ; break ; } } private getScrollOffset ( ) { return ( this . _elementRef . nativeElement as HTMLElement ) . scrollTop ; } private getContainerHeight ( ) { return ( this . _elementRef . nativeElement as HTMLElement ) . clientHeight ; } }