@schoolbelle/common
Version: 
678 lines (670 loc) • 20.8 kB
JavaScript
import { isEqual, get, minBy, maxBy, findIndex, sortedIndexBy, sortedLastIndexBy } from 'lodash-es';
import { v4 } from 'uuid';
import { Subject, merge, throwError, of, Subscription, timer } from 'rxjs';
import { debounceTime, distinctUntilChanged, tap, filter, map, share, startWith, switchMap, catchError, take, debounce } from 'rxjs/operators';
import { Injectable, Component, Input, ChangeDetectorRef, ChangeDetectionStrategy, NgModule, defineInjectable } from '@angular/core';
import { CommonModule } from '@angular/common';
/**
 * @fileoverview added by tsickle
 * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
 */
/**
 * @template T
 */
class InfiniteListService {
    constructor() {
        this.id = v4();
        this._list = [];
        this.list = [];
        this.queryParams = {};
        this.newQueryParams = {};
        this._isLoading = false;
        this._noMore = false;
        this._noLatest = false;
        this._stopped = false;
        this._filter = (/**
         * @param {?} item
         * @return {?}
         */
        (item) => { return true; });
        this._map = (/**
         * @param {?} item
         * @return {?}
         */
        (item) => { return item; });
        this.forceLoadMoreEvent = new Subject();
        this.forceLoadLatestEvent = new Subject();
        this.fakeChangeEvent = new Subject();
        this.onQueryParamsChange = new Subject();
        this.onScrollHittingBottom = new Subject();
        this.onScrollHittingTop = new Subject();
        this.disableOnScrollHittingBottomOrTop = false;
        this.listFetchedEvent = new Subject();
        this.listChangeEvent = new Subject();
        this.propertyChangeEvent = new Subject();
        this.errorEvent = new Subject();
    }
    /**
     * @return {?}
     */
    get isLoading() {
        return this._isLoading;
    }
    /**
     * @param {?} n
     * @return {?}
     */
    set isLoading(n) {
        /** @type {?} */
        const o = this._isLoading;
        if (o !== n) {
            this._isLoading = n;
            this.propertyChangeEvent.next({ type: 'isLoading', data: { n, o } });
        }
    }
    /**
     * @return {?}
     */
    get noMore() {
        return this._noMore;
    }
    /**
     * @param {?} n
     * @return {?}
     */
    set noMore(n) {
        /** @type {?} */
        const o = this._noMore;
        if (o !== n) {
            this._noMore = n;
            this.propertyChangeEvent.next({ type: 'noMore', data: { n, o } });
        }
    }
    /**
     * @return {?}
     */
    get noLatest() {
        return this._noLatest;
    }
    /**
     * @param {?} n
     * @return {?}
     */
    set noLatest(n) {
        /** @type {?} */
        const o = this._noLatest;
        if (o !== n) {
            this._noLatest = n;
            this.propertyChangeEvent.next({ type: 'noLatest', data: { n, o } });
        }
    }
    /**
     * @return {?}
     */
    get stopped() {
        return this._stopped;
    }
    /**
     * @param {?} n
     * @return {?}
     */
    set stopped(n) {
        /** @type {?} */
        const o = this._stopped;
        if (o !== n) {
            this._stopped = n;
            this.propertyChangeEvent.next({ type: 'stopped', data: { n, o } });
        }
    }
    /**
     * @return {?}
     */
    loadMore() {
        this.forceLoadMoreEvent.next(null);
        return this.listFetchedEvent.asObservable().pipe(filter((/**
         * @param {?} e
         * @return {?}
         */
        e => e.type === 'fetched')), take(1));
    }
    /**
     * @return {?}
     */
    loadLatest() {
        this.forceLoadLatestEvent.next(null);
        return this.listFetchedEvent.asObservable().pipe(filter((/**
         * @param {?} e
         * @return {?}
         */
        e => e.type === 'fetched')), take(1));
    }
    /**
     * @param {?} n
     * @return {?}
     */
    set filter(n) {
        /** @type {?} */
        const o = this._filter;
        if (o === n)
            return;
        this._filter = n;
        this._stopped = false;
        this.propertyChangeEvent.next({ type: 'filter', data: { o, n } });
    }
    /**
     * @return {?}
     */
    get filter() {
        return this._filter;
    }
    /**
     * @param {?} n
     * @return {?}
     */
    set map(n) {
        /** @type {?} */
        const o = this._map;
        if (o === n)
            return;
        this._map = n;
        this._stopped = false;
        this.propertyChangeEvent.next({ type: 'map', data: { o, n } });
    }
    /**
     * @return {?}
     */
    get map() {
        return this._map;
    }
    /**
     * @return {?}
     */
    get onChange() {
        return merge(this.onListChange, this.onPropertyChange, this.onError);
    }
    /**
     * @return {?}
     */
    get onPropertyChange() {
        return this.propertyChangeEvent.asObservable();
    }
    /**
     * @return {?}
     */
    get onListChange() {
        return this.listChangeEvent.asObservable();
    }
    /**
     * @return {?}
     */
    get onError() {
        return this.errorEvent.asObservable();
    }
    /**
     * @param {?} loadfn
     * @param {?} id_key_name
     * @param {?=} defaultParams
     * @param {?=} opt
     * @return {?}
     */
    getListObservable(loadfn, id_key_name, defaultParams = {}, opt = null) {
        if (!opt)
            opt = {};
        if (typeof opt.inital_run === 'undefined')
            opt.inital_run = false;
        if (typeof opt.max_size === 'undefined')
            opt.max_size = 10;
        this.id_key_name = id_key_name;
        this.max_size = opt.max_size;
        this.initial_run = opt.inital_run;
        return merge(this.onQueryParamsChange.pipe(//watch queryParams change
        debounceTime(500), distinctUntilChanged((/**
         * @param {?} a
         * @param {?} b
         * @return {?}
         */
        (a, b) => isEqual(a, b))), tap((/**
         * @param {?} params
         * @return {?}
         */
        (params) => this.newQueryParams = params)), map((/**
         * @param {?} data
         * @return {?}
         */
        (data) => ({ type: 'queryChanged', data })))), this.onScrollHittingBottom.pipe(//watch scroll-reaching-bottom event
        filter((/**
         * @return {?}
         */
        () => !this.disableOnScrollHittingBottomOrTop)), debounceTime(500), filter((/**
         * @return {?}
         */
        () => this.noMore === false)), filter((/**
         * @return {?}
         */
        () => this.stopped === false)), map((/**
         * @return {?}
         */
        () => ({ type: 'bottomReached', data: null })))), this.onScrollHittingTop.pipe(filter((/**
         * @return {?}
         */
        () => !this.disableOnScrollHittingBottomOrTop)), debounceTime(500), filter((/**
         * @return {?}
         */
        () => this.noLatest === false)), filter((/**
         * @return {?}
         */
        () => this.stopped === false)), map((/**
         * @return {?}
         */
        () => ({ type: 'topReached', data: null })))), this.forceLoadMoreEvent.asObservable().pipe(map((/**
         * @return {?}
         */
        () => ({ type: 'bottomReached', data: null })))), this.forceLoadLatestEvent.asObservable().pipe(map((/**
         * @return {?}
         */
        () => ({ type: 'topReached', data: null })))), this.fakeChangeEvent.asObservable().pipe(map((/**
         * @return {?}
         */
        () => ({ type: 'fakeChange', data: null })))), this.propertyChangeEvent.asObservable().pipe(filter((/**
         * @param {?} e
         * @return {?}
         */
        (e) => ['stopped', 'filter', 'map'].includes(e.type)))))
            .pipe(opt.inital_run ? startWith({ type: 'bottomReached', data: null }) : tap(null), tap((/**
         * @return {?}
         */
        () => this.isLoading = false)), switchMap((/**
         * @param {?} event
         * @return {?}
         */
        event => {
            /** @type {?} */
            let params;
            if (event.type === 'stopped' && event.data.n) {
                return of(null);
            }
            if (event.type === 'filter' || event.type === 'map' || event.type === 'fakeChange') {
                return of(null);
            }
            if (event.type === 'topReached') {
                /** @type {?} */
                const first_id = get(maxBy(this._list, id_key_name), id_key_name);
                params = Object.assign({ first_id }, defaultParams, this.newQueryParams, { last_id: null });
            }
            else {
                /** @type {?} */
                const last_id = get(minBy(this._list, id_key_name), id_key_name);
                params = Object.assign({ last_id }, defaultParams, this.newQueryParams);
            }
            this.isLoading = true;
            return loadfn(params).pipe(map((/**
             * @param {?} partial
             * @return {?}
             */
            (partial) => {
                if (!isEqual(this.newQueryParams, this.queryParams)) {
                    this.queryParams = this.newQueryParams;
                    this.flush(false);
                }
                return partial;
            })), tap((/**
             * @param {?} partial
             * @return {?}
             */
            (partial) => {
                // size query will be used when set
                if (partial.length < this.max_size) {
                    if (event.type === 'topReached')
                        this.noLatest = true;
                    else
                        this.noMore = true;
                }
            })), // handle no more
            tap((/**
             * @param {?} partial
             * @return {?}
             */
            partial => partial.forEach((/**
             * @param {?} item
             * @return {?}
             */
            item => this.insert(item))))), // push into _list
            tap((/**
             * @return {?}
             */
            () => this.disableOnScrollHittingBottomOrTop = false), (/**
             * @return {?}
             */
            () => this.disableOnScrollHittingBottomOrTop = false)), tap((/**
             * @param {?} partial
             * @return {?}
             */
            (partial) => this.listFetchedEvent.next({ type: 'fetched', data: partial }))), catchError((/**
             * @param {?} error
             * @return {?}
             */
            error => {
                console.error(error);
                this.errorEvent.next({ type: 'error', error });
                return throwError(error);
            })));
        })), tap((/**
         * @return {?}
         */
        () => this.isLoading = false)))
            .pipe(map((/**
         * @return {?}
         */
        () => this.list)), share());
    }
    /**
     * @param {?} queryParams
     * @param {?=} initial_run
     * @return {?}
     */
    updateQueryParams(queryParams, initial_run = true) {
        if (isEqual(queryParams, this.queryParams))
            return;
        this.disableOnScrollHittingBottomOrTop = true;
        this.flush();
        if (initial_run) {
            this.onQueryParamsChange.next(queryParams);
        }
        else {
            this.newQueryParams = queryParams;
        }
    }
    /**
     * @return {?}
     */
    scrollHasHitBottom() {
        this.onScrollHittingBottom.next();
    }
    /**
     * @return {?}
     */
    scrollHasHitTop() {
        this.onScrollHittingTop.next();
    }
    /**
     * @protected
     * @param {?} item
     * @return {?}
     */
    insert(item) {
        /** @type {?} */
        const startInnerIndex = sortedIndexBy(this._list, item, (/**
         * @param {?} li
         * @return {?}
         */
        li => -1 * li[this.id_key_name]));
        /** @type {?} */
        const endInnerIndex = sortedLastIndexBy(this._list, item, (/**
         * @param {?} li
         * @return {?}
         */
        li => -1 * li[this.id_key_name]));
        /** @type {?} */
        const startOuterIndex = sortedIndexBy(this.list, item, (/**
         * @param {?} li
         * @return {?}
         */
        li => -1 * li[this.id_key_name]));
        /** @type {?} */
        const endOuterIndex = sortedLastIndexBy(this.list, item, (/**
         * @param {?} li
         * @return {?}
         */
        li => -1 * li[this.id_key_name]));
        if (startInnerIndex === endInnerIndex) {
            this._list.splice(endInnerIndex, 0, item);
        }
        if (this.filter(item) && startOuterIndex === endOuterIndex) {
            /** @type {?} */
            const n = this.map(item);
            this.list.splice(endOuterIndex, 0, n);
            return startOuterIndex;
        }
        else {
            return -1;
        }
    }
    /**
     * @param {?} item
     * @return {?}
     */
    add(item) {
        /** @type {?} */
        const index = this.insert(item);
        if (index !== -1) {
            this.fakeChangeEvent.next();
            this.listChangeEvent.next({ type: 'add', data: { n: this.map(item) } });
        }
        return index;
    }
    /**
     * @param {?} item_o
     * @param {?} item_n
     * @return {?}
     */
    replace(item_o, item_n) {
        /** @type {?} */
        const innerIndex = sortedIndexBy(this._list, item_o, (/**
         * @param {?} li
         * @return {?}
         */
        li => -1 * li[this.id_key_name]));
        /** @type {?} */
        const outerIndex = sortedIndexBy(this.list, item_o, (/**
         * @param {?} li
         * @return {?}
         */
        li => -1 * li[this.id_key_name]));
        if (innerIndex !== -1) {
            this._list.splice(innerIndex, 1, item_n);
        }
        if (this.filter(item_n) && outerIndex !== -1) {
            /** @type {?} */
            const o = this.list[outerIndex];
            /** @type {?} */
            const n = this.map(item_n);
            this.list.splice(outerIndex, 1, n);
            this.listChangeEvent.next({ type: 'replace', data: { o: this.map(o), n: this.map(n) } });
            return outerIndex;
        }
        else {
            return -1;
        }
    }
    /**
     * @param {?} conditions
     * @param {?} data
     * @return {?}
     */
    update(conditions, data) {
        /** @type {?} */
        const index = findIndex(this._list, conditions);
        if (index !== -1) {
            /** @type {?} */
            const o = this._list[index];
            /** @type {?} */
            const n = Object.assign({}, o, data);
            return this.replace(o, n);
        }
        return index;
    }
    /**
     * @param {?} conditions
     * @return {?}
     */
    remove(conditions) {
        /** @type {?} */
        const innerIndex = findIndex(this._list, conditions);
        /** @type {?} */
        const outerIndex = findIndex(this.list, conditions);
        if (innerIndex !== -1) {
            this._list.splice(innerIndex, 1);
        }
        if (outerIndex !== -1) {
            /** @type {?} */
            const item = this.list[outerIndex];
            this.list.splice(outerIndex, 1);
            this.fakeChangeEvent.next();
            this.listChangeEvent.next({ type: 'remove', data: { o: this.map(item) } });
        }
        return outerIndex;
    }
    /**
     * rerun filter and map on list
     * @param {?=} emit
     * @return {?}
     */
    refresh(emit = true) {
        /** @type {?} */
        const o = this.list.slice(0);
        /** @type {?} */
        const n = this._list.filter((/**
         * @param {?} item
         * @return {?}
         */
        (item) => this.filter(item))).map((/**
         * @param {?} item
         * @return {?}
         */
        item => this.map(item)));
        this.list = n;
        if (emit)
            this.fakeChangeEvent.next();
        if (emit)
            this.listChangeEvent.next({ type: 'refresh', data: { n, o } });
        return this.list;
    }
    /**
     * @param {?=} emit
     * @return {?}
     */
    flush(emit = true) {
        this._list.splice(0, this._list.length);
        this.isLoading = false;
        this.noMore = false;
        this.stopped = false;
        return this.refresh(emit);
    }
}
InfiniteListService.decorators = [
    { type: Injectable, args: [{
                providedIn: 'root'
            },] }
];
/** @nocollapse */ InfiniteListService.ngInjectableDef = defineInjectable({ factory: function InfiniteListService_Factory() { return new InfiniteListService(); }, token: InfiniteListService, providedIn: "root" });
/**
 * @fileoverview added by tsickle
 * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
 */
class InfiniteListInfoComponent {
    /**
     * @param {?} cdRef
     */
    constructor(cdRef) {
        this.cdRef = cdRef;
        this.subscription = new Subscription();
    }
    /**
     * @return {?}
     */
    ngOnInit() {
        this.subscription.add(this.infinite.onChange.pipe(filter((/**
         * @param {?} e
         * @return {?}
         */
        e => ['noMore', 'stopped', 'refresh'].includes(e.type)))).subscribe((/**
         * @return {?}
         */
        () => {
            this.cdRef.detectChanges();
        })));
        this.subscription.add(this.infinite.onChange.pipe(filter((/**
         * @param {?} e
         * @return {?}
         */
        e => e.type === 'isLoading')), debounce((/**
         * @param {?} e
         * @return {?}
         */
        (e) => {
            if (this.infinite.isLoading !== true)
                return timer(1000);
            else
                return timer(100);
        }))).subscribe((/**
         * @return {?}
         */
        () => {
            this.cdRef.detectChanges();
        })));
    }
    /**
     * @return {?}
     */
    ngOnDestroy() {
        this.subscription.unsubscribe();
    }
    /**
     * @return {?}
     */
    stopSearching() {
        this.infinite.stopped = true;
        this.infinite.isLoading = false;
    }
    /**
     * @return {?}
     */
    resumeSearching() {
        this.infinite.stopped = false;
    }
}
InfiniteListInfoComponent.decorators = [
    { type: Component, args: [{
                selector: 'app-infinite-list-info',
                template: "<div style=\"height:40px;\" class=\"d-flex align-items-center py-5\">\n  <p class=\"w-100 text-center text-muted\" *ngIf=\"infinite.noMore && infinite.list.length !== 0\" style=\"opacity:.6;\" i18n>No more posts</p>\n  <p class=\"w-100 text-center text-muted\" *ngIf=\"!infinite.noMore && !infinite.stopped && infinite.isLoading\">\n      <ng-container *ngIf=\"!query;else searching\" i18n>Fetching...<i class=\"fa fa-spinner fa-spin fa-fw\"></i></ng-container>\n      <ng-template #searching>\n          <ng-container i18n>Searching...</ng-container>({{infinite.list.length}}/{{infinite._list.length}})\n          <button class=\"btn btn-sm btn-link ml-3\" (click)=\"stopSearching()\" i18n>Cancel Search</button>\n      </ng-template>\n  </p>\n  <p class=\"w-100 text-center text-muted\" *ngIf=\"!infinite.noMore && infinite.stopped\">\n      <button class=\"btn btn-sm btn-link\" (click)=\"resumeSearching()\" i18n>Resume Search</button>\n  </p>\n\n</div>\n",
                changeDetection: ChangeDetectionStrategy.OnPush,
                styles: [""]
            }] }
];
/** @nocollapse */
InfiniteListInfoComponent.ctorParameters = () => [
    { type: ChangeDetectorRef }
];
InfiniteListInfoComponent.propDecorators = {
    infinite: [{ type: Input }],
    query: [{ type: Input }]
};
/**
 * @fileoverview added by tsickle
 * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
 */
class InfiniteListInfoModule {
}
InfiniteListInfoModule.decorators = [
    { type: NgModule, args: [{
                declarations: [InfiniteListInfoComponent],
                imports: [
                    CommonModule
                ],
                exports: [InfiniteListInfoComponent],
            },] }
];
/**
 * @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
 */
/**
 * @fileoverview added by tsickle
 * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
 */
export { InfiniteListService, InfiniteListInfoComponent, InfiniteListInfoModule };
//# sourceMappingURL=schoolbelle-common-infinite-list.js.map