ngx-infinite-scroll
Version:
An infinite scroll directive for Angular
787 lines (764 loc) • 21.8 kB
JavaScript
import { Directive, ElementRef, EventEmitter, Input, NgModule, NgZone, Output } from '@angular/core';
import { fromEvent, of } from 'rxjs';
import { filter, map, mergeMap, sampleTime, tap } from 'rxjs/operators';
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/**
* @param {?} selector
* @param {?} scrollWindow
* @param {?} defaultElement
* @param {?} fromRoot
* @return {?}
*/
function resolveContainerElement(selector, scrollWindow, defaultElement, fromRoot) {
/** @type {?} */
const hasWindow = window && !!window.document && window.document.documentElement;
/** @type {?} */
let container = hasWindow && scrollWindow ? window : defaultElement;
if (selector) {
/** @type {?} */
const containerIsString = selector && hasWindow && typeof selector === 'string';
container = containerIsString
? findElement(selector, defaultElement.nativeElement, fromRoot)
: selector;
if (!container) {
throw new Error('ngx-infinite-scroll {resolveContainerElement()}: selector for');
}
}
return container;
}
/**
* @param {?} selector
* @param {?} customRoot
* @param {?} fromRoot
* @return {?}
*/
function findElement(selector, customRoot, fromRoot) {
/** @type {?} */
const rootEl = fromRoot ? window.document : customRoot;
return rootEl.querySelector(selector);
}
/**
* @param {?} prop
* @return {?}
*/
function inputPropChanged(prop) {
return prop && !prop.firstChange;
}
/**
* @return {?}
*/
function hasWindowDefined() {
return typeof window !== 'undefined';
}
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/** @type {?} */
const VerticalProps = {
clientHeight: "clientHeight",
offsetHeight: "offsetHeight",
scrollHeight: "scrollHeight",
pageYOffset: "pageYOffset",
offsetTop: "offsetTop",
scrollTop: "scrollTop",
top: "top"
};
/** @type {?} */
const HorizontalProps = {
clientHeight: "clientWidth",
offsetHeight: "offsetWidth",
scrollHeight: "scrollWidth",
pageYOffset: "pageXOffset",
offsetTop: "offsetLeft",
scrollTop: "scrollLeft",
top: "left"
};
class AxisResolver {
/**
* @param {?=} vertical
*/
constructor(vertical = true) {
this.vertical = vertical;
this.propsMap = vertical ? VerticalProps : HorizontalProps;
}
/**
* @return {?}
*/
clientHeightKey() {
return this.propsMap.clientHeight;
}
/**
* @return {?}
*/
offsetHeightKey() {
return this.propsMap.offsetHeight;
}
/**
* @return {?}
*/
scrollHeightKey() {
return this.propsMap.scrollHeight;
}
/**
* @return {?}
*/
pageYOffsetKey() {
return this.propsMap.pageYOffset;
}
/**
* @return {?}
*/
offsetTopKey() {
return this.propsMap.offsetTop;
}
/**
* @return {?}
*/
scrollTopKey() {
return this.propsMap.scrollTop;
}
/**
* @return {?}
*/
topKey() {
return this.propsMap.top;
}
}
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/**
* @record
*/
/**
* @record
*/
/**
* @record
*/
/**
* @record
*/
/**
* @param {?} alwaysCallback
* @param {?} shouldFireScrollEvent
* @param {?} isTriggeredCurrentTotal
* @return {?}
*/
function shouldTriggerEvents(alwaysCallback, shouldFireScrollEvent, isTriggeredCurrentTotal) {
if (alwaysCallback && shouldFireScrollEvent) {
return true;
}
if (!isTriggeredCurrentTotal && shouldFireScrollEvent) {
return true;
}
return false;
}
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/**
* @param {?} __0
* @return {?}
*/
function createResolver({ windowElement, axis }) {
return createResolverWithContainer({ axis, isWindow: isElementWindow(windowElement) }, windowElement);
}
/**
* @param {?} resolver
* @param {?} windowElement
* @return {?}
*/
function createResolverWithContainer(resolver, windowElement) {
/** @type {?} */
const container = resolver.isWindow || (windowElement && !windowElement.nativeElement)
? windowElement
: windowElement.nativeElement;
return Object.assign(Object.assign({}, resolver), { container });
}
/**
* @param {?} windowElement
* @return {?}
*/
function isElementWindow(windowElement) {
/** @type {?} */
const isWindow = ['Window', 'global'].some((/**
* @param {?} obj
* @return {?}
*/
(obj) => Object.prototype.toString.call(windowElement).includes(obj)));
return isWindow;
}
/**
* @param {?} isContainerWindow
* @param {?} windowElement
* @return {?}
*/
function getDocumentElement(isContainerWindow, windowElement) {
return isContainerWindow ? windowElement.document.documentElement : null;
}
/**
* @param {?} element
* @param {?} resolver
* @return {?}
*/
function calculatePoints(element, resolver) {
/** @type {?} */
const height = extractHeightForElement(resolver);
return resolver.isWindow
? calculatePointsForWindow(height, element, resolver)
: calculatePointsForElement(height, element, resolver);
}
/**
* @param {?} height
* @param {?} element
* @param {?} resolver
* @return {?}
*/
function calculatePointsForWindow(height, element, resolver) {
const { axis, container, isWindow } = resolver;
const { offsetHeightKey, clientHeightKey } = extractHeightPropKeys(axis);
// scrolled until now / current y point
/** @type {?} */
const scrolled = height +
getElementPageYOffset(getDocumentElement(isWindow, container), axis, isWindow);
// total height / most bottom y point
/** @type {?} */
const nativeElementHeight = getElementHeight(element.nativeElement, isWindow, offsetHeightKey, clientHeightKey);
/** @type {?} */
const totalToScroll = getElementOffsetTop(element.nativeElement, axis, isWindow) +
nativeElementHeight;
return { height, scrolled, totalToScroll, isWindow };
}
/**
* @param {?} height
* @param {?} element
* @param {?} resolver
* @return {?}
*/
function calculatePointsForElement(height, element, resolver) {
const { axis, container } = resolver;
// perhaps use container.offsetTop instead of 'scrollTop'
/** @type {?} */
const scrolled = container[axis.scrollTopKey()];
/** @type {?} */
const totalToScroll = container[axis.scrollHeightKey()];
return { height, scrolled, totalToScroll, isWindow: false };
}
/**
* @param {?} axis
* @return {?}
*/
function extractHeightPropKeys(axis) {
return {
offsetHeightKey: axis.offsetHeightKey(),
clientHeightKey: axis.clientHeightKey()
};
}
/**
* @param {?} __0
* @return {?}
*/
function extractHeightForElement({ container, isWindow, axis }) {
const { offsetHeightKey, clientHeightKey } = extractHeightPropKeys(axis);
return getElementHeight(container, isWindow, offsetHeightKey, clientHeightKey);
}
/**
* @param {?} elem
* @param {?} isWindow
* @param {?} offsetHeightKey
* @param {?} clientHeightKey
* @return {?}
*/
function getElementHeight(elem, isWindow, offsetHeightKey, clientHeightKey) {
if (isNaN(elem[offsetHeightKey])) {
/** @type {?} */
const docElem = getDocumentElement(isWindow, elem);
return docElem ? docElem[clientHeightKey] : 0;
}
else {
return elem[offsetHeightKey];
}
}
/**
* @param {?} elem
* @param {?} axis
* @param {?} isWindow
* @return {?}
*/
function getElementOffsetTop(elem, axis, isWindow) {
/** @type {?} */
const topKey = axis.topKey();
// elem = elem.nativeElement;
if (!elem.getBoundingClientRect) {
// || elem.css('none')) {
return;
}
return (elem.getBoundingClientRect()[topKey] +
getElementPageYOffset(elem, axis, isWindow));
}
/**
* @param {?} elem
* @param {?} axis
* @param {?} isWindow
* @return {?}
*/
function getElementPageYOffset(elem, axis, isWindow) {
/** @type {?} */
const pageYOffset = axis.pageYOffsetKey();
/** @type {?} */
const scrollTop = axis.scrollTopKey();
/** @type {?} */
const offsetTop = axis.offsetTopKey();
if (isNaN(window.pageYOffset)) {
return getDocumentElement(isWindow, elem)[scrollTop];
}
else if (elem.ownerDocument) {
return elem.ownerDocument.defaultView[pageYOffset];
}
else {
return elem[offsetTop];
}
}
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/**
* @param {?} container
* @param {?} distance
* @param {?} scrollingDown
* @return {?}
*/
function shouldFireScrollEvent(container, distance, scrollingDown) {
/** @type {?} */
let remaining;
/** @type {?} */
let containerBreakpoint;
if (container.totalToScroll <= 0) {
return false;
}
/** @type {?} */
const scrolledUntilNow = container.isWindow ? container.scrolled : container.height + container.scrolled;
if (scrollingDown) {
remaining =
(container.totalToScroll - scrolledUntilNow) / container.totalToScroll;
containerBreakpoint = distance.down / 10;
}
else {
/** @type {?} */
const totalHiddenContentHeight = container.scrolled + (container.totalToScroll - scrolledUntilNow);
remaining = container.scrolled / totalHiddenContentHeight;
containerBreakpoint = distance.up / 10;
}
/** @type {?} */
const shouldFireEvent = remaining <= containerBreakpoint;
return shouldFireEvent;
}
/**
* @param {?} lastScrollPosition
* @param {?} container
* @return {?}
*/
function isScrollingDownwards(lastScrollPosition, container) {
return lastScrollPosition < container.scrolled;
}
/**
* @param {?} lastScrollPosition
* @param {?} container
* @param {?} distance
* @return {?}
*/
function getScrollStats(lastScrollPosition, container, distance) {
/** @type {?} */
const scrollDown = isScrollingDownwards(lastScrollPosition, container);
return {
fire: shouldFireScrollEvent(container, distance, scrollDown),
scrollDown
};
}
/**
* @param {?} position
* @param {?} scrollState
* @return {?}
*/
/**
* @param {?} totalToScroll
* @param {?} scrollState
* @return {?}
*/
/**
* @param {?} scrollState
* @return {?}
*/
/**
* @param {?} scroll
* @param {?} scrollState
* @param {?} triggered
* @param {?} isScrollingDown
* @return {?}
*/
/**
* @param {?} totalToScroll
* @param {?} scrollState
* @param {?} isScrollingDown
* @return {?}
*/
/**
* @param {?} scrollState
* @param {?} scrolledUntilNow
* @param {?} totalToScroll
* @return {?}
*/
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
class ScrollState {
/**
* @param {?} __0
*/
constructor({ totalToScroll }) {
this.lastScrollPosition = 0;
this.lastTotalToScroll = 0;
this.totalToScroll = 0;
this.triggered = {
down: 0,
up: 0
};
this.totalToScroll = totalToScroll;
}
/**
* @param {?} position
* @return {?}
*/
updateScrollPosition(position) {
return (this.lastScrollPosition = position);
}
/**
* @param {?} totalToScroll
* @return {?}
*/
updateTotalToScroll(totalToScroll) {
if (this.lastTotalToScroll !== totalToScroll) {
this.lastTotalToScroll = this.totalToScroll;
this.totalToScroll = totalToScroll;
}
}
/**
* @param {?} scrolledUntilNow
* @param {?} totalToScroll
* @return {?}
*/
updateScroll(scrolledUntilNow, totalToScroll) {
this.updateScrollPosition(scrolledUntilNow);
this.updateTotalToScroll(totalToScroll);
}
/**
* @param {?} scroll
* @param {?} isScrollingDown
* @return {?}
*/
updateTriggeredFlag(scroll, isScrollingDown) {
if (isScrollingDown) {
this.triggered.down = scroll;
}
else {
this.triggered.up = scroll;
}
}
/**
* @param {?} totalToScroll
* @param {?} isScrollingDown
* @return {?}
*/
isTriggeredScroll(totalToScroll, isScrollingDown) {
return isScrollingDown
? this.triggered.down === totalToScroll
: this.triggered.up === totalToScroll;
}
}
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/**
* @param {?} config
* @return {?}
*/
function createScroller(config) {
const { scrollContainer, scrollWindow, element, fromRoot } = config;
/** @type {?} */
const resolver = createResolver({
axis: new AxisResolver(!config.horizontal),
windowElement: resolveContainerElement(scrollContainer, scrollWindow, element, fromRoot)
});
/** @type {?} */
const scrollState = new ScrollState({
totalToScroll: calculatePoints(element, resolver)
});
/** @type {?} */
const options = {
container: resolver.container,
throttle: config.throttle
};
/** @type {?} */
const distance = {
up: config.upDistance,
down: config.downDistance
};
return attachScrollEvent(options).pipe(mergeMap((/**
* @return {?}
*/
() => of(calculatePoints(element, resolver)))), map((/**
* @param {?} positionStats
* @return {?}
*/
(positionStats) => toInfiniteScrollParams(scrollState.lastScrollPosition, positionStats, distance))), tap((/**
* @param {?} __0
* @return {?}
*/
({ stats }) => scrollState.updateScroll(stats.scrolled, stats.totalToScroll))), filter((/**
* @param {?} __0
* @return {?}
*/
({ fire, scrollDown, stats: { totalToScroll } }) => shouldTriggerEvents(config.alwaysCallback, fire, scrollState.isTriggeredScroll(totalToScroll, scrollDown)))), tap((/**
* @param {?} __0
* @return {?}
*/
({ scrollDown, stats: { totalToScroll } }) => {
scrollState.updateTriggeredFlag(totalToScroll, scrollDown);
})), map(toInfiniteScrollAction));
}
/**
* @param {?} options
* @return {?}
*/
function attachScrollEvent(options) {
/** @type {?} */
let obs = fromEvent(options.container, 'scroll');
// For an unknown reason calling `sampleTime()` causes trouble for many users, even with `options.throttle = 0`.
// Let's avoid calling the function unless needed.
// See https://github.com/orizens/ngx-infinite-scroll/issues/198
if (options.throttle) {
obs = obs.pipe(sampleTime(options.throttle));
}
return obs;
}
/**
* @param {?} lastScrollPosition
* @param {?} stats
* @param {?} distance
* @return {?}
*/
function toInfiniteScrollParams(lastScrollPosition, stats, distance) {
const { scrollDown, fire } = getScrollStats(lastScrollPosition, stats, distance);
return {
scrollDown,
fire,
stats
};
}
/** @type {?} */
const InfiniteScrollActions = {
DOWN: '[NGX_ISE] DOWN',
UP: '[NGX_ISE] UP'
};
/**
* @param {?} response
* @return {?}
*/
function toInfiniteScrollAction(response) {
const { scrollDown, stats: { scrolled: currentScrollPosition } } = response;
return {
type: scrollDown ? InfiniteScrollActions.DOWN : InfiniteScrollActions.UP,
payload: {
currentScrollPosition
}
};
}
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
class InfiniteScrollDirective {
/**
* @param {?} element
* @param {?} zone
*/
constructor(element, zone) {
this.element = element;
this.zone = zone;
this.scrolled = new EventEmitter();
this.scrolledUp = new EventEmitter();
this.infiniteScrollDistance = 2;
this.infiniteScrollUpDistance = 1.5;
this.infiniteScrollThrottle = 150;
this.infiniteScrollDisabled = false;
this.infiniteScrollContainer = null;
this.scrollWindow = true;
this.immediateCheck = false;
this.horizontal = false;
this.alwaysCallback = false;
this.fromRoot = false;
}
/**
* @return {?}
*/
ngAfterViewInit() {
if (!this.infiniteScrollDisabled) {
this.setup();
}
}
/**
* @param {?} __0
* @return {?}
*/
ngOnChanges({ infiniteScrollContainer, infiniteScrollDisabled, infiniteScrollDistance }) {
/** @type {?} */
const containerChanged = inputPropChanged(infiniteScrollContainer);
/** @type {?} */
const disabledChanged = inputPropChanged(infiniteScrollDisabled);
/** @type {?} */
const distanceChanged = inputPropChanged(infiniteScrollDistance);
/** @type {?} */
const shouldSetup = (!disabledChanged && !this.infiniteScrollDisabled) ||
(disabledChanged && !infiniteScrollDisabled.currentValue) || distanceChanged;
if (containerChanged || disabledChanged || distanceChanged) {
this.destroyScroller();
if (shouldSetup) {
this.setup();
}
}
}
/**
* @return {?}
*/
setup() {
if (hasWindowDefined()) {
this.zone.runOutsideAngular((/**
* @return {?}
*/
() => {
this.disposeScroller = createScroller({
fromRoot: this.fromRoot,
alwaysCallback: this.alwaysCallback,
disable: this.infiniteScrollDisabled,
downDistance: this.infiniteScrollDistance,
element: this.element,
horizontal: this.horizontal,
scrollContainer: this.infiniteScrollContainer,
scrollWindow: this.scrollWindow,
throttle: this.infiniteScrollThrottle,
upDistance: this.infiniteScrollUpDistance
}).subscribe((/**
* @param {?} payload
* @return {?}
*/
(payload) => this.zone.run((/**
* @return {?}
*/
() => this.handleOnScroll(payload)))));
}));
}
}
/**
* @param {?} __0
* @return {?}
*/
handleOnScroll({ type, payload }) {
switch (type) {
case InfiniteScrollActions.DOWN:
return this.scrolled.emit(payload);
case InfiniteScrollActions.UP:
return this.scrolledUp.emit(payload);
default:
return;
}
}
/**
* @return {?}
*/
ngOnDestroy() {
this.destroyScroller();
}
/**
* @return {?}
*/
destroyScroller() {
if (this.disposeScroller) {
this.disposeScroller.unsubscribe();
}
}
}
InfiniteScrollDirective.decorators = [
{ type: Directive, args: [{
selector: '[infiniteScroll], [infinite-scroll], [data-infinite-scroll]'
},] },
];
/** @nocollapse */
InfiniteScrollDirective.ctorParameters = () => [
{ type: ElementRef },
{ type: NgZone }
];
InfiniteScrollDirective.propDecorators = {
scrolled: [{ type: Output }],
scrolledUp: [{ type: Output }],
infiniteScrollDistance: [{ type: Input }],
infiniteScrollUpDistance: [{ type: Input }],
infiniteScrollThrottle: [{ type: Input }],
infiniteScrollDisabled: [{ type: Input }],
infiniteScrollContainer: [{ type: Input }],
scrollWindow: [{ type: Input }],
immediateCheck: [{ type: Input }],
horizontal: [{ type: Input }],
alwaysCallback: [{ type: Input }],
fromRoot: [{ type: Input }]
};
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
class InfiniteScrollModule {
}
InfiniteScrollModule.decorators = [
{ type: NgModule, args: [{
declarations: [InfiniteScrollDirective],
exports: [InfiniteScrollDirective],
imports: [],
providers: []
},] },
];
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/**
* Angular library starter.
* Build an Angular library compatible with AoT compilation & Tree shaking.
* Written by Roberto Simonetti.
* MIT license.
* https://github.com/robisim74/angular-library-starter
*/
/**
* Entry point for all public APIs of the package.
*/
/**
* @fileoverview added by tsickle
* @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/**
* Generated bundle index. Do not edit.
*/
export { InfiniteScrollDirective, InfiniteScrollModule };
//# sourceMappingURL=ngx-infinite-scroll.js.map