@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
JavaScript
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