UNPKG

ng-spy

Version:

A lightweight, dependecy-free scrollspy for angular. Use this library to spy on HTML elements on your page when the window is scrolled or resized.

291 lines (280 loc) 13.2 kB
import * as i0 from '@angular/core'; import { InjectionToken, PLATFORM_ID, Injectable, Inject, Directive, Input, NgModule } from '@angular/core'; import { EMPTY, fromEvent, Subject } from 'rxjs'; import { auditTime, takeUntil } from 'rxjs/operators'; import { isPlatformBrowser } from '@angular/common'; const RESIZE_TIME_THRESHOLD = new InjectionToken('Time in milli-seconds', { providedIn: 'root', factory: () => 300 }); const SCROLL_TIME_THRESHOLD = new InjectionToken('Time in milli-seconds', { providedIn: 'root', factory: () => 10 }); class WindowService { constructor(platformId, resizeTime, scrollTime) { this.resizeTime = resizeTime; this.scrollTime = scrollTime; this.isBrowser = true; if (!isPlatformBrowser(platformId)) { this.isBrowser = false; this.scrollEvent$ = this.resizeEvent$ = EMPTY; } else { this.scrollEvent$ = fromEvent(window, 'scroll', { passive: true }).pipe(auditTime(this.scrollTime)); this.resizeEvent$ = fromEvent(window, 'resize', { passive: true }).pipe(auditTime(this.resizeTime)); } } getScrollEventForContainer(scrollContainer) { if (!this.isBrowser) { return EMPTY; } return fromEvent(scrollContainer.nativeElement, 'scroll', { passive: true }).pipe(auditTime(this.scrollTime)); } get scrollEvent() { return this.scrollEvent$; } get resizeEvent() { return this.resizeEvent$; } get scrollTop() { if (!this.isBrowser) { return 0; } return Math.max(window.pageYOffset, document.documentElement.scrollTop, document.body.scrollTop); } get viewportHeight() { if (!this.isBrowser) { return 0; } return Math.max(document.documentElement.clientHeight, window.innerHeight || 0); } getElementHeight(el) { if (!this.isBrowser) { return 0; } return el.nativeElement.offsetHeight; } getElementOffsetTop(el) { if (!this.isBrowser) { return 0; } return el.nativeElement.offsetTop; } getElementScrollTop(el) { if (!this.isBrowser) { return 0; } return el.nativeElement.scrollTop; } } WindowService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.1.0", ngImport: i0, type: WindowService, deps: [{ token: PLATFORM_ID }, { token: RESIZE_TIME_THRESHOLD }, { token: SCROLL_TIME_THRESHOLD }], target: i0.ɵɵFactoryTarget.Injectable }); WindowService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "14.1.0", ngImport: i0, type: WindowService, providedIn: 'root' }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.1.0", ngImport: i0, type: WindowService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: function () { return [{ type: undefined, decorators: [{ type: Inject, args: [PLATFORM_ID] }] }, { type: undefined, decorators: [{ type: Inject, args: [RESIZE_TIME_THRESHOLD] }] }, { type: undefined, decorators: [{ type: Inject, args: [SCROLL_TIME_THRESHOLD] }] }]; } }); class ScrollSpyService { constructor(windowService) { this.windowService = windowService; this.stopSpying$ = new Subject(); this.activeSpyTarget$ = new Subject(); this.spyTargets = []; this.thresholdTop = 0; this.thresholdBottom = 0; this.isSpying = false; this.scrollEvent = this.windowService.scrollEvent.pipe(takeUntil(this.stopSpying$)); this.resizeEvent = this.windowService.resizeEvent.pipe(takeUntil(this.stopSpying$)); } spy({ scrollContainer, thresholdTop = 0, thresholdBottom = 0 } = {}) { // this is to prevent duplicate listeners if (this.isSpying) { return; } this.isSpying = true; this.scrollContainer = scrollContainer; this.thresholdTop = thresholdTop; this.thresholdBottom = thresholdBottom; this.scrollEvent.subscribe(() => this.checkActiveElement(scrollContainer)); this.resizeEvent.subscribe(() => this.checkActiveElement(scrollContainer)); if (scrollContainer != null) { this.windowService.getScrollEventForContainer(scrollContainer) .pipe(takeUntil(this.stopSpying$)) .subscribe(() => this.checkActiveElement(scrollContainer)); } this.checkActiveElement(scrollContainer); } addTarget(target) { this.spyTargets.push(Object.assign({}, target)); this.checkActiveElement(this.scrollContainer); } removeTarget(target) { this.spyTargets = this.spyTargets.filter(spyTarget => target !== spyTarget.name); this.checkActiveElement(this.scrollContainer); } checkActiveElement(scrollContainer) { let activeTarget = null; for (const target of this.spyTargets) { const activeElement = activeTarget != null ? activeTarget.element : null; if (this.isElementActive(target.element, scrollContainer, activeElement)) { activeTarget = target; } } this.activeSpyTarget$.next(activeTarget ? activeTarget.name : null); } isElementActive(element, scrollContainer, currentActiveElement) { const targetOffsetTop = this.windowService.getElementOffsetTop(element); const targetHeight = this.windowService.getElementHeight(element); if (currentActiveElement != null && this.windowService.getElementOffsetTop(currentActiveElement) > targetOffsetTop) { return false; } const hasContainer = (scrollContainer != null); const isInsideWindow = this.isElementInsideWindow(hasContainer, targetHeight, targetOffsetTop); if (isInsideWindow && !hasContainer) { return true; } return isInsideWindow && hasContainer && this.isElementInsiedScrollContainer(scrollContainer, targetHeight, targetOffsetTop); } isElementInsideWindow(hasContainer, elementHeight, elementOffsetTop) { const scrollTop = this.windowService.scrollTop; const viewportHeight = this.windowService.viewportHeight; // target bottom edge is below window top edge && target top edge is above window bottom edge // if target has a container, don't check for thresholds on the window if (hasContainer) { return elementOffsetTop + elementHeight > scrollTop && elementOffsetTop < scrollTop + viewportHeight; } return elementOffsetTop + elementHeight > scrollTop + this.thresholdTop && elementOffsetTop < scrollTop + viewportHeight - this.thresholdBottom; } isElementInsiedScrollContainer(container, elementHeight, elementOffsetTop) { const scrollContainerScrollTop = this.windowService.getElementScrollTop(container); const scrollContainerHeight = this.windowService.getElementHeight(container); const elementOffsetTopFromParent = elementOffsetTop - this.windowService.getElementOffsetTop(container); // element bottom edge is below container top edge && element top edge is above container bottom edge return elementOffsetTopFromParent + elementHeight > scrollContainerScrollTop + this.thresholdTop && elementOffsetTopFromParent < scrollContainerScrollTop + scrollContainerHeight - this.thresholdBottom; } get activeSpyTarget() { return this.activeSpyTarget$.asObservable(); } stopSpying() { this.stopSpying$.next(); this.spyTargets = []; this.isSpying = false; } } ScrollSpyService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.1.0", ngImport: i0, type: ScrollSpyService, deps: [{ token: WindowService }], target: i0.ɵɵFactoryTarget.Injectable }); ScrollSpyService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "14.1.0", ngImport: i0, type: ScrollSpyService, providedIn: 'root' }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.1.0", ngImport: i0, type: ScrollSpyService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: function () { return [{ type: WindowService }]; } }); class SpyOnDirective { constructor(el, renderer, spyService) { this.el = el; this.renderer = renderer; this.spyService = spyService; this.isActive = false; } ngOnInit() { this.spyService.activeSpyTarget.subscribe((targetName) => { if (!this.isActive && targetName === this.spyOn) { this.setActive(); } else if (this.isActive && targetName !== this.spyOn) { this.setInActive(); } }); } get htmlElement() { return this.el.nativeElement; } setActive() { this.isActive = true; if (this.activeClass) { this.renderer.addClass(this.htmlElement, this.activeClass); } } setInActive() { this.isActive = false; if (this.activeClass) { this.renderer.removeClass(this.htmlElement, this.activeClass); } } } SpyOnDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.1.0", ngImport: i0, type: SpyOnDirective, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }, { token: ScrollSpyService }], target: i0.ɵɵFactoryTarget.Directive }); SpyOnDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "14.1.0", type: SpyOnDirective, selector: "[spyOn]", inputs: { activeClass: "activeClass", spyOn: "spyOn" }, ngImport: i0 }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.1.0", ngImport: i0, type: SpyOnDirective, decorators: [{ type: Directive, args: [{ selector: '[spyOn]' }] }], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: i0.Renderer2 }, { type: ScrollSpyService }]; }, propDecorators: { activeClass: [{ type: Input }], spyOn: [{ type: Input }] } }); class SpyTargetDirective { constructor(el, spyService, renderer) { this.el = el; this.spyService = spyService; this.renderer = renderer; } ngOnInit() { this.renderer.setAttribute(this.htmlElement, 'id', this.spyTarget); this.spyService.addTarget({ name: this.spyTarget, element: this.el }); } get htmlElement() { return this.el.nativeElement; } ngOnDestroy() { this.spyService.removeTarget(this.spyTarget); } } SpyTargetDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.1.0", ngImport: i0, type: SpyTargetDirective, deps: [{ token: i0.ElementRef }, { token: ScrollSpyService }, { token: i0.Renderer2 }], target: i0.ɵɵFactoryTarget.Directive }); SpyTargetDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "14.1.0", type: SpyTargetDirective, selector: "[spyTarget]", inputs: { spyTarget: "spyTarget" }, ngImport: i0 }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.1.0", ngImport: i0, type: SpyTargetDirective, decorators: [{ type: Directive, args: [{ selector: '[spyTarget]' }] }], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: ScrollSpyService }, { type: i0.Renderer2 }]; }, propDecorators: { spyTarget: [{ type: Input }] } }); class ScrollSpyModule { } ScrollSpyModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.1.0", ngImport: i0, type: ScrollSpyModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); ScrollSpyModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "14.1.0", ngImport: i0, type: ScrollSpyModule, declarations: [SpyTargetDirective, SpyOnDirective], exports: [SpyTargetDirective, SpyOnDirective] }); ScrollSpyModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "14.1.0", ngImport: i0, type: ScrollSpyModule }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.1.0", ngImport: i0, type: ScrollSpyModule, decorators: [{ type: NgModule, args: [{ declarations: [SpyTargetDirective, SpyOnDirective], imports: [], exports: [SpyTargetDirective, SpyOnDirective] }] }] }); /* * Public API Surface of scroll-spy */ /** * Generated bundle index. Do not edit. */ export { RESIZE_TIME_THRESHOLD, SCROLL_TIME_THRESHOLD, ScrollSpyModule, ScrollSpyService, SpyOnDirective, SpyTargetDirective }; //# sourceMappingURL=ng-spy.mjs.map