UNPKG

ngx-intersection-observer

Version:
328 lines (320 loc) 14.6 kB
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