import { ElementRef , Inject , Injectable , PLATFORM_ID , Optional } from '@angular/core' ; import { DOCUMENT , isPlatformBrowser } from '@angular/common' ; import { coerceElement } from '@angular/cdk/coercion' ; import { getRtlScrollAxisType , RtlScrollAxisType } from '@angular/cdk/platform' ; import { _Bottom , _Left , _Right , _Top , _Without } from '@angular/cdk/scrolling' ; import { fromEvent , merge , of , Observable , Subject , Subscriber , animationFrameScheduler } from 'rxjs' ; import { expand , finalize , take , takeUntil , takeWhile } from 'rxjs/operators' ; import BezierEasing from 'bezier-easing' ; import { SMOOTH_SCROLL_OPTIONS , SmoothScrollElement , SmoothScrollStep , SmoothScrollToElementOptions , SmoothScrollToOptions } from './smooth-scroll.model' ; @ Injectable ( { providedIn : 'root' } ) export class SmoothScrollManager { private readonly _defaultOptions : SmoothScrollToOptions ; private _onGoingScrolls = new Map < HTMLElement , Subject < void > > ( ) ; private get _w ( ) : Window { return this . _document . defaultView ; } /**
     * Timing method
     */ private get _now ( ) { return this . _w . performance && this . _w . performance . now ? this . _w . performance . now . bind ( this . _w . performance ) : Date . now ; } constructor ( @ Inject ( DOCUMENT ) private _document , @ Inject ( PLATFORM_ID ) private _platform , @ Optional ( ) @ Inject ( SMOOTH_SCROLL_OPTIONS ) customDefaultOptions ) { this . _defaultOptions = { duration : 468 , easing : { x1 : 0.42 , y1 : 0 , x2 : 0.58 , y2 : 1 } , ... customDefaultOptions , } ; } /**
     * changes scroll position inside an element
     */ private _scrollElement ( el , x , y ) { el . scrollLeft = x ; el . scrollTop = y ; } /**
     * Handles a given parameter of type HTMLElement, ElementRef or selector
     */ private _getElement ( el , parent ? ) { if ( typeof el === 'string' ) { return ( parent || this . _document ) . querySelector ( el ) ; } return coerceElement < HTMLElement > ( el ) ; } /**
     * Initializes a destroyer stream, re-initializes it if the element is already being scrolled
     */ private _initSmoothScroll ( el ) { if ( this . _onGoingScrolls . has ( el ) ) { this . _onGoingScrolls . get ( el ) ! . next ( ) ; } return this . _onGoingScrolls . set ( el , new Subject < void > ( ) ) ! . get ( el ) ! ; } /**
     * Checks if smooth scroll has reached, cleans up the smooth scroll stream and resolves its promise
     */ private _isFinished ( context , destroyed , resolve ) { if ( context . currentX !== context . x || context . currentY !== context . y ) { return true ; } destroyed . next ( ) ; resolve ( ) ; return false ; } /**
     * Terminates an ongoing smooth scroll
     */ private _interrupted ( el , destroyed ) { return merge ( fromEvent ( el , 'wheel' , { passive : true , capture : true } ) , fromEvent ( el , 'touchmove' , { passive : true , capture : true } ) , destroyed ) . pipe ( take ( 1 ) ) ; } /**
     * Deletes the destroyer function, runs if the smooth scroll has finished or interrupted
     */ private _destroy ( el , destroyed ) { destroyed . complete ( ) ; this . _onGoingScrolls . delete ( el ) ; } /**
     * A function called recursively that, given a context, steps through scrolling
     */ private _step ( context ) { return new Observable ( ( subscriber ) => { let elapsed = ( this . _now ( ) - context . startTime ) / context . duration ; elapsed = elapsed > 1 ? 1 : elapsed ; const value = context . easing ( elapsed ) ; context . currentX = context . startX + ( context . x - context . startX ) * value ; context . currentY = context . startY + ( context . y - context . startY ) * value ; this . _scrollElement ( context . scrollable , context . currentX , context . currentY ) ; animationFrameScheduler . schedule ( ( ) => subscriber . next ( context ) ) ; } ) ; } private _applyScrollToOptions ( el , options ) { if ( ! options . duration ! ) { this . _scrollElement ( el , options ! . left ! , options ! . top ! ) ; return Promise . resolve ( ) ; } const destroyed = this . _initSmoothScroll ( el ) ; const context = { scrollable : el , startTime : this . _now ( ) , startX : el . scrollLeft , startY : el . scrollTop , x : options . left == null ? el . scrollLeft : ~ ~ options . left , y : options . top == null ? el . scrollTop : ~ ~ options . top , duration : options . duration ! , easing : BezierEasing ( options . easing ! . x1 ! , options . easing ! . y1 ! , options . easing ! . x2 ! , options . easing ! . y2 ! ) } ; return new Promise ( resolve => { of ( null ) . pipe ( expand ( ( ) => this . _step ( context ) . pipe ( takeWhile ( ( currContext ) => this . _isFinished ( currContext , destroyed , resolve ) ) ) ) , takeUntil ( this . _interrupted ( el , destroyed ) ) , finalize ( ( ) => this . _destroy ( el , destroyed ) ) ) . subscribe ( ) ; } ) ; } param scrollable param customOptions scrollTo ( scrollable , customOptions ) { if ( isPlatformBrowser ( this . _platform ) ) { const el = this . _getElement ( scrollable ) ; const isRtl = getComputedStyle ( el ) . direction === 'rtl' ; const rtlScrollAxisType = getRtlScrollAxisType ( ) ; const options = { ... ( this . _defaultOptions as _Without < _Bottom & _Top > ) , ... customOptions , ... ( { left : customOptions . left == null ? ( isRtl ? customOptions . end : customOptions . start ) : customOptions . left , right : customOptions . right == null ? ( isRtl ? customOptions . start : customOptions . end ) : customOptions . right } as _Without < _Bottom & _Top > ) } ; if ( options . bottom != null ) { ( options as _Without < _Bottom > & _Top ) . top = el . scrollHeight - el . clientHeight - options . bottom ; } if ( isRtl && rtlScrollAxisType !== RtlScrollAxisType . NORMAL ) { if ( options . left != null ) { ( options as _Without < _Left > & _Right ) . right = el . scrollWidth - el . clientWidth - options . left ; } if ( rtlScrollAxisType === RtlScrollAxisType . INVERTED ) { options . left = options . right ; } else if ( rtlScrollAxisType === RtlScrollAxisType . NEGATED ) { options . left = options . right ? - options . right : options . right ; } } else { if ( options . right != null ) { ( options as _Without < _Right > & _Left ) . left = el . scrollWidth - el . clientWidth - options . right ; } } return this . _applyScrollToOptions ( el , options ) ; } return Promise . resolve ( ) ; } /**
     * Scroll to element by reference or selector
     */ scrollToElement ( scrollable , target , customOptions = { } ) { const scrollableEl = this . _getElement ( scrollable ) ; const targetEl = this . _getElement ( target , scrollableEl ) ; const options = { ... customOptions , ... { left : targetEl . offsetLeft + ( customOptions . left || 0 ) , top : targetEl . offsetTop + ( customOptions . top || 0 ) } } ; return targetEl ? this . scrollTo ( scrollableEl , options ) : Promise . resolve ( ) ; } }