import { Platform } from '@angular/cdk/platform' ; import { AfterViewInit , ChangeDetectionStrategy , ChangeDetectorRef , Component , ElementRef , EventEmitter , Inject , Input , NgZone , OnChanges , OnDestroy , Output , Renderer2 , SimpleChanges , ViewChild , ViewEncapsulation } from '@angular/core' ; import { fromEvent , Subject } from 'rxjs' ; import { takeUntil , throttleTime } from 'rxjs/operators' ; import { ThyAnchorLinkComponent } from './anchor-link.component' ; import { getOffset } from 'ngx-tethys/util' ; import { ThyScrollService } from 'ngx-tethys/core' ; import { DOCUMENT } from '@angular/common' ; interface Section { linkComponent : ThyAnchorLinkComponent ; top : number ; } const sharpMatcherRegx = /#([^#]+)$/ ; @ Component ( { selector : 'thy-anchor' , exportAs : 'thyAnchor' , preserveWhitespaces : false , template : `
        <thy-affix *ngIf="thyAffix; else content" [thyOffsetTop]="thyOffsetTop" [thyContainer]="container">
            <ng-template [ngTemplateOutlet]="content"></ng-template>
        </thy-affix>
        <ng-template #content>
            <div class="thy-anchor-wrapper" [ngStyle]="wrapperStyle">
                <div class="thy-anchor">
                    <div class="thy-anchor-ink">
                        <div class="thy-anchor-ink-full" #ink></div>
                    </div>
                    <ng-content></ng-content>
                </div>
            </div>
        </ng-template>
    ` , encapsulation : ViewEncapsulation . None , changeDetection : ChangeDetectionStrategy . OnPush } ) export class ThyAnchorComponent implements OnDestroy , AfterViewInit , OnChanges { @ ViewChild ( 'ink' ) private ink ! : ElementRef ; @ Input ( ) thyAffix = true ; @ Input ( ) thyBounds = 5 ; @ Input ( ) thyOffsetTop ? : number = undefined ; @ Input ( ) thyContainer ? : string | HTMLElement ; @ Input ( ) thyTarget : string | HTMLElement = '' ; @ Output ( ) readonly thyClick = new EventEmitter < ThyAnchorLinkComponent > ( ) ; @ Output ( ) readonly thyScroll = new EventEmitter < ThyAnchorLinkComponent > ( ) ; visible = false ; wrapperStyle = { 'max-height' : '100vh' } ; container ? : HTMLElement | Window ; private links : ThyAnchorLinkComponent [ ] = [ ] ; private animating = false ; private destroy$ = new Subject ( ) ; private handleScrollTimeoutID = - 1 ; constructor ( @ Inject ( DOCUMENT ) private document , private cdr , private platform , private zone , private renderer , private scrollService ) { } registerLink ( link ) { this . links . push ( link ) ; } unregisterLink ( link ) { this . links . splice ( this . links . indexOf ( link ) , 1 ) ; } private getContainer ( ) { return this . container || window ; } ngAfterViewInit ( ) { this . registerScrollEvent ( ) ; } ngOnDestroy ( ) { clearTimeout ( this . handleScrollTimeoutID ) ; this . destroy$ . next ( ) ; this . destroy$ . complete ( ) ; } private registerScrollEvent ( ) { if ( ! this . platform . isBrowser ) { return ; } this . destroy$ . next ( ) ; this . zone . runOutsideAngular ( ( ) => { fromEvent ( this . getContainer ( ) , 'scroll' ) . pipe ( throttleTime ( 50 ) , takeUntil ( this . destroy$ ) ) . subscribe ( ( ) => this . handleScroll ( ) ) ; } ) ; this . handleScrollTimeoutID = setTimeout ( ( ) => this . handleScroll ( ) ) ; } handleScroll ( ) { if ( typeof document === 'undefined' || this . animating ) { return ; } const container = this . container instanceof HTMLElement ? this . container : this . document ; const sections = [ ] ; const scope = ( this . thyOffsetTop || 0 ) + this . thyBounds ; this . links . forEach ( linkComponent => { const sharpLinkMatch = sharpMatcherRegx . exec ( linkComponent . thyHref . toString ( ) ) ; if ( ! sharpLinkMatch ) { return ; } const target = container . querySelector ( `#${ sharpLinkMatch [ 1 ] }` ) as HTMLElement ; if ( target ) { const top = getOffset ( target , this . getContainer ( ) ) . top ; if ( top < scope ) { sections . push ( { top , linkComponent } ) ; } } } ) ; this . visible = ! ! sections . length ; if ( ! this . visible ) { this . clearActive ( ) ; this . cdr . detectChanges ( ) ; } else { const maxSection = sections . reduce ( ( prev , curr ) => ( curr . top > prev . top ? curr : prev ) ) ; this . handleActive ( maxSection . linkComponent ) ; } this . setVisible ( ) ; } private clearActive ( ) { this . links . forEach ( i => { i . unsetActive ( ) ; } ) ; } private handleActive ( linkComponent ) { this . clearActive ( ) ; linkComponent . setActive ( ) ; const linkNode = linkComponent . getLinkTitleElement ( ) ; this . ink . nativeElement . style . top = `${ linkNode . offsetTop }px` ; this . ink . nativeElement . style . height = `${ linkNode . clientHeight }px` ; this . visible = true ; this . setVisible ( ) ; this . thyScroll . emit ( linkComponent ) ; } private setVisible ( ) { const visible = this . visible ; const visibleClassname = 'visible' ; if ( this . ink ) { if ( visible ) { this . renderer . addClass ( this . ink . nativeElement , visibleClassname ) ; } else { this . renderer . removeClass ( this . ink . nativeElement , visibleClassname ) ; } } } handleScrollTo ( linkComponent ) { const container = this . container instanceof HTMLElement ? this . container : this . document ; const linkElement = container . querySelector ( linkComponent . thyHref ) ; if ( ! linkElement ) { return ; } this . animating = true ; const containerScrollTop = this . scrollService . getScroll ( this . getContainer ( ) ) ; const elementOffsetTop = getOffset ( linkElement , this . getContainer ( ) ) . top ; const targetScrollTop = containerScrollTop + elementOffsetTop - ( this . thyOffsetTop || 0 ) ; this . scrollService . scrollTo ( this . getContainer ( ) , targetScrollTop , undefined , ( ) => { this . animating = false ; } ) ; this . handleActive ( linkComponent ) ; this . thyClick . emit ( linkComponent ) ; } ngOnChanges ( changes ) { const { thyOffsetTop , thyContainer } = changes ; if ( thyOffsetTop ) { this . wrapperStyle = { 'max-height' : `calc(100vh - ${ this . thyOffsetTop }px)` } ; } if ( thyContainer && this . thyContainer ) { const container = this . thyContainer || this . thyTarget ; this . container = typeof container === 'string' ? this . document . querySelector ( container ) : container ; this . registerScrollEvent ( ) ; } } }