UNPKG

@thejlifex/ngx-scroll-spy

Version:

An Angular library that can spy the position of elements (inside a scrollable container) and emit the currently active (visible) element in the viewport.

358 lines (349 loc) 15.9 kB
import { isPlatformServer } from '@angular/common'; import * as i0 from '@angular/core'; import { PLATFORM_ID, Injectable, Inject, Directive, Input, HostBinding } from '@angular/core'; import { Subject, fromEvent, debounceTime, takeUntil, filter } from 'rxjs'; /** * Encapsulates the logic for scroll spy on a scrollable element (`scrollContainer`). * * Scroll spy uses the [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API). * * Supports window resizing. * * Inspired by: * - https://grafikart.fr/tutoriels/scrollspy-js-page-491 * - https://codepen.io/diaphragm/pen/bGNBVxw * - https://github.com/chefomar/angular-scroll-spy */ class ScrollSpy { constructor({ scrollContainer } = {}) { this.spyTargetIsIntersectingSubject$ = new Subject(); /** * Is emitted when a spy-target has transitioned into a state of intersection (`isIntersecting: true`) or out of a state of intersection (`isIntersecting: false`). */ this.spyTargetIsIntersecting$ = this.spyTargetIsIntersectingSubject$.asObservable(); this.spyTargets = []; this.intersectionObserver = null; this.resizeDebounceTime = 300; /** * Defines the (vertical) position of the IntersectionObserver line. * Value between `0` (at the top) and `100` (at the bottom). * * @todo (For a future version of ngx-scroll-spy) This property should be configured by the user. */ this.offset = 20; this.initIntersectionObserver(scrollContainer); this.resizeSubscription = fromEvent(window, 'resize').pipe(debounceTime(this.resizeDebounceTime)).subscribe(() => this.initIntersectionObserver(scrollContainer)); } /** * Adds a spy-target and starts to observe that target. */ addTarget(spyTarget) { var _a; this.spyTargets.push(spyTarget); (_a = this.intersectionObserver) === null || _a === void 0 ? void 0 : _a.observe(spyTarget.element); } /** * Removes a spy-target and stops observing that target. */ removeTarget(spyTarget) { var _a; this.spyTargets = this.spyTargets.filter(spyTarget => spyTarget.id !== spyTarget.id); (_a = this.intersectionObserver) === null || _a === void 0 ? void 0 : _a.unobserve(spyTarget.element); } /** * Stops observing all spy-targets and completes spyTargetIsIntersecting$ subject. */ stopSpying() { var _a, _b; (_a = this.resizeSubscription) === null || _a === void 0 ? void 0 : _a.unsubscribe(); (_b = this.intersectionObserver) === null || _b === void 0 ? void 0 : _b.disconnect(); this.intersectionObserver = null; this.spyTargets = []; this.spyTargetIsIntersectingSubject$.complete(); } /** * Initialize IntersectionObserver. */ initIntersectionObserver(scrollContainer) { var _a; (_a = this.intersectionObserver) === null || _a === void 0 ? void 0 : _a.disconnect(); this.intersectionObserver = new IntersectionObserver((entries) => this.intersectionObserverCallback(entries), { root: scrollContainer, rootMargin: `-${this.offset}% 0px -${100 - this.offset}% 0px` // IntersectionObserver as one line. }); for (const spyTarget of this.spyTargets) { this.intersectionObserver.observe(spyTarget.element); } } /** * A function which is called when one or more spy-targets have transitioned into a state of intersection (`isIntersecting: true`) or out of a state of intersection (`isIntersecting: false`). */ intersectionObserverCallback(entries) { for (const entry of entries) { const spyTarget = this.spyTargets.find((item) => item.element === entry.target); this.spyTargetIsIntersectingSubject$.next({ spyTarget, isIntersecting: entry.isIntersecting }); } } } /** * Scroll spy service. * * Creates a scroll-spy on the Document/Body per default and destroys it when the service is destroyed. * * Allows you to create (and destroy) scroll-spy on a scroll-container (any scrollable element). */ class ScrollSpyService { constructor(platformId) { this.platformId = platformId; if (isPlatformServer(this.platformId)) { return; } this.defaultScrollSpy = new ScrollSpy(); this.scrollSpies = new Map(); } ngOnDestroy() { if (isPlatformServer(this.platformId)) { return; } this.defaultScrollSpy.stopSpying(); this.scrollSpies.forEach((item) => item.stopSpying()); this.scrollSpies.clear(); } /** * Create a scroll-spy for the provided `scrollContainer`. */ createScrollSpy(scrollContainerId, scrollContainer) { this.scrollSpies.set(scrollContainerId, new ScrollSpy({ scrollContainer })); } /** * Destroy the scroll-spy with ID equals the provided `scrollContainerId`. */ destroyScrollSpy(scrollContainerId) { var _a; (_a = this.scrollSpies.get(scrollContainerId)) === null || _a === void 0 ? void 0 : _a.stopSpying(); this.scrollSpies.delete(scrollContainerId); } /** * Get the `spyTargetIsIntersecting$` observable for the provided `scrollContainerId`, * * Or the `spyTargetIsIntersecting$` of the `defaultScrollSpy` when `scrollContainerId` is `undefined`. */ getSpyTargetIsIntersecting$(scrollContainerId) { return this.getScrollSpy(scrollContainerId).spyTargetIsIntersecting$; } /** * Adds a spy-target and starts to observe that target. */ addTarget(spyTarget) { this.getScrollSpy(spyTarget.containerId).addTarget(spyTarget); } /** * Removes a spy-target and stops observing that target. */ removeTarget(spyTarget) { this.getScrollSpy(spyTarget.containerId).removeTarget(spyTarget); } /** * Get the scroll-spy for the proivded `scrollContainerId`. * * When `scrollContainerId` is `undefined` the `defaultScrollSpy` is returned. */ getScrollSpy(scrollContainerId) { if (!scrollContainerId) { return this.defaultScrollSpy; } const scrollSpy = this.scrollSpies.get(scrollContainerId); if (!scrollSpy) { throw new Error(`Unknown spyTargetContainerId: "${scrollContainerId}". Please make sure that the spyTargetContainerId inputs have the same value on the three directives: spy, spyTarget and spyTargetContainer.`); } return scrollSpy; } } ScrollSpyService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.2.4", ngImport: i0, type: ScrollSpyService, deps: [{ token: PLATFORM_ID }], target: i0.ɵɵFactoryTarget.Injectable }); ScrollSpyService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "14.2.4", ngImport: i0, type: ScrollSpyService, providedIn: 'root' }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.2.4", ngImport: i0, type: ScrollSpyService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: function () { return [{ type: undefined, decorators: [{ type: Inject, args: [PLATFORM_ID] }] }]; } }); /** * Spies on the spyTarget that has the provided `spyTargetId`. */ class SpyDirective { constructor(scrollSpyService, platformId, changeDetectorRef) { this.scrollSpyService = scrollSpyService; this.platformId = platformId; this.changeDetectorRef = changeDetectorRef; /** * Class name to add to this element when the spyTarget has transitioned into a state of intersection (is visisble). * * @default * 'active' */ this.spyActiveClass = 'active'; this.directiveDestroyed$ = new Subject(); } ngOnInit() { if (isPlatformServer(this.platformId)) { return; } if (!this.spyTargetId) { throw new Error('SpyDirective: spyTargetId needs to be set.'); } this.scrollSpyService.getSpyTargetIsIntersecting$(this.spyTargetContainerId) .pipe(takeUntil(this.directiveDestroyed$), filter((event) => event.spyTarget.id === this.spyTargetId && event.spyTarget.containerId === this.spyTargetContainerId)) .subscribe((event) => { this.activeClassInDOM = event.isIntersecting ? this.spyActiveClass : undefined; /** * Manually trigger change detection to update activeClass in the DOM * (Needed for the case changeDetection OnPush is used). */ this.changeDetectorRef.detectChanges(); }); } ngOnDestroy() { this.directiveDestroyed$.next(); this.directiveDestroyed$.complete(); } } SpyDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.2.4", ngImport: i0, type: SpyDirective, deps: [{ token: ScrollSpyService }, { token: PLATFORM_ID }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Directive }); SpyDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "14.2.4", type: SpyDirective, isStandalone: true, selector: "[spy]", inputs: { spyTargetId: "spyTargetId", spyTargetContainerId: "spyTargetContainerId", spyActiveClass: "spyActiveClass" }, host: { properties: { "class": "this.activeClassInDOM" } }, ngImport: i0 }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.2.4", ngImport: i0, type: SpyDirective, decorators: [{ type: Directive, args: [{ selector: '[spy]', standalone: true }] }], ctorParameters: function () { return [{ type: ScrollSpyService }, { type: undefined, decorators: [{ type: Inject, args: [PLATFORM_ID] }] }, { type: i0.ChangeDetectorRef }]; }, propDecorators: { spyTargetId: [{ type: Input }], spyTargetContainerId: [{ type: Input }], spyActiveClass: [{ type: Input }], activeClassInDOM: [{ type: HostBinding, args: ['class'] }] } }); /** * Adds this element as spy-target and starts to observe this target. * * Automatically removes this element as spy-target and stops observing this target when the element is destroyed. */ class SpyTargetDirective { constructor(elementRef, scrollSpyService, platformId) { this.elementRef = elementRef; this.scrollSpyService = scrollSpyService; this.platformId = platformId; } ngOnInit() { if (isPlatformServer(this.platformId)) { return; } if (!this.spyTargetId) { throw new Error('SpyTargetDirective: spyTargetId needs to be set.'); } this.target = { id: this.spyTargetId, element: this.elementRef.nativeElement, containerId: this.spyTargetContainerId }; this.scrollSpyService.addTarget(this.target); } ngOnDestroy() { if (isPlatformServer(this.platformId)) { return; } this.scrollSpyService.removeTarget(this.target); } } SpyTargetDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.2.4", ngImport: i0, type: SpyTargetDirective, deps: [{ token: i0.ElementRef }, { token: ScrollSpyService }, { token: PLATFORM_ID }], target: i0.ɵɵFactoryTarget.Directive }); SpyTargetDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "14.2.4", type: SpyTargetDirective, isStandalone: true, selector: "[spyTarget]", inputs: { spyTargetId: "spyTargetId", spyTargetContainerId: "spyTargetContainerId" }, ngImport: i0 }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.2.4", ngImport: i0, type: SpyTargetDirective, decorators: [{ type: Directive, args: [{ selector: '[spyTarget]', standalone: true }] }], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: ScrollSpyService }, { type: undefined, decorators: [{ type: Inject, args: [PLATFORM_ID] }] }]; }, propDecorators: { spyTargetId: [{ type: Input }], spyTargetContainerId: [{ type: Input }] } }); /** * Creates a scroll-spy on this spy targets container (element). * * Automatically destroys the scroll-spy when the element is destroyed. * * This is useful: * * - when you have multiple (independent) scrollable elements on the same page, * - or the scrollable element on your page is not the browser default scrollable element, for example `<mat-sidenav-content>` if you are using [Angular Material Sidenav](https://material.angular.io/components/sidenav) * * where you want to have a scroll-spy. */ class SpyTargetContainerDirective { constructor(scrollSpyService, platformId, elementRef) { this.scrollSpyService = scrollSpyService; this.platformId = platformId; this.elementRef = elementRef; } ngOnInit() { if (isPlatformServer(this.platformId)) { return; } if (!this.spyTargetContainerId) { throw new Error('SpyTargetContainerDirective: spyTargetContainerId needs to be set.'); } this.scrollSpyService.createScrollSpy(this.spyTargetContainerId, this.elementRef.nativeElement); } ngOnDestroy() { if (isPlatformServer(this.platformId)) { return; } this.scrollSpyService.destroyScrollSpy(this.spyTargetContainerId); } } SpyTargetContainerDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.2.4", ngImport: i0, type: SpyTargetContainerDirective, deps: [{ token: ScrollSpyService }, { token: PLATFORM_ID }, { token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive }); SpyTargetContainerDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "14.2.4", type: SpyTargetContainerDirective, isStandalone: true, selector: "[spyTargetContainer]", inputs: { spyTargetContainerId: "spyTargetContainerId" }, ngImport: i0 }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.2.4", ngImport: i0, type: SpyTargetContainerDirective, decorators: [{ type: Directive, args: [{ selector: '[spyTargetContainer]', standalone: true }] }], ctorParameters: function () { return [{ type: ScrollSpyService }, { type: undefined, decorators: [{ type: Inject, args: [PLATFORM_ID] }] }, { type: i0.ElementRef }]; }, propDecorators: { spyTargetContainerId: [{ type: Input }] } }); /* * Public API Surface of ngx-scroll-spy */ /** * Generated bundle index. Do not edit. */ export { SpyDirective, SpyTargetContainerDirective, SpyTargetDirective }; //# sourceMappingURL=thejlifex-ngx-scroll-spy.mjs.map