UNPKG

ngx-infinite-scroller

Version:

Infinite bidirectional scroll directive for Angular 11

408 lines (395 loc) 14.9 kB
import { Injectable, EventEmitter, Directive, Inject, PLATFORM_ID, ElementRef, Renderer2, Input, Output, NgModule } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; import { Subject, fromEvent, zip } from 'rxjs'; import { filter, tap, takeWhile, map, pairwise, debounceTime, skipWhile } from 'rxjs/operators'; import isNumber from 'is-number'; class DirectiveStateService { get scrollTop() { return this._el.nativeElement.scrollTop; } get scrollHeight() { return this._el.nativeElement.scrollHeight; } get clientHeight() { return this._el.nativeElement.clientHeight; } get initMode() { return this._initMode; } set initMode(initMode) { this._initMode = initMode; } get scrollStreamActive() { return this._scrollStreamActive; } set scrollStreamActive(active) { this._scrollStreamActive = active; } get previousScrollPositionpUpdated() { return this._previousScrollPositionpUpdated; } set previousScrollPositionpUpdated(previousScrollPositionpUpdated) { this._previousScrollPositionpUpdated = previousScrollPositionpUpdated; } get previousScrollTop() { return this._previousScrollTop; } get previousScrollHeight() { return this._previousScrollHeight; } setup(params) { this._el = params.el; this._initMode = params.initMode; this._scrollStreamActive = params.scrollStreamActive; this._previousScrollPositionpUpdated = params.previousScrollPositionpUpdated; this.updatePreviousScrollTop(); this.updatePreviousScrollHeight(); } updatePreviousScrollTop() { this._previousScrollTop = this._el.nativeElement.scrollTop; } updatePreviousScrollHeight() { this._previousScrollHeight = this._el.nativeElement.scrollHeight; } } DirectiveStateService.decorators = [ { type: Injectable } ]; var InitialScrollPosition; (function (InitialScrollPosition) { InitialScrollPosition["DEFAULT"] = "DEFAULT"; InitialScrollPosition["TOP"] = "TOP"; InitialScrollPosition["MIDDLE"] = "MIDDLE"; InitialScrollPosition["BOTTOM"] = "BOTTOM"; })(InitialScrollPosition || (InitialScrollPosition = {})); class DirectiveContext { constructor() { } } class StrategyBase { constructor(directive, state) { this.directive = directive; this.state = state; } wasScrolledDown(prevPos, currentPos) { return prevPos.scrollTop < currentPos.scrollTop; } wasScrolledUp(prevPos, currentPos) { return !this.wasScrolledDown(prevPos, currentPos); } isScrollDownEnough(pos, scrollPositionTrigger) { return ((pos.scrollTop + pos.clientHeight) / pos.scrollHeight) > (scrollPositionTrigger / 100); } isScrollUpEnough(pos, scrollPositionTrigger) { return (pos.scrollTop / pos.scrollHeight) < (scrollPositionTrigger / 100); } getInitialScrollPositionValue(defaultScrollPosition) { const { initialScrollPosition } = this.directive; if (isNumber(initialScrollPosition)) { return Number(initialScrollPosition); } const initialScrollPositions = this.getInitialScrollPositions(); if (initialScrollPosition === InitialScrollPosition.DEFAULT) { return initialScrollPositions[defaultScrollPosition]; } return initialScrollPositions[initialScrollPosition]; } getInitialScrollPositions() { const { scrollHeight, clientHeight } = this.state; return { [InitialScrollPosition.TOP]: 0, [InitialScrollPosition.MIDDLE]: scrollHeight / 2 - clientHeight / 2, [InitialScrollPosition.BOTTOM]: scrollHeight, }; } } class ScrollingToTop extends StrategyBase { constructor(directive, state) { super(directive, state); } scrollDirectionChanged(scrollPairChanged) { return scrollPairChanged.pipe(filter((scrollPositions) => { return super.wasScrolledUp(scrollPositions[0], scrollPositions[1]); })); } scrollRequestZoneChanged(scrollDirectionChanged) { return scrollDirectionChanged.pipe(filter((scrollPositions) => { return super.isScrollUpEnough(scrollPositions[1], this.directive.scrollUpPercentilePositionTrigger); })); } askForUpdate() { this.directive.onScrollUp.next(); } setInitialScrollPosition() { const initialScrollPositionValue = super.getInitialScrollPositionValue(InitialScrollPosition.BOTTOM); this.directive.scrollTo(initialScrollPositionValue); } setPreviousScrollPosition() { const prevScrollPosition = this.state.previousScrollTop + (this.state.scrollHeight - this.state.previousScrollHeight); this.directive.scrollTo(prevScrollPosition); } } class ScrollingToBottom extends StrategyBase { constructor(directive, state) { super(directive, state); } scrollDirectionChanged(scrollPairChanged) { return scrollPairChanged.pipe(filter((scrollPositions) => { return super.wasScrolledDown(scrollPositions[0], scrollPositions[1]); })); } scrollRequestZoneChanged(scrollDirectionChanged) { return scrollDirectionChanged.pipe(filter((scrollPositions) => { return super.isScrollDownEnough(scrollPositions[1], this.directive.scrollDownPercentilePositionTrigger); })); } askForUpdate() { this.directive.onScrollDown.next(); } setInitialScrollPosition() { const initialScrollPositionValue = super.getInitialScrollPositionValue(InitialScrollPosition.TOP); this.directive.scrollTo(initialScrollPositionValue); } setPreviousScrollPosition() { const prevScrollPosition = this.state.previousScrollTop; this.directive.scrollTo(prevScrollPosition); } } class ScrollingToBoth extends StrategyBase { constructor(directive, state) { super(directive, state); } scrollDirectionChanged(scrollPairChanged) { return scrollPairChanged; } scrollRequestZoneChanged(scrollDirectionChanged) { return scrollDirectionChanged.pipe(filter((scrollPositions) => { return (super.isScrollUpEnough(scrollPositions[1], this.directive.scrollUpPercentilePositionTrigger) || super.isScrollDownEnough(scrollPositions[1], this.directive.scrollDownPercentilePositionTrigger)); }), tap((scrollPositions) => { this.scrolledUp = super.wasScrolledUp(scrollPositions[0], scrollPositions[1]); })); } askForUpdate() { if (this.scrolledUp) { this.directive.onScrollUp.next(); } else { this.directive.onScrollDown.next(); } } setInitialScrollPosition() { const initialScrollPositionValue = super.getInitialScrollPositionValue(InitialScrollPosition.MIDDLE); this.directive.scrollTo(initialScrollPositionValue); } setPreviousScrollPosition() { let prevScrollPosition; if (this.scrolledUp) { prevScrollPosition = this.state.previousScrollTop + (this.state.scrollHeight - this.state.previousScrollHeight); } else { prevScrollPosition = this.state.previousScrollTop; } this.directive.scrollTo(prevScrollPosition); } } class ScrollHeightListener { constructor(directive, state) { this.directive = directive; this.state = state; this.DEFAULT_REQUEST_TIMEOUT = 30000; } start() { this.listener = window.requestAnimationFrame(this.listen.bind(this)); if (!this.httpRequestTimeout) { this.httpRequestTimeout = setTimeout(() => { this.stopIfRequestTimeout(); }, this.DEFAULT_REQUEST_TIMEOUT); } } stop() { window.cancelAnimationFrame(this.listener); clearTimeout(this.httpRequestTimeout); this.httpRequestTimeout = null; } listen() { if (this.state.previousScrollHeight !== this.state.scrollHeight) { this.stop(); this.directive.onScrollbarHeightChanged(); } else { this.start(); } } stopIfRequestTimeout() { if (!this.state.previousScrollPositionpUpdated) { this.stop(); } } } class NgxInfiniteScrollerDirective extends DirectiveContext { constructor(platformId, el, renderer, state) { super(); this.platformId = platformId; this.el = el; this.renderer = renderer; this.state = state; this.strategy = 'scrollingToBottom'; this.initialScrollPosition = InitialScrollPosition.DEFAULT; this.scrollbarAnimationInterval = 100; this.scrollDebounceTimeAfterScrollHeightChanged = 50; this.scrollDebounceTimeAfterDOMMutationOnInit = 1000; this.scrollUpPercentilePositionTrigger = 2; this.scrollDownPercentilePositionTrigger = 98; this.onScrollUp = new EventEmitter(); this.onScrollDown = new EventEmitter(); this.scrollHeightChanged = new Subject(); this.domMutationEmitter = new Subject(); this.isBrowser = isPlatformBrowser(platformId); this.state.setup({ el: el, initMode: true, scrollStreamActive: true, previousScrollPositionpUpdated: false }); } get scrollPairChanged() { if (this.scrollChanged) { return this.scrollChanged.pipe(takeWhile(() => this.state.scrollStreamActive), map((e) => { return { scrollHeight: e.target.scrollHeight, scrollTop: e.target.scrollTop, clientHeight: e.target.clientHeight, }; }), pairwise(), debounceTime(this.scrollbarAnimationInterval)); } } get scrollDirectionChanged() { return this.scrollingStrategy.scrollDirectionChanged(this.scrollPairChanged); } get scrollRequestZoneChanged() { return this.scrollingStrategy.scrollRequestZoneChanged(this.scrollDirectionChanged).pipe(tap(() => { this.state.updatePreviousScrollTop(); this.state.updatePreviousScrollHeight(); this.state.previousScrollPositionpUpdated = false; this.scrollHeightListener.start(); })); } ngOnInit() { this.useStrategy(); this.useScrollHeightListener(); this.registerScrollEventHandler(); this.registerMutationObserver(); this.registerInitialScrollPostionHandler(); this.registerPreviousScrollPositionHandler(); } ngAfterViewInit() { this.registerScrollSpy(); } ngOnDestroy() { this.unregisterMutationObserver(); } scrollTo(position) { this.state.scrollStreamActive = false; this.renderer.setProperty(this.el.nativeElement, 'scrollTop', position); this.state.scrollStreamActive = true; } onScrollbarHeightChanged() { this.scrollHeightChanged.next(); } registerScrollEventHandler() { this.scrollChanged = fromEvent(this.el.nativeElement, 'scroll'); } registerMutationObserver() { if (this.isBrowser) { this.domMutationObserver = new MutationObserver((mutations) => { this.domMutationEmitter.next(mutations); }); const config = { attributes: true, childList: true, characterData: true }; this.domMutationObserver.observe(this.el.nativeElement, config); } } registerInitialScrollPostionHandler() { this.domMutationEmitter.pipe(takeWhile(() => this.state.initMode), debounceTime(this.scrollDebounceTimeAfterDOMMutationOnInit)).subscribe(() => { this.scrollingStrategy.setInitialScrollPosition(); this.state.initMode = false; }); } registerPreviousScrollPositionHandler() { zip(this.scrollRequestZoneChanged, this.scrollHeightChanged).pipe(skipWhile(() => this.state.initMode), debounceTime(this.scrollDebounceTimeAfterScrollHeightChanged)).subscribe(() => { this.scrollingStrategy.setPreviousScrollPosition(); this.state.previousScrollPositionpUpdated = true; }); } registerScrollSpy() { this.scrollRequestZoneChanged.subscribe(() => { this.scrollingStrategy.askForUpdate(); }); } unregisterMutationObserver() { if (this.domMutationObserver) { this.domMutationObserver.disconnect(); } } useStrategy() { switch (this.strategy) { case 'scrollingToBoth': this.scrollingStrategy = new ScrollingToBoth(this, this.state); break; case 'scrollingToTop': this.scrollingStrategy = new ScrollingToTop(this, this.state); break; case 'scrollingToBottom': default: this.scrollingStrategy = new ScrollingToBottom(this, this.state); break; } } useScrollHeightListener() { this.scrollHeightListener = new ScrollHeightListener(this, this.state); } } NgxInfiniteScrollerDirective.decorators = [ { type: Directive, args: [{ selector: '[ngxInfiniteScroller]' },] } ]; NgxInfiniteScrollerDirective.ctorParameters = () => [ { type: undefined, decorators: [{ type: Inject, args: [PLATFORM_ID,] }] }, { type: ElementRef }, { type: Renderer2 }, { type: DirectiveStateService } ]; NgxInfiniteScrollerDirective.propDecorators = { strategy: [{ type: Input }], initialScrollPosition: [{ type: Input }], scrollbarAnimationInterval: [{ type: Input }], scrollDebounceTimeAfterScrollHeightChanged: [{ type: Input }], scrollDebounceTimeAfterDOMMutationOnInit: [{ type: Input }], scrollUpPercentilePositionTrigger: [{ type: Input }], scrollDownPercentilePositionTrigger: [{ type: Input }], onScrollUp: [{ type: Output }], onScrollDown: [{ type: Output }] }; class NgxInfiniteScrollerModule { } NgxInfiniteScrollerModule.decorators = [ { type: NgModule, args: [{ declarations: [ NgxInfiniteScrollerDirective ], imports: [], exports: [ NgxInfiniteScrollerDirective ], providers: [ DirectiveStateService ], bootstrap: [] },] } ]; /** * Generated bundle index. Do not edit. */ export { NgxInfiniteScrollerModule, NgxInfiniteScrollerDirective as ɵa, DirectiveContext as ɵb, DirectiveStateService as ɵc }; //# sourceMappingURL=ngx-infinite-scroller.js.map