UNPKG

@schoolbelle/common

Version:

678 lines (670 loc) 20.8 kB
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