diff --git a/projects/deneb-ui-demo/src/app/app.module.ts b/projects/deneb-ui-demo/src/app/app.module.ts index 7689bd7..db2876c 100644 --- a/projects/deneb-ui-demo/src/app/app.module.ts +++ b/projects/deneb-ui-demo/src/app/app.module.ts @@ -17,6 +17,7 @@ import { TimelineMeterExample } from './timeline-meter/timeline-meter.component' import { ToastDemo } from './toast/toast.component'; import { ToggleDemo } from './toggle/toggle.component'; import { ResponsiveImageComponent } from './responsive-image/responsive-image.component'; +import { InfiniteService } from './infinite-list/infinite.service'; @NgModule({ declarations: [ @@ -78,6 +79,9 @@ import { ResponsiveImageComponent } from './responsive-image/responsive-image.co } ], {enableTracing: false}) ], + providers: [ + InfiniteService + ], bootstrap: [App] }) export class AppModule { diff --git a/projects/deneb-ui-demo/src/app/infinite-list/infinite-list.component.ts b/projects/deneb-ui-demo/src/app/infinite-list/infinite-list.component.ts index 292fc74..12751e2 100644 --- a/projects/deneb-ui-demo/src/app/infinite-list/infinite-list.component.ts +++ b/projects/deneb-ui-demo/src/app/infinite-list/infinite-list.component.ts @@ -1,6 +1,12 @@ import {Component, ElementRef, OnInit, ViewChild} from '@angular/core'; +import { InfiniteService } from './infinite.service'; +import { + InfiniteDataBucket, + InfiniteDataBucketsStub +} from '../../../../irohalab/deneb-ui/src/infinite-list/infinite-data-collection'; +import { lastValueFrom } from 'rxjs'; -const MOCK_DATA = require('../../MOCK_DATA.json'); +// const MOCK_DATA = require('../../MOCK_DATA.json'); @Component({ selector: 'infinite-list-demo', @@ -11,13 +17,16 @@ const MOCK_DATA = require('../../MOCK_DATA.json'); height: 100%; position: relative; background-color: #f0f0f0; + display: flex; + flex-direction: row; } infinite-list { width: 600px; height: 100%; display: block; } - `] + `], + providers: [InfiniteService] }) export class InfiniteListDemo implements OnInit { @@ -25,26 +34,23 @@ export class InfiniteListDemo implements OnInit { newPosition = 0; + bucketsStub: InfiniteDataBucketsStub; + scrollPosition: number = 0; + + constructor(private infiniteService: InfiniteService) { + } + onScrollPositionChange(p: number) { - console.log(p); + this.scrollPosition = p; } ngOnInit(): void { + this.bucketsStub = new InfiniteDataBucketsStub(this.infiniteService.buckets, this, this.onLoadBucket); + // this.onLoadBucket(0); + this.collection = []; + } - this.newPosition = 5000; - - setTimeout(() => { - this.collection = MOCK_DATA; - }, 3000); - - // setTimeout(() => { - // this.collection = MOCK_DATA.filter(item => item.id % 2 === 0); - // // console.log('current collection', this.collection); - // }, 5000); - // - // setTimeout(() => { - // this.collection = MOCK_DATA.filter(item => true); - // // console.log('current collection', this.collection); - // }, 7000); + onLoadBucket(bucketIndex: number): Promise> { + return this.infiniteService.getBucketData(bucketIndex); } } diff --git a/projects/deneb-ui-demo/src/app/infinite-list/infinite-list.html b/projects/deneb-ui-demo/src/app/infinite-list/infinite-list.html index 4313598..5819439 100644 --- a/projects/deneb-ui-demo/src/app/infinite-list/infinite-list.html +++ b/projects/deneb-ui-demo/src/app/infinite-list/infinite-list.html @@ -1,7 +1,13 @@
- + +
+

scroll position: {{scrollPosition}}

+
diff --git a/projects/deneb-ui-demo/src/app/infinite-list/infinite.service.ts b/projects/deneb-ui-demo/src/app/infinite-list/infinite.service.ts new file mode 100644 index 0000000..1e51439 --- /dev/null +++ b/projects/deneb-ui-demo/src/app/infinite-list/infinite.service.ts @@ -0,0 +1,37 @@ +import { Injectable } from '@angular/core'; +import { InfiniteDataBucket } from '../../../../irohalab/deneb-ui/src/infinite-list/infinite-data-collection'; +import { Observable, of } from 'rxjs'; +const MOCK_DATA = require('../../MOCK_DATA.json'); +const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + +@Injectable() +export class InfiniteService { + public buckets!: InfiniteDataBucket[]; + constructor() { + const length = MOCK_DATA.length; + const bucketCount = Math.ceil(length / 20); + this.buckets = []; + for (let i = 0; i < bucketCount; i++) { + const start = i * 20; + let end = start + 20; + if (end >= length) { + end = length - 1; + } + this.buckets.push({ + start, + end, + filled: false + }); + } + } + + async getBucketData(bucketIndex: number): Promise { + await sleep(3000); + console.log('bucket data filled'); + const bucket = this.buckets[bucketIndex]; + if (bucket) { + return await Promise.resolve(MOCK_DATA.slice(bucket.start, bucket.end + 1)); + } + return Promise.reject(new Error('bucket index out of range')); + } +} diff --git a/projects/deneb-ui-demo/src/app/infinite-list/list-item/list-item.component.ts b/projects/deneb-ui-demo/src/app/infinite-list/list-item/list-item.component.ts index 1b90848..da37c7e 100644 --- a/projects/deneb-ui-demo/src/app/infinite-list/list-item/list-item.component.ts +++ b/projects/deneb-ui-demo/src/app/infinite-list/list-item/list-item.component.ts @@ -43,6 +43,13 @@ import { InfiniteList, SCROLL_STATE } from '../../../../../irohalab/deneb-ui/src padding: 0.5rem; background-color: #eaeaea; } + .state-label { + position: absolute; + bottom: 10px; + right: 10px; + padding: 0.5rem; + background-color: #eaeaea; + } `] }) export class ListItemExample implements OnDestroy { @@ -50,11 +57,15 @@ export class ListItemExample implements OnDestroy { @Input() index: number; + @Input() isInit: boolean; + + state: string; + private _subscription = new Subscription(); constructor(private _infiniteList: InfiniteList) { this._subscription.add(this._infiniteList.scrollStateChange.subscribe((state: SCROLL_STATE) => { - console.log('state changed: ', state); + this.state = SCROLL_STATE[state]; })); } diff --git a/projects/deneb-ui-demo/src/app/infinite-list/list-item/list-item.html b/projects/deneb-ui-demo/src/app/infinite-list/list-item/list-item.html index c066269..4ef0b6a 100644 --- a/projects/deneb-ui-demo/src/app/infinite-list/list-item/list-item.html +++ b/projects/deneb-ui-demo/src/app/infinite-list/list-item/list-item.html @@ -1,11 +1,23 @@
-
+
image
{{item.content}}
+
{{state}}
+
{{index}}
+
+
+
+
+
+
+
+
+
+
{{state}}
{{index}}
diff --git a/projects/deneb-ui-demo/src/app/timeline-meter/timeline-meter.service.ts b/projects/deneb-ui-demo/src/app/timeline-meter/timeline-meter.service.ts new file mode 100644 index 0000000..fe51ac5 --- /dev/null +++ b/projects/deneb-ui-demo/src/app/timeline-meter/timeline-meter.service.ts @@ -0,0 +1,11 @@ +import { Injectable } from '@angular/core'; + +@Injectable() +export class TimelineMeterService { + // getTimeline(): {}[] { + // + // } + getBuckets(): any[] { + return []; + } +} diff --git a/projects/irohalab/deneb-ui/src/infinite-list/index.ts b/projects/irohalab/deneb-ui/src/infinite-list/index.ts index fed029d..f163fb4 100644 --- a/projects/irohalab/deneb-ui/src/infinite-list/index.ts +++ b/projects/irohalab/deneb-ui/src/infinite-list/index.ts @@ -19,3 +19,4 @@ export class UIInfiniteListModule { export * from './infinite-for-of'; export * from './infinite-list'; +export * from './infinite-data-collection'; diff --git a/projects/irohalab/deneb-ui/src/infinite-list/infinite-data-collection.ts b/projects/irohalab/deneb-ui/src/infinite-list/infinite-data-collection.ts new file mode 100644 index 0000000..31ff107 --- /dev/null +++ b/projects/irohalab/deneb-ui/src/infinite-list/infinite-data-collection.ts @@ -0,0 +1,17 @@ +export type InfiniteDataBucket = { + start: number; + end: number; + filled?: boolean; + fetching?: boolean; +} + +export class InfiniteDataBucketsStub { + constructor(public buckets: InfiniteDataBucket[], + public context: any, + public onLoadBucket: (bucketIndex: number) => Promise>) { + } + + loadBucket(bucketIndex: number): Promise> { + return this.onLoadBucket.call(this.context, bucketIndex); + } +} diff --git a/projects/irohalab/deneb-ui/src/infinite-list/infinite-for-of.ts b/projects/irohalab/deneb-ui/src/infinite-list/infinite-for-of.ts index da18054..6eabeb3 100644 --- a/projects/irohalab/deneb-ui/src/infinite-list/infinite-for-of.ts +++ b/projects/irohalab/deneb-ui/src/infinite-list/infinite-for-of.ts @@ -22,26 +22,27 @@ import { } from '@angular/core'; import {InfiniteList} from './infinite-list'; import {Subscription} from 'rxjs'; +import { InfiniteDataBucket, InfiniteDataBucketsStub } from './infinite-data-collection'; export class Recycler { private limit: number = 0; - private _scrapViews: Map = new Map(); + private scrapViews: Map = new Map(); getView(position: number): ViewRef | null { - let view = this._scrapViews.get(position); - if (!view && this._scrapViews.size > 0) { - position = this._scrapViews.keys().next().value; - view = this._scrapViews.get(position); + let view = this.scrapViews.get(position); + if (!view && this.scrapViews.size > 0) { + position = this.scrapViews.keys().next().value; + view = this.scrapViews.get(position); } if (view) { - this._scrapViews.delete(position); + this.scrapViews.delete(position); } return view || null; } recycleView(position: number, view: ViewRef) { view.detach(); - this._scrapViews.set(position, view); + this.scrapViews.set(position, view); } /** @@ -51,12 +52,12 @@ export class Recycler { if (this.limit <= 1) { return; } - let keyIterator = this._scrapViews.keys(); + let keyIterator = this.scrapViews.keys(); let key: number; - while (this._scrapViews.size > this.limit) { + while (this.scrapViews.size > this.limit) { key = keyIterator.next().value; - this._scrapViews.get(key).destroy(); - this._scrapViews.delete(key); + this.scrapViews.get(key).destroy(); + this.scrapViews.delete(key); } } @@ -66,15 +67,15 @@ export class Recycler { } clean() { - this._scrapViews.forEach((view: ViewRef) => { + this.scrapViews.forEach((view: ViewRef) => { view.destroy(); }); - this._scrapViews.clear(); + this.scrapViews.clear(); } } export class InfiniteRow { - constructor(public $implicit: any, public index: number, public count: number) { + constructor(public $implicit: any, public index: number, public count: number, public isInitialized: boolean) { } get first(): boolean { @@ -135,10 +136,12 @@ export class InfiniteForOf implements OnChanges, DoCheck, OnInit, OnDestroy { private _pendingMeasurement: number; - private _collection: any[]; + private _collection: any[] = []; private _recycler: Recycler = new Recycler(); + private _bucketsStub: InfiniteDataBucketsStub; + @Input() infiniteForOf: NgIterable; @Input() @@ -157,6 +160,14 @@ export class InfiniteForOf implements OnChanges, DoCheck, OnInit, OnDestroy { return this._trackByFn; } + @Input() + set infiniteForWithBucket(stub: InfiniteDataBucketsStub) { + if (!stub) { + this._bucketsStub = new InfiniteDataBucketsStub([], null, null); + } + this._bucketsStub = stub; + } + @Input() set infiniteForTemplate(value: TemplateRef) { if (value) { @@ -164,6 +175,18 @@ export class InfiniteForOf implements OnChanges, DoCheck, OnInit, OnDestroy { } } + get buckets(): InfiniteDataBucket[] { + return this._bucketsStub ? this._bucketsStub.buckets : []; + } + + get length() { + if (this.buckets.length === 0) { + return this._collection ? this._collection.length : 0; + } else { + return this.buckets[this.buckets.length - 1].end + 1; + } + } + constructor(private _infiniteList: InfiniteList, private _differs: IterableDiffers, private _template: TemplateRef, @@ -194,11 +217,8 @@ export class InfiniteForOf implements OnChanges, DoCheck, OnInit, OnDestroy { } private applyChanges(changes: IterableChanges) { - if (!this._collection) { - this._collection = []; - } let isMeasurementRequired = false; - + console.log(changes); changes.forEachOperation((item: IterableChangeRecord, adjustedPreviousIndex: number, currentIndex: number) => { if (item.previousIndex == null) { // new item @@ -222,6 +242,7 @@ export class InfiniteForOf implements OnChanges, DoCheck, OnInit, OnDestroy { if (isMeasurementRequired) { this.requestMeasure(); + return; } this.requestLayout(); @@ -238,9 +259,12 @@ export class InfiniteForOf implements OnChanges, DoCheck, OnInit, OnDestroy { this.requestLayout(); } )); - this._subscription.add(this._infiniteList.sizeChange.subscribe( + this._subscription.add(this._infiniteList.sizeChange + .pipe(filter(([width, height]) => { + return width !== 0 && height !== 0; + })).subscribe( ([width, height]) => { - // console.log('sizeChange: ', width, height); + console.log('sizeChange: ', width, height); this._containerWidth = width; this._containerHeight = height; this.requestMeasure(); @@ -272,9 +296,8 @@ export class InfiniteForOf implements OnChanges, DoCheck, OnInit, OnDestroy { } private measure() { - let collectionNumber = !this._collection || this._collection.length === 0 ? 0 : this._collection.length; this._isInMeasure = true; - this._infiniteList.holderHeight = this._infiniteList.rowHeight * collectionNumber; + this._infiniteList.holderHeight = this._infiniteList.rowHeight * this.length; // calculate a approximate number of which a view can contain this.calculateScrapViewsLimit(); this._isInMeasure = false; @@ -291,7 +314,8 @@ export class InfiniteForOf implements OnChanges, DoCheck, OnInit, OnDestroy { let {width, height} = this._infiniteList.measure(); this._containerWidth = width; this._containerHeight = height; - if (!this._collection || this._collection.length === 0) { + if (this.length === 0) { + console.log('length = 0 layout'); // detach all views without recycle them. for (let i = 0; i < this._viewContainerRef.length; i++) { let child = > this._viewContainerRef.get(i); @@ -305,6 +329,7 @@ export class InfiniteForOf implements OnChanges, DoCheck, OnInit, OnDestroy { this._invalidate = false; return; } + console.log('length != 0 layout'); this.findPositionInRange(); for (let i = 0; i < this._viewContainerRef.length; i++) { let child = > this._viewContainerRef.get(i); @@ -371,22 +396,66 @@ export class InfiniteForOf implements OnChanges, DoCheck, OnInit, OnDestroy { let firstPositionOffset = scrollY - firstPosition * this._infiniteList.rowHeight; let lastPosition = Math.ceil((this._containerHeight + firstPositionOffset) / this._infiniteList.rowHeight) + firstPosition; this._firstItemPosition = Math.max(firstPosition - 1, 0); - this._lastItemPosition = Math.min(lastPosition + 1, this._collection.length - 1); + this._lastItemPosition = Math.min(lastPosition + 1, this.length - 1); } private getView(position: number): ViewRef { - let view = this._recycler.getView(position); + let bucketIndex = -1; + if (this.buckets.length > 0) { + bucketIndex = this.findBucketIndexByPosition(position); + } + if (bucketIndex > -1) { + const bucket = this.buckets[bucketIndex]; + if (!bucket.filled) { + this.loadBucket(bucketIndex); + } + } let item = this._collection[position]; - let count = this._collection.length; + const isInitialized = !!item; + let count = this.length; + let view = this._recycler.getView(position); if (!view) { - view = this._template.createEmbeddedView(new InfiniteRow(item, position, count)); + view = this._template.createEmbeddedView(new InfiniteRow(item || {}, position, count, isInitialized)); } else { - (view as EmbeddedViewRef).context.$implicit = item; + (view as EmbeddedViewRef).context.$implicit = item || {}; (view as EmbeddedViewRef).context.index = position; (view as EmbeddedViewRef).context.count = count; + (view as EmbeddedViewRef).context.isInitialized = isInitialized; } return view; } + + private findBucketIndexByPosition(position: number): number { + for (let i = 0; i < this.buckets.length; i++) { + let bucket = this.buckets[i]; + if (bucket.start <= position && position <= bucket.end) { + return i; + } + } + return -1; + } + + private loadBucket(bucketIndex: number): void { + if (this.buckets.length === 0) { + return; + } + const bucket = this.buckets[bucketIndex]; + if (!bucket || bucket.fetching) { + return; + } + bucket.fetching = true; + this._bucketsStub.loadBucket(bucketIndex) + .then((bucketData: Iterable) => { + bucket.fetching = false; + let i = 0; + for (let item of bucketData) { + this._collection[bucket.start + i] = item; + i++; + } + bucket.filled = true; + this.requestLayout(); + }); + } }