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
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;
}
}
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