UNPKG

@avtest/ng-spy

Version:

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

306 lines (295 loc) 13.9 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; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: WindowService, deps: [{ token: PLATFORM_ID }, { token: RESIZE_TIME_THRESHOLD }, { token: SCROLL_TIME_THRESHOLD }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: WindowService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: WindowService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [{ 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({ ...target }); this.checkActiveElement(this.scrollContainer); } removeTarget(target) { this.spyTargets = this.spyTargets.filter(spyTarget => target !== spyTarget.name); this.checkActiveElement(this.scrollContainer); } checkActiveElement(scrollContainer = this.scrollContainer) { let activeTarget = null; let scrollContainerOffset = this.getTotalOffset(scrollContainer); for (const target of this.spyTargets) { const activeElement = activeTarget != null ? activeTarget.element : null; if (this.isElementActive(target.element, scrollContainer, scrollContainerOffset, activeElement)) { activeTarget = target; } } this.activeSpyTarget$.next(activeTarget ? activeTarget.name : null); } isElementActive(element, scrollContainer, scrollContainerOffset, currentActiveElement) { const targetOffsetTop = this.windowService.getElementOffsetTop(element); const targetHeight = this.windowService.getElementHeight(element); if (currentActiveElement != null && this.windowService.getElementOffsetTop(currentActiveElement) < targetOffsetTop) { return false; } return this.isElementInsideWindow(element, scrollContainer, scrollContainerOffset, targetHeight, targetOffsetTop); } getTotalOffset(element) { if (!element) { return 0; } let totalOffset = 0; let current = element.nativeElement; while (current.offsetParent != null) { totalOffset += current.offsetTop; current = current.offsetParent; } return totalOffset; } isElementInsideWindow(element, scrollContainer, scrollContainerOffset, 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 (scrollContainer != null) { // element has to be inside the portion of the container that is visible const containerHeight = this.windowService.getElementHeight(scrollContainer); const containerScrollTop = this.windowService.getElementScrollTop(scrollContainer); // < 0: container is "above" the screen // > 0: container is on or below the screen const distanceToContainer = scrollContainerOffset - scrollTop; const visibleContainerHeight = Math.min(viewportHeight - distanceToContainer, containerHeight); // < 0: it is too far down to see if (visibleContainerHeight < 0) { return false; } // elementOffsetTop is a "global" value so we have to calculate the offset _inside_ the container const relativeElementOffset = this.getTotalOffset(element); // now we need figure out which scrolled _part_ of the container is visible return (relativeElementOffset + elementHeight) > (scrollContainerOffset + containerScrollTop) && relativeElementOffset < (scrollContainerOffset + containerScrollTop + visibleContainerHeight); } return elementOffsetTop + elementHeight > scrollTop + this.thresholdTop && elementOffsetTop < scrollTop + viewportHeight - this.thresholdBottom; } get activeSpyTarget() { return this.activeSpyTarget$.asObservable(); } stopSpying() { this.stopSpying$.next(); this.spyTargets = []; this.isSpying = false; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: ScrollSpyService, deps: [{ token: WindowService }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: ScrollSpyService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: ScrollSpyService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [{ 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); } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: SpyOnDirective, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }, { token: ScrollSpyService }], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "17.0.4", type: SpyOnDirective, isStandalone: true, selector: "[spyOn]", inputs: { activeClass: "activeClass", spyOn: "spyOn" }, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: SpyOnDirective, decorators: [{ type: Directive, args: [{ selector: '[spyOn]', standalone: true }] }], ctorParameters: () => [{ 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); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: SpyTargetDirective, deps: [{ token: i0.ElementRef }, { token: ScrollSpyService }, { token: i0.Renderer2 }], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "17.0.4", type: SpyTargetDirective, isStandalone: true, selector: "[spyTarget]", inputs: { spyTarget: "spyTarget" }, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: SpyTargetDirective, decorators: [{ type: Directive, args: [{ selector: '[spyTarget]', standalone: true }] }], ctorParameters: () => [{ type: i0.ElementRef }, { type: ScrollSpyService }, { type: i0.Renderer2 }], propDecorators: { spyTarget: [{ type: Input }] } }); class ScrollSpyModule { static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: ScrollSpyModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); } static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "17.0.4", ngImport: i0, type: ScrollSpyModule, imports: [SpyTargetDirective, SpyOnDirective], exports: [SpyTargetDirective, SpyOnDirective] }); } static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: ScrollSpyModule }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: ScrollSpyModule, decorators: [{ type: NgModule, args: [{ imports: [ SpyTargetDirective, SpyOnDirective ], 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=avtest-ng-spy.mjs.map