Skip to content

Instantly share code, notes, and snippets.

@inorganik
Created November 26, 2019 17:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save inorganik/a9b3ac67cb1f6a38fcfdc0d7c12047b0 to your computer and use it in GitHub Desktop.
Save inorganik/a9b3ac67cb1f6a38fcfdc0d7c12047b0 to your computer and use it in GitHub Desktop.
Angular scroll spy directive
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