mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-20 00:23:03 +00:00
fix: performance degradation when scrolling
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {ScrollingModule} from '@angular/cdk/scrolling';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {HttpClientModule} from '@angular/common/http';
|
||||
import {NgModule} from '@angular/core';
|
||||
@@ -135,6 +136,7 @@ import {VideoListItem} from './types/video/video-list-item.component';
|
||||
'm': 59,
|
||||
},
|
||||
}),
|
||||
ScrollingModule,
|
||||
StorageModule,
|
||||
TranslateModule.forChild(),
|
||||
ThingTranslateModule.forChild(),
|
||||
|
||||
@@ -20,6 +20,7 @@ import {Component} from '@angular/core';
|
||||
@Component({
|
||||
selector: 'stapps-skeleton-list-item',
|
||||
templateUrl: 'skeleton-list-item.html',
|
||||
styleUrls: ['skeleton-list-item.scss'],
|
||||
})
|
||||
export class SkeletonListItem {
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<ion-item>
|
||||
<ion-thumbnail slot="start">
|
||||
<ion-skeleton-text animated></ion-skeleton-text>
|
||||
</ion-thumbnail>
|
||||
<ion-thumbnail slot="start" class="ion-margin-end">
|
||||
<ion-skeleton-text animated></ion-skeleton-text>
|
||||
</ion-thumbnail>
|
||||
<ion-grid>
|
||||
<ion-row>
|
||||
<ion-col>
|
||||
|
||||
7
src/app/modules/data/elements/skeleton-list-item.scss
Normal file
7
src/app/modules/data/elements/skeleton-list-item.scss
Normal file
@@ -0,0 +1,7 @@
|
||||
::ng-deep {
|
||||
ion-grid,ion-col {
|
||||
padding-inline-start: 0px!important;
|
||||
padding-top: 0px!important;
|
||||
padding-bottom: 0px!important;
|
||||
}
|
||||
}
|
||||
@@ -25,12 +25,17 @@ import {DataRoutingService} from '../data-routing.service';
|
||||
templateUrl: 'data-list-item.html',
|
||||
})
|
||||
export class DataListItem {
|
||||
/**
|
||||
* Whether or not the list item should show a thumbnail
|
||||
*/
|
||||
@Input() hideThumbnail = false;
|
||||
|
||||
/**
|
||||
* An item to show
|
||||
*/
|
||||
@Input() item: SCThings;
|
||||
|
||||
constructor(private dataRoutingService: DataRoutingService) {}
|
||||
constructor(private readonly dataRoutingService: DataRoutingService) {}
|
||||
|
||||
/**
|
||||
* Emit event that an item was selected
|
||||
|
||||
@@ -1,37 +1,30 @@
|
||||
<ion-item class="ion-text-wrap" button="true" lines="inset" (click)="notifySelect()">
|
||||
<ion-thumbnail slot="start">
|
||||
<ion-item button="true" lines="inset" (click)="notifySelect()">
|
||||
<ion-thumbnail slot="start" *ngIf="!hideThumbnail" class="ion-margin-end">
|
||||
<ion-icon color="medium" [attr.name]="item.type | dataIcon"></ion-icon>
|
||||
</ion-thumbnail>
|
||||
<ion-label [ngSwitch]="true">
|
||||
<div>
|
||||
<stapps-catalog-list-item [item]="item" *ngSwitchCase="item.type === 'catalog'"></stapps-catalog-list-item>
|
||||
<stapps-date-series-list-item [item]="item" *ngSwitchCase="item.type === 'date series'"></stapps-date-series-list-item>
|
||||
<stapps-dish-list-item [item]="item" *ngSwitchCase="item.type === 'dish'"></stapps-dish-list-item>
|
||||
<stapps-event-list-item [item]="item" *ngSwitchCase="item.type === 'academic event'"></stapps-event-list-item>
|
||||
<stapps-event-list-item [item]="item" *ngSwitchCase="item.type === 'sport course'"></stapps-event-list-item>
|
||||
<stapps-favorite-list-item [item]="item" *ngSwitchCase="item.type === 'favorite'"></stapps-favorite-list-item>
|
||||
<stapps-message-list-item [item]="item" *ngSwitchCase="item.type === 'message'"></stapps-message-list-item>
|
||||
<stapps-organization-list-item [item]="item" *ngSwitchCase="item.type === 'organization'"></stapps-organization-list-item>
|
||||
<stapps-person-list-item [item]="item" *ngSwitchCase="item.type === 'person'"></stapps-person-list-item>
|
||||
<stapps-place-list-item [item]="item" *ngSwitchCase="item.type === 'building'"></stapps-place-list-item>
|
||||
<stapps-place-list-item [item]="item" *ngSwitchCase="item.type === 'floor'"></stapps-place-list-item>
|
||||
<stapps-place-list-item [item]="item" *ngSwitchCase="item.type === 'point of interest'"></stapps-place-list-item>
|
||||
<stapps-place-list-item [item]="item" *ngSwitchCase="item.type === 'room'"></stapps-place-list-item>
|
||||
<stapps-semester-list-item [item]="item" *ngSwitchCase="item.type === 'semester'"></stapps-semester-list-item>
|
||||
<stapps-video-list-item [item]="item" *ngSwitchCase="item.type === 'video'"></stapps-video-list-item>
|
||||
<ion-grid *ngSwitchDefault>
|
||||
<ion-row>
|
||||
<ion-col>
|
||||
<div class="ion-text-wrap">
|
||||
<h2 class="name">{{'name' | thingTranslate: item}}</h2>
|
||||
<p *ngIf="item.description">
|
||||
<stapps-long-inline-text [text]="'description' | thingTranslate: item" [size]="80"></stapps-long-inline-text>
|
||||
</p>
|
||||
<ion-note>{{'type' | thingTranslate: item}}</ion-note>
|
||||
</div>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
<ion-label class="ion-text-wrap" [ngSwitch]="true">
|
||||
<stapps-catalog-list-item [item]="item" *ngSwitchCase="item.type === 'catalog'"></stapps-catalog-list-item>
|
||||
<stapps-date-series-list-item [item]="item" *ngSwitchCase="item.type === 'date series'"></stapps-date-series-list-item>
|
||||
<stapps-dish-list-item [item]="item" *ngSwitchCase="item.type === 'dish'"></stapps-dish-list-item>
|
||||
<stapps-event-list-item [item]="item" *ngSwitchCase="item.type === 'academic event'"></stapps-event-list-item>
|
||||
<stapps-event-list-item [item]="item" *ngSwitchCase="item.type === 'sport course'"></stapps-event-list-item>
|
||||
<stapps-favorite-list-item [item]="item" *ngSwitchCase="item.type === 'favorite'"></stapps-favorite-list-item>
|
||||
<stapps-message-list-item [item]="item" *ngSwitchCase="item.type === 'message'"></stapps-message-list-item>
|
||||
<stapps-organization-list-item [item]="item" *ngSwitchCase="item.type === 'organization'"></stapps-organization-list-item>
|
||||
<stapps-person-list-item [item]="item" *ngSwitchCase="item.type === 'person'"></stapps-person-list-item>
|
||||
<stapps-place-list-item [item]="item" *ngSwitchCase="item.type === 'building'"></stapps-place-list-item>
|
||||
<stapps-place-list-item [item]="item" *ngSwitchCase="item.type === 'floor'"></stapps-place-list-item>
|
||||
<stapps-place-list-item [item]="item" *ngSwitchCase="item.type === 'point of interest'"></stapps-place-list-item>
|
||||
<stapps-place-list-item [item]="item" *ngSwitchCase="item.type === 'room'"></stapps-place-list-item>
|
||||
<stapps-semester-list-item [item]="item" *ngSwitchCase="item.type === 'semester'"></stapps-semester-list-item>
|
||||
<stapps-video-list-item [item]="item" *ngSwitchCase="item.type === 'video'"></stapps-video-list-item>
|
||||
<div *ngSwitchDefault>
|
||||
<h2>
|
||||
{{'name' | thingTranslate: item}}
|
||||
</h2>
|
||||
<p *ngIf="item.description">
|
||||
<stapps-long-inline-text [text]="'description' | thingTranslate: item" [size]="80"></stapps-long-inline-text>
|
||||
</p>
|
||||
</div>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
::ng-deep {
|
||||
ion-grid,ion-col {
|
||||
padding-inline-start: 0px!important;
|
||||
padding-top: 0px!important;
|
||||
padding-bottom: 0px!important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
|
||||
import {DataListComponent} from './data-list.component';
|
||||
import {TranslateModule} from '@ngx-translate/core';
|
||||
|
||||
describe('DataListComponent', () => {
|
||||
let component: DataListComponent;
|
||||
@@ -8,7 +9,10 @@ describe('DataListComponent', () => {
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ DataListComponent ]
|
||||
declarations: [ DataListComponent ],
|
||||
imports: [
|
||||
TranslateModule.forRoot(),
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
@@ -12,8 +12,11 @@
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
|
||||
import {CdkVirtualScrollViewport} from '@angular/cdk/scrolling';
|
||||
import {Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild} from '@angular/core';
|
||||
import {SCThings} from '@openstapps/core';
|
||||
import {BehaviorSubject, Observable, Subscription} from 'rxjs';
|
||||
|
||||
/**
|
||||
* Shows the list of items
|
||||
*/
|
||||
@@ -22,16 +25,84 @@ import {SCThings} from '@openstapps/core';
|
||||
templateUrl: 'data-list.html',
|
||||
styleUrls: ['data-list.scss'],
|
||||
})
|
||||
export class DataListComponent implements OnInit {
|
||||
// tslint:disable-next-line:completed-docs
|
||||
@Input() items: SCThings[];
|
||||
// tslint:disable-next-line:completed-docs
|
||||
@Output('loadmore') loadMore = new EventEmitter<Event>();
|
||||
|
||||
ngOnInit(): void {
|
||||
export class DataListComponent implements OnChanges, OnInit {
|
||||
/**
|
||||
* Amount of list items left to show (in percent) that should trigger a data reload
|
||||
*/
|
||||
private readonly reloadThreshold = 0.2;
|
||||
/**
|
||||
* All SCThings to display
|
||||
*/
|
||||
@Input() items?: SCThings[];
|
||||
/**
|
||||
* Stream of SCThings for virtual scroll to consume
|
||||
*/
|
||||
itemStream = new BehaviorSubject<SCThings[]>([]);
|
||||
/**
|
||||
* Output binding to trigger pagination fetch
|
||||
*/
|
||||
@Output('loadmore') loadMore = new EventEmitter<DataSourceRefreshed>();
|
||||
/**
|
||||
* Indicates whether or not the list is to display SCThings of a single type
|
||||
*/
|
||||
@Input() singleType = false;
|
||||
// tslint:disable-next-line: completed-docs
|
||||
@ViewChild(CdkVirtualScrollViewport) viewPort: CdkVirtualScrollViewport;
|
||||
|
||||
/**
|
||||
* Uniquely identifies item at a certain list index
|
||||
*/
|
||||
identifyItem(_index: number, item: SCThings) {
|
||||
return item.uid;
|
||||
}
|
||||
|
||||
notifyLoadMore(e: Event) {
|
||||
this.loadMore.emit(e);
|
||||
// tslint:disable-next-line: completed-docs
|
||||
ngOnChanges(_changes: SimpleChanges): void {
|
||||
if (Array.isArray(this.items)) {
|
||||
if (this.itemStream.getValue().length === 0) {
|
||||
this.itemStream = new BehaviorSubject<SCThings[]>(this.items);
|
||||
} else {
|
||||
if ((this.items[0].uid ?? '') !== this.itemStream.getValue()[0].uid) {
|
||||
this.itemStream = new BehaviorSubject<SCThings[]>(this.items);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// tslint:disable-next-line: completed-docs
|
||||
ngOnInit(): void {
|
||||
if (typeof this.queryChanged !== 'undefined') {
|
||||
this.subscriptions.push(this.queryChanged.subscribe(() => {
|
||||
this.viewPort.scrollToIndex(0);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// tslint:disable-next-line: completed-docs
|
||||
ngOnDestroy(): void {
|
||||
for (const subscription of this.subscriptions) {
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Component proxy for dataSource.finishedLoadMore
|
||||
*/
|
||||
notifyLoadMore() {
|
||||
this.loadMore.emit((items) => {
|
||||
this.itemStream.next(items);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to call whenever scroll view visible range changed
|
||||
*/
|
||||
scrolled(_event: Event) {
|
||||
if ((this.items?.length ?? 0) - this.viewPort.getRenderedRange().end <= (this.items?.length ?? 0) * this.reloadThreshold) {
|
||||
this.notifyLoadMore();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type DataSourceRefreshed = (items: SCThings[]) => unknown;
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
<ion-content>
|
||||
<div *ngIf="items; else loading">
|
||||
<ion-list *ngIf='items.length > 0; else empty'>
|
||||
<stapps-data-list-item [item]="item" *ngFor="let item of items"></stapps-data-list-item>
|
||||
</ion-list>
|
||||
<ng-template #empty>
|
||||
<div>
|
||||
<ion-label class='notFoundContainer'>
|
||||
{{'search.nothing_found' | translate}}
|
||||
</ion-label>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
<ng-template #loading>
|
||||
<stapps-skeleton-list-item *ngFor="let skeleton of [1, 2, 3, 4, 5]"></stapps-skeleton-list-item>
|
||||
</ng-template>
|
||||
<ion-infinite-scroll (ionInfinite)="notifyLoadMore($event)">
|
||||
<ion-infinite-scroll-content></ion-infinite-scroll-content>
|
||||
</ion-infinite-scroll>
|
||||
</ion-content>
|
||||
<cdk-virtual-scroll-viewport itemSize="80" minBufferPx="1500" maxBufferPx="2000" (scrolledIndexChange)="scrolled($event)"
|
||||
[style.display]="items && items.length ? 'block': 'none'">
|
||||
<ion-list>
|
||||
<stapps-data-list-item *cdkVirtualFor="let item of itemStream;trackBy: identifyItem" [item]="item"
|
||||
[hideThumbnail]="singleType"></stapps-data-list-item>
|
||||
</ion-list>
|
||||
</cdk-virtual-scroll-viewport>
|
||||
<div [style.display]="items && items.length === 0 ? 'block': 'none'">
|
||||
<ion-label class='notFoundContainer'>
|
||||
{{'search.nothing_found' | translate | titlecase}}
|
||||
</ion-label>
|
||||
</div>
|
||||
<ion-list [style.display]="items ? 'none': 'block'">
|
||||
<stapps-skeleton-list-item *ngFor="let skeleton of [1, 2, 3, 4, 5]"></stapps-skeleton-list-item>
|
||||
</ion-list>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
cdk-virtual-scroll-viewport {
|
||||
min-height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
::ng-deep {
|
||||
.cdk-virtual-scroll-content-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import {ContextMenuService} from '../../menu/context/context-menu.service';
|
||||
import {SettingsProvider} from '../../settings/settings.provider';
|
||||
import {DataRoutingService} from '../data-routing.service';
|
||||
import {DataProvider} from '../data.provider';
|
||||
import {DataSourceRefreshed} from './data-list.component';
|
||||
|
||||
/**
|
||||
* SearchPageComponent queries things and shows list of things as search results and filter as context menu
|
||||
@@ -71,6 +72,10 @@ export class SearchPageComponent implements OnInit {
|
||||
* Time to wait for search query if search text is changing
|
||||
*/
|
||||
searchQueryDueTime = 1000;
|
||||
/**
|
||||
* Search response only ever contains a single SCThingType
|
||||
*/
|
||||
singleTypeResponse = false;
|
||||
/**
|
||||
* Api query sorting
|
||||
*/
|
||||
@@ -183,6 +188,7 @@ export class SearchPageComponent implements OnInit {
|
||||
|
||||
return this.dataProvider.search(searchOptions)
|
||||
.then(async (result) => {
|
||||
this.singleTypeResponse = result.facets.find(facet => facet.field === 'type')?.buckets.length === 1;
|
||||
if (append) {
|
||||
let items = await this.items;
|
||||
// append results
|
||||
@@ -219,10 +225,10 @@ export class SearchPageComponent implements OnInit {
|
||||
* Loads next page of things
|
||||
*/
|
||||
// tslint:disable-next-line:no-any
|
||||
async loadMore(event: any): Promise<void> {
|
||||
async loadMore(finished: DataSourceRefreshed): Promise<void> {
|
||||
this.from += this.pageSize;
|
||||
await this.fetchAndUpdateItems(true);
|
||||
event.target.complete();
|
||||
finished(await this.items);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<stapps-context contentId="data-list"></stapps-context>
|
||||
<stapps-context></stapps-context>
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
@@ -15,5 +15,5 @@
|
||||
</ion-header>
|
||||
|
||||
<ion-content>
|
||||
<stapps-data-list id="data-list" [items]="items | async" (loadmore)="loadMore($event)"></stapps-data-list>
|
||||
<stapps-data-list id="data-list" [items]="items | async" [singleType]="singleTypeResponse" (loadmore)="loadMore($event)"></stapps-data-list>
|
||||
</ion-content>
|
||||
|
||||
Reference in New Issue
Block a user