ngx-intersection-observer
Version:
Intersection observer for Angular
328 lines (320 loc) • 14.6 kB
JavaScript
import * as i0 from '@angular/core';
import { Injectable, Optional, EventEmitter, Directive, Input, Output, NgModule } from '@angular/core';
import { ReplaySubject, fromEvent, EMPTY, debounceTime, distinctUntilChanged, merge, Subscription } from 'rxjs';
class IntersectionObserverConfig {
constructor() {
/** Debounces the intersection check. */
this.debounce = 50;
/** Specifies how many precentage of the element need to be visible to treat it as intersection. */
this.threshold = 30;
/** Automatically remove classes from the element. */
this.autoRemove = true;
/** Scroll Listener, false = IntersectionObserver */
this.useScroll = false;
}
}
class IntersectionObserverService {
constructor(intersectionObserverConfig) {
this.intersectionObserverConfig = intersectionObserverConfig;
this._windowScrollY$ = new ReplaySubject();
this._windowResize$ = new ReplaySubject();
this._windowViewportChange$ = new ReplaySubject();
this._pageYOffset = 0;
// Get the config or default
this._config = intersectionObserverConfig ? intersectionObserverConfig : {
debounce: 10,
threshold: 20
};
// Manage scroll position initially
this.manageScrollPos();
// Subscribe to window scroll event and debounce it
this.onScroll$ =
typeof window !== "undefined" ? fromEvent(window, "scroll") : EMPTY;
this.scrollSub = this.onScroll$
.pipe(debounceTime(this._config.debounce), distinctUntilChanged())
.subscribe(t => {
this.manageScrollPos();
this._windowScrollY$.next(this._pageYOffset);
});
// Subscribe to window resize event and debounce it
this.onResize$ =
typeof window !== "undefined" ? fromEvent(window, "resize") : EMPTY;
this.resizeSub = this.onResize$
.pipe(debounceTime(this._config.debounce), distinctUntilChanged())
.subscribe(t => {
this.manageScrollPos();
this._windowResize$.next(this._pageYOffset);
});
// Observable that fires on scroll or resize
this.viewportChangeSub = merge(this._windowScrollY$, this._windowResize$)
.subscribe(t => {
this._windowViewportChange$.next(this._pageYOffset);
});
}
/** Gets the page offset Y axis */
get pageYOffset() {
return this._pageYOffset;
}
/** Gets the intersection observer config */
get config() {
return this._config;
}
/** Gets an observable to the window scroll event */
get windowScrollY$() {
return this._windowScrollY$;
}
/** Gets an observable to the window resize event */
get windowResize$() {
return this._windowResize$;
}
/** Gets an observable to the window viewport event (combination of resize and scroll) */
get windowViewportChange$() {
return this._windowViewportChange$;
}
manageScrollPos() {
this._pageYOffset = typeof window !== "undefined" ? window.pageYOffset : 0;
}
ngOnDestroy() {
this.scrollSub.unsubscribe();
this.resizeSub.unsubscribe();
this.viewportChangeSub.unsubscribe();
}
}
IntersectionObserverService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.1.0", ngImport: i0, type: IntersectionObserverService, deps: [{ token: IntersectionObserverConfig, optional: true }], target: i0.ɵɵFactoryTarget.Injectable });
IntersectionObserverService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "14.1.0", ngImport: i0, type: IntersectionObserverService, providedIn: 'root' });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.1.0", ngImport: i0, type: IntersectionObserverService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}], ctorParameters: function () { return [{ type: IntersectionObserverConfig, decorators: [{
type: Optional
}] }]; } });
class IntersectionObserverDirective {
constructor(element, renderer, intersectionObserverService, intersectionObserverConfig) {
this.element = element;
this.renderer = renderer;
this.intersectionObserverService = intersectionObserverService;
this.intersectionObserverConfig = intersectionObserverConfig;
// Private fields
this._viewportChangeSub = new Subscription();
this._visitClass = [];
this._leaveClass = [];
this._removeVisitClass = [];
this._removeLeaveClass = [];
this._elementVisible = false;
this._hasClasses = false;
// Directive outputs
this.intersection = new EventEmitter(); // Event that fires once an element intersects.
}
ngOnInit() {
// Generate arrays of class stings
this._visitClass = this.getClassArray(this.visitClass ?? "");
this._leaveClass = this.getClassArray(this.leaveClass ?? "");
this._removeVisitClass = this.getClassArray(this.removeVisitClass ?? "");
this._removeLeaveClass = this.getClassArray(this.removeLeaveClass ?? "");
this._hasClasses = (this.visitClass || this.leaveClass || this.removeVisitClass || this.removeLeaveClass) ? true : false;
// Identify which intersection mechanism should be used
// (IntersectionObserver or Scroll Listener) default IntersectionObserver
let useScroll = this.intersectionObserverConfig?.useScroll;
useScroll = useScroll == undefined ? false : useScroll;
useScroll = this.useScroll == undefined ? useScroll : this.useScroll;
// Get threshold or default to 30
let threshold = this.intersectionObserverConfig?.threshold;
threshold = threshold == undefined ? 30 : threshold;
threshold = this.threshold == undefined ? threshold : this.threshold;
// Auto remove
let autoRemove = this.intersectionObserverConfig?.autoRemove;
autoRemove = autoRemove == undefined ? true : autoRemove;
autoRemove = this.autoRemove == undefined ? autoRemove : this.autoRemove;
// using intersecting observer by default, else fallback to scroll Listener
if ("IntersectionObserver" in window && !useScroll) {
const options = {
root: null,
threshold: threshold / 100,
rootMargin: "0px"
};
const observer = new IntersectionObserver((entries, _) => {
entries.forEach((entry) => {
this.handleIntersection(entry.isIntersecting);
});
}, options);
observer.observe(this.element.nativeElement);
return;
}
// Fallback to scroll listener
this._viewportChangeSub = this.intersectionObserverService.windowViewportChange$.subscribe(() => this.checkForIntersection());
}
/**
* Gets an array of classes.
* @param classString String with classes separated by whitespace.
* @returns An array with classes.
*/
getClassArray(classString) {
let classes = new Array();
classString.split(" ").forEach(cls => {
if (cls.trim()) {
classes.push(cls.trim());
}
});
return classes;
}
/**
* Checks if the element is visible within the viewport.
* @returns void
* */
checkForIntersection() {
const thresholdPx = (this.elementHeight / 100) * this.threshold;
const scrollTriggerMax = this.offsetTop + thresholdPx - this.winHeight;
const scrollTriggerMin = (this.offsetTop + (this.elementHeight - thresholdPx));
this.handleIntersection(this.intersectionObserverService.pageYOffset >= scrollTriggerMax &&
this.intersectionObserverService.pageYOffset <= scrollTriggerMin);
}
/**
*
* @param intersect Determines if the elements intersects with its viewport or not.
* @returns void
*/
handleIntersection(intersect) {
this._elementVisible = intersect;
this.handleClasses();
this.intersection.emit({ element: this.element, intersect: intersect });
}
/**
* Adds or removes classes on the element when it enters or leaves the viewport.
* @returns void
* */
handleClasses() {
// No classes, skip
if (!this._hasClasses)
return;
if (this._elementVisible) {
this.addClasses(this._visitClass);
if (this.autoRemove) {
this.removeClasses(this._leaveClass);
}
this.removeClasses(this._removeVisitClass);
}
else {
this.addClasses(this._leaveClass);
if (this.autoRemove) {
this.removeClasses(this._visitClass);
}
this.removeClasses(this._removeLeaveClass);
}
}
/**
* Helper to add a list of classes to the element.
* @param classes The list of classes to add.
* @returns void
*/
addClasses(classes) {
classes.forEach(cls => {
if (!this.element.nativeElement.classList.contains(cls)) {
this.renderer.addClass(this.element.nativeElement, cls);
}
});
}
/**
* Helper to remove a list of classes from the element.
* @param classes The list of classes to remove.
* @returns void
*/
removeClasses(classes) {
classes.forEach(cls => {
if (this.element.nativeElement.classList.contains(cls)) {
this.renderer.removeClass(this.element.nativeElement, cls);
}
});
}
/**
* Gets the height of the browser window.
* @returns the height of the browser window.
*/
get winHeight() {
return typeof window !== "undefined" ? window.innerHeight : 0;
}
/**
* Gets the offset of the element.
* @returns The elements offset.
*/
get offsetTop() {
if (typeof this.element.nativeElement.getBoundingClientRect === "function") {
const viewportTop = this.element.nativeElement.getBoundingClientRect().top;
return viewportTop + this.intersectionObserverService.pageYOffset - this.element.nativeElement.clientTop;
}
else {
return 0;
}
}
/**
* Gets the height of the element (Including border)
* @returns the height of the element.
*/
get elementHeight() {
return this.element.nativeElement.offsetHeight;
}
ngOnDestroy() {
this._viewportChangeSub.unsubscribe();
}
}
IntersectionObserverDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.1.0", ngImport: i0, type: IntersectionObserverDirective, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }, { token: IntersectionObserverService }, { token: IntersectionObserverConfig, optional: true }], target: i0.ɵɵFactoryTarget.Directive });
IntersectionObserverDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "14.1.0", type: IntersectionObserverDirective, selector: "[intersectionObserver]", inputs: { visitClass: "visitClass", leaveClass: "leaveClass", removeVisitClass: "removeVisitClass", removeLeaveClass: "removeLeaveClass", useScroll: "useScroll", threshold: "threshold", autoRemove: "autoRemove" }, outputs: { intersection: "intersection" }, ngImport: i0 });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.1.0", ngImport: i0, type: IntersectionObserverDirective, decorators: [{
type: Directive,
args: [{
selector: "[intersectionObserver]",
}]
}], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: i0.Renderer2 }, { type: IntersectionObserverService }, { type: IntersectionObserverConfig, decorators: [{
type: Optional
}] }]; }, propDecorators: { visitClass: [{
type: Input
}], leaveClass: [{
type: Input
}], removeVisitClass: [{
type: Input
}], removeLeaveClass: [{
type: Input
}], useScroll: [{
type: Input
}], threshold: [{
type: Input
}], autoRemove: [{
type: Input
}], intersection: [{
type: Output
}] } });
class IntersectionObserverModule {
static forRoot(config) {
return {
ngModule: IntersectionObserverModule,
providers: [
{ provide: IntersectionObserverConfig, useValue: config ? config : { debounce: 10, threshold: 30 }, multi: false },
IntersectionObserverService
]
};
}
}
IntersectionObserverModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.1.0", ngImport: i0, type: IntersectionObserverModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
IntersectionObserverModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "14.1.0", ngImport: i0, type: IntersectionObserverModule, declarations: [IntersectionObserverDirective], exports: [IntersectionObserverDirective] });
IntersectionObserverModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "14.1.0", ngImport: i0, type: IntersectionObserverModule });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.1.0", ngImport: i0, type: IntersectionObserverModule, decorators: [{
type: NgModule,
args: [{
declarations: [
IntersectionObserverDirective
],
imports: [],
exports: [
IntersectionObserverDirective
]
}]
}] });
/*
* Public API Surface of intersection-observer
*/
/**
* Generated bundle index. Do not edit.
*/
export { IntersectionObserverDirective, IntersectionObserverModule, IntersectionObserverService };
//# sourceMappingURL=ngx-intersection-observer.mjs.map