Created
November 26, 2019 17:06
-
-
Save inorganik/a9b3ac67cb1f6a38fcfdc0d7c12047b0 to your computer and use it in GitHub Desktop.
Angular scroll spy directive
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Directive, Inject, Input, Output, EventEmitter, OnInit, ElementRef } from '@angular/core'; | |
import { WINDOW } from '../../services/window.service'; | |
import { Router, NavigationEnd } from '@angular/router'; | |
import { fromEvent, Subject, Observable } from 'rxjs'; | |
import { map, distinctUntilChanged, filter, takeUntil, debounceTime } from 'rxjs/operators'; | |
/* | |
* Emits boolean event when element passes | |
* threshold of either fromBottom or fromTop, | |
* or emits when element is in view | |
*/ | |
@Directive({ | |
selector: '[appScrollSpy]' | |
}) | |
export class ScrollSpyDirective implements OnInit { | |
@Input() fromBottom: number; | |
@Input() fromTop: number; | |
@Input() inView = false; | |
@Input() emitTrueOnce = false; | |
@Output() scrollSpy = new EventEmitter<boolean>(); | |
destroyed$ = new Subject(); | |
scroll$: Observable<boolean>; | |
initial: boolean; | |
emittedTrue: boolean; | |
constructor( | |
@Inject(WINDOW) private window: any, | |
private el: ElementRef, | |
private router: Router | |
) { | |
// recalc for navigation change | |
this.router.events.pipe( | |
filter(event => event instanceof NavigationEnd), | |
takeUntil(this.destroyed$), | |
debounceTime(200) | |
).subscribe(() => this.scrollSpy.emit(this.calculatePosition())); | |
// recalc for content change (page height) | |
} | |
ngOnInit() { | |
// initial layout result | |
this.initial = this.calculatePosition(); | |
if (this.initial) { | |
this.emittedTrue = true; | |
} | |
this.scrollSpy.emit(this.calculatePosition()); | |
if (this.window && this.window.navigator) { | |
this.scroll$ = fromEvent(this.window, 'scroll').pipe( | |
map(() => this.calculatePosition()), | |
distinctUntilChanged(), | |
takeUntil(this.destroyed$) | |
); | |
// watch scrolling | |
if (this.emitTrueOnce) { | |
this.scroll$.pipe( | |
filter(result => result), | |
distinctUntilChanged() | |
).subscribe(result => this.scrollSpy.emit(result)); | |
} else { | |
this.scroll$.subscribe(result => this.scrollSpy.emit(result)); | |
} | |
} | |
} | |
calculatePosition(): boolean { | |
if (this.window.innerHeight) { | |
const bottomScrollPos = this.window.pageYOffset + this.window.innerHeight; | |
if (this.fromBottom) { | |
const distanceFromBottom = this.getDocumentHeight() - bottomScrollPos; | |
return distanceFromBottom > this.fromBottom; | |
} else if (this.fromTop) { | |
return this.window.pageYOffset > this.fromTop; | |
} else if (this.inView) { | |
const elHeight = this.el.nativeElement.offsetHeight; | |
const topOffset = this.el.nativeElement.offsetTop; | |
const bottomScrollOffset = topOffset + elHeight; | |
const aboveBottom = bottomScrollPos > bottomScrollOffset; | |
const belowTop = this.window.pageYOffset < topOffset; | |
return aboveBottom && belowTop; | |
} | |
} | |
return false; // was true | |
} | |
getDocumentHeight(): number { | |
if (this.window.document) { | |
const body = this.window.document.body, | |
html = this.window.document.documentElement; | |
return Math.max( | |
body.scrollHeight, | |
body.offsetHeight, | |
html.clientHeight, | |
html.scrollHeight, | |
html.offsetHeight | |
); | |
} else { | |
return 0; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment