import { EventEmitter } from '@angular/core' ; import { PageScrollConfig } from './types/page-scroll.config' ; import { PageScrollTarget } from './types/page-scroll-target' ; import { PageScrollViews } from './types/page-scroll-view' ; import { EasingLogic } from './types/easing-logic' ; /**
 * An Interface specifying the possible options to be passed into the newInstance() factory method
 */ export interface PageScrollOptions extends PageScrollConfig { /**
     * The document object of the current app
     */ document : Document ; /**
     * A specification of the DOM element to scroll to. Either a string referring to an
     * element using a valid css selector (`#target`, `.class`, `div.class`) or a HTMLElement
     * that is attached to the document's DOM tree.
     */ scrollTarget : PageScrollTarget ; /**
     * Array of HTMLElements or the body object that should be manipulated while performing
     * the scroll animation.
     */ scrollViews ? : PageScrollViews [ ] ; /**
     * Maximum speed to be used for the scroll animation. Only taken
     * into account of no duration is provided
     */ speed ? : number ; /**
     * A listener to be called whenever the scroll animation stops
     */ scrollFinishListener ? : EventEmitter < boolean > ; namespace ? : string ; verticalScrolling ? : boolean ; duration ? : number ; scrollOffset ? : number ; advancedInlineOffsetCalculation ? : boolean ; interruptEvents ? : string [ ] ; interruptKeys ? : string [ ] ; interruptible ? : boolean ; scrollInView ? : boolean ; easingLogic ? : EasingLogic ; } /**
 * Represents a scrolling action
 */ export class PageScrollInstance { public pageScrollOptions : PageScrollOptions ; private isInlineScrolling : boolean ; private interruptListener : EventListenerOrEventListenerObject ; /**
     * These properties will be set/manipulated if the scroll animation starts
     */ public startScrollPosition = 0 ; public targetScrollPosition : number ; public distanceToScroll : number ; public startTime : number ; public endTime : number ; public executionDuration : number ; public interruptListenersAttached = false ; public timer : any = null ; /**
     * Private constructor, requires the properties assumed to be the bare minimum.
     * Use the factory methods to create instances:
     *       PageScrollService constructor ( pageScrollOptions ) { if ( ! pageScrollOptions . scrollViews || pageScrollOptions . scrollViews . length === 0 ) { pageScrollOptions . scrollViews = [ pageScrollOptions . document . documentElement , pageScrollOptions . document . body , pageScrollOptions . document . body . parentNode , ] ; this . isInlineScrolling = false ; } else { this . isInlineScrolling = true ; } this . pageScrollOptions = pageScrollOptions ; } private static getScrollingTargetPosition ( pageScrollOptions , scrollTargetElement ) { const body = pageScrollOptions . document . body ; const docEl = pageScrollOptions . document . documentElement ; const windowPageYOffset = pageScrollOptions . document . defaultView && pageScrollOptions . document . defaultView . pageYOffset || undefined ; const windowPageXOffset = pageScrollOptions . document . defaultView && pageScrollOptions . document . defaultView . pageXOffset || undefined ; const scrollTop = windowPageYOffset || docEl . scrollTop || body . scrollTop ; const scrollLeft = windowPageXOffset || docEl . scrollLeft || body . scrollLeft ; const clientTop = docEl . clientTop || body . clientTop || 0 ; const clientLeft = docEl . clientLeft || body . clientLeft || 0 ; if ( scrollTargetElement === undefined || scrollTargetElement === null ) { return { top : scrollTop , left : scrollLeft } ; } const box = scrollTargetElement . getBoundingClientRect ( ) ; const top = box . top + scrollTop - clientTop ; const left = box . left + scrollLeft - clientLeft ; return { top : Math . round ( top ) , left : Math . round ( left ) } ; } private static getInlineScrollingTargetPosition ( pageScrollOptions , scrollTargetElement ) { const position = { top : scrollTargetElement . offsetTop , left : scrollTargetElement . offsetLeft } ; if ( pageScrollOptions . advancedInlineOffsetCalculation && pageScrollOptions . scrollViews . length === 1 ) { const accumulatedParentsPos = { top : 0 , left : 0 } ; const theWindow = scrollTargetElement . ownerDocument . defaultView ; let parentFound = false ; let parent = scrollTargetElement . parentElement ; while ( ! parentFound && parent !== undefined && parent !== null ) { if ( theWindow . getComputedStyle ( parent ) . getPropertyValue ( 'position' ) === 'relative' ) { accumulatedParentsPos . top += parent . offsetTop ; accumulatedParentsPos . left += parent . offsetLeft ; } parent = parent . parentElement ; parentFound = parent === pageScrollOptions . scrollViews [ 0 ] ; } if ( parentFound ) { position . top += accumulatedParentsPos . top ; position . left += accumulatedParentsPos . left ; } else { } } return position ; } public getScrollPropertyValue ( scrollingView ) { if ( ! this . pageScrollOptions . verticalScrolling ) { return scrollingView . scrollLeft ; } return scrollingView . scrollTop ; } public getScrollClientPropertyValue ( scrollingView ) { if ( ! this . pageScrollOptions . verticalScrolling ) { return scrollingView . clientWidth ; } return scrollingView . clientHeight ; } /**
     * Extract the exact location of the scrollTarget element.
     *
     * Extract the scrollTarget HTMLElement from the given PageScrollTarget object. The latter one may be
     * a string like "#heading2", then this method returns the corresponding DOM element for that id.
     *
     */ public extractScrollTargetPosition ( ) { const scrollTargetElement = this . getScrollTargetElement ( ) ; if ( scrollTargetElement === null || scrollTargetElement === undefined ) { return { top : NaN , left : NaN } ; } if ( this . isInlineScrolling ) { return PageScrollInstance . getInlineScrollingTargetPosition ( this . pageScrollOptions , scrollTargetElement ) ; } return PageScrollInstance . getScrollingTargetPosition ( this . pageScrollOptions , scrollTargetElement ) ; } /**
     * Get the top offset of the scroll animation.
     * This automatically takes the offset location of the scrolling container/scrolling view
     * into account (for nested/inline scrolling).
     */ public getCurrentOffset ( ) { return this . pageScrollOptions . scrollOffset ; } return public setScrollPosition ( position ) { return this . pageScrollOptions . scrollViews . reduce ( ( oneAlreadyWorked , scrollingView ) => { const startScrollPropertyValue = this . getScrollPropertyValue ( scrollingView ) ; if ( scrollingView && startScrollPropertyValue !== undefined && startScrollPropertyValue !== null ) { const scrollDistance = Math . abs ( startScrollPropertyValue - position ) ; const isSmallMovement = scrollDistance < this . pageScrollOptions . _minScrollDistance ; if ( ! this . pageScrollOptions . verticalScrolling ) { scrollingView . scrollLeft = position ; } else { scrollingView . scrollTop = position ; } if ( isSmallMovement || scrollDistance > Math . abs ( this . getScrollPropertyValue ( scrollingView ) - position ) ) { return true ; } } return oneAlreadyWorked ; } , false ) ; } param value public fireEvent ( value ) { if ( this . pageScrollOptions . scrollFinishListener ) { this . pageScrollOptions . scrollFinishListener . emit ( value ) ; } } /**
     * Attach the interrupt listeners to the PageScrollInstance body. The given interruptReporter
     * will be called if any of the attached events is fired.
     *
     * Possibly attached interruptListeners are automatically removed from the body before the new one will be attached.
     */ public attachInterruptListeners ( interruptReporter ) { if ( this . interruptListenersAttached ) { this . detachInterruptListeners ( ) ; } this . interruptListener = ( event ) : void => { interruptReporter . report ( event , this ) ; } ; this . pageScrollOptions . interruptEvents . forEach ( ( event ) => this . pageScrollOptions . document . body . addEventListener ( event , this . interruptListener ) ) ; this . interruptListenersAttached = true ; } /**
     * Remove event listeners from the body and stop listening for events that might be treated as "animation
     * interrupt" events.
     */ public detachInterruptListeners ( ) { this . pageScrollOptions . interruptEvents . forEach ( ( event ) => this . pageScrollOptions . document . body . removeEventListener ( event , this . interruptListener ) ) ; this . interruptListenersAttached = false ; } private getScrollTargetElement ( ) { if ( typeof this . pageScrollOptions . scrollTarget === 'string' ) { const targetSelector = this . pageScrollOptions . scrollTarget as string ; if ( targetSelector . match ( /^#[^\s]+$/g ) !== null ) { return this . pageScrollOptions . document . getElementById ( targetSelector . substr ( 1 ) ) ; } return this . pageScrollOptions . document . querySelector ( targetSelector ) as HTMLElement ; } return this . pageScrollOptions . scrollTarget as HTMLElement ; } } /**
 * An Interface a listener should implement to be notified about possible interrupt events
 * that happened due to user interaction while a scroll animation takes place.
 *
 * The PageScrollService provides an implementation to a PageScrollInstance to be notified
 * about scroll animation interrupts and stop related animations.
 */ export interface InterruptReporter { report ( event , pageScrollInstance ) : void ; }