Skip to content

Commit f5a537b

Browse files
[UXP-34] implement images preload, refactor
1 parent 4fcb332 commit f5a537b

4 files changed

Lines changed: 42 additions & 176 deletions

File tree

src/app/shared/carousel/carousel.component.html

Lines changed: 2 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<ngb-carousel #carousel [interval]="2000" (slide)="onSlide($event)" class="ds-carousel" *ngIf="!(isLoading$ | async)">
2-
<ng-template ngbSlide *ngFor="let item of (currentPageItems$ | async); let i = index; let last = last">
2+
<ng-template ngbSlide *ngFor="let item of (carouselItems$ | async); let i = index; let last = last">
33
<ng-container *ngIf="getItemLink(item.indexableObject); let currentLink; else carouselContent">
44
<a *ngIf="internalLinkService.isLinkInternal(currentLink)" [routerLink]="internalLinkService.getRelativePath(currentLink)">
55
<ng-container *ngTemplateOutlet="carouselContent"></ng-container>
@@ -18,7 +18,7 @@
1818
<img [src]="href" [alt]="item.indexableObject.metadata[title][0].value" class="img-fluid"
1919
[ngClass]="{'w-100': carouselOptions.fitWidth, 'h-100': carouselOptions.fitHeight}">
2020
</div>
21-
<div class="carousel-caption">
21+
<div class="carousel-caption" *ngIf="item.indexableObject.metadata">
2222
<div class="carousel-caption-inner">
2323
<h3 data-test="carouselObjTitle" [class]="carouselOptions.titleStyle"
2424
*ngIf="item.indexableObject.metadata[title]">
@@ -56,38 +56,6 @@
5656
<i class="fas fa-pause" *ngIf="!paused"></i>
5757
</button>
5858
</div>
59-
<div class="mt-4 w-100 d-flex justify-content-center align-items-center" *ngIf="totalPages > 1">
60-
<div
61-
class="page-item"
62-
[class.disabled]="currentPage === 1 || (isLoading$ | async)"
63-
>
64-
<button (click)="previousPage()" [disabled]="currentPage === 1 || (isLoading$ | async)" class="page-link prev">
65-
«
66-
</button>
67-
</div>
68-
69-
<div
70-
*ngFor="let page of (currentlyVisiblePages$ | async)"
71-
class="page-item"
72-
[class.active]="page === currentPage"
73-
>
74-
<button
75-
class="page-link"
76-
(click)="changePage(page)"
77-
[disabled]="isLoading$ | async"
78-
>
79-
{{ page < 10 ? '0' + page : page }}
80-
</button>
81-
</div>
8259

83-
<div
84-
class="page-item"
85-
[class.disabled]="currentPage === totalPages || (isLoading$ | async)"
86-
>
87-
<button (click)="nextPage()" [disabled]="currentPage === totalPages || (isLoading$ | async)" class="page-link next">
88-
»
89-
</button>
90-
</div>
91-
</div>
9260

9361

src/app/shared/carousel/carousel.component.spec.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ describe('CarouselComponent', () => {
5050
getThumbnailFor(item: Item): Observable<RemoteData<Bitstream>> {
5151
return createSuccessfulRemoteDataObject$(new Bitstream());
5252
},
53-
findAllByItemAndBundleName(item: Item, bundleName: string, options?: FindListOptions, ...linksToFollow: FollowLinkConfig<Bitstream>[]): Observable<RemoteData<PaginatedList<Bitstream>>> {
53+
showableByItem(item: Item, bundleName: string, options?: FindListOptions, ...linksToFollow: FollowLinkConfig<Bitstream>[]): Observable<RemoteData<PaginatedList<Bitstream>>> {
5454
return createSuccessfulRemoteDataObject$(createPaginatedList([mockBitstream1]));
5555
},
5656
});
@@ -181,8 +181,8 @@ describe('CarouselComponent', () => {
181181
providers: [
182182
CarouselComponent,
183183
{ provide: ObjectCacheService, useValue: {} },
184-
{ provide: InternalLinkService, useValue: {} },
185-
{ provide: UUIDService, useValue: {} },
184+
{ provide: InternalLinkService, useValue: {} },
185+
{ provide: UUIDService, useValue: {} },
186186
{ provide: Store, useValue: {} },
187187
{ provide: RemoteDataBuildService, useValue: {} },
188188
{ provide: HALEndpointService, useValue: {} },
@@ -202,7 +202,7 @@ describe('CarouselComponent', () => {
202202
beforeEach(() => {
203203
fixture = TestBed.createComponent(CarouselComponent);
204204
component = fixture.componentInstance;
205-
mockBitstreamDataService.findAllByItemAndBundleName.and.returnValue(createSuccessfulRemoteDataObject$(createPaginatedList([mockBitstream1])));
205+
mockBitstreamDataService.showableByItem.and.returnValue(createSuccessfulRemoteDataObject$(createPaginatedList([mockBitstream1])));
206206
component.carouselOptions = carouselOptions;
207207

208208
fixture.detectChanges();
@@ -235,7 +235,7 @@ describe('CarouselComponent', () => {
235235
beforeEach(() => {
236236
fixture = TestBed.createComponent(CarouselComponent);
237237
component = fixture.componentInstance;
238-
mockBitstreamDataService.findAllByItemAndBundleName.and.returnValue(createSuccessfulRemoteDataObject$(createPaginatedList([mockBitstream2])));
238+
mockBitstreamDataService.showableByItem.and.returnValue(createSuccessfulRemoteDataObject$(createPaginatedList([mockBitstream2])));
239239
component.carouselOptions = carouselOptions;
240240

241241
fixture.detectChanges();
Lines changed: 34 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {Component, Input, OnInit, ViewChild} from '@angular/core';
22
import {NgbCarousel, NgbSlideEvent, NgbSlideEventSource} from '@ng-bootstrap/ng-bootstrap';
3-
import { BehaviorSubject, concatMap, from, Observable, of } from 'rxjs';
3+
import { BehaviorSubject, from, Observable } from 'rxjs';
44
import { filter, map, mergeMap, reduce, switchMap, take } from 'rxjs/operators';
55
import {PaginatedList} from '../../core/data/paginated-list.model';
66
import {BitstreamFormat} from '../../core/shared/bitstream-format.model';
@@ -18,8 +18,8 @@ import { SearchObjects } from '../search/models/search-objects.model';
1818
import { SortOptions } from '../../core/cache/models/sort-options.model';
1919
import { PaginationComponentOptions } from '../pagination/pagination-component-options.model';
2020
import { PaginatedSearchOptions } from '../search/models/paginated-search-options.model';
21-
import { DSpaceObjectType } from '../../core/shared/dspace-object-type.model';
2221
import { InternalLinkService } from '../../core/services/internal-link.service';
22+
import difference from 'lodash/difference';
2323

2424
/**
2525
* Component representing the Carousel component section.
@@ -38,12 +38,6 @@ export class CarouselComponent implements OnInit {
3838
@Input()
3939
carouselOptions: CarouselOptions;
4040

41-
/**
42-
* Option to activate dependency between slide event and pagination
43-
*/
44-
@Input()
45-
changePageOnSlide = false;
46-
4741
/**
4842
* Carousel section title field.
4943
*/
@@ -89,43 +83,27 @@ export class CarouselComponent implements OnInit {
8983
isLoading$ = new BehaviorSubject(true);
9084

9185
/**
92-
* The total search item pages
93-
*/
94-
totalPages = 0;
95-
/**
96-
* the total number of item available
97-
*/
98-
totalItems = 0;
99-
100-
/**
101-
* The map of the item to show
102-
*/
103-
itemMap: Map<number,ItemSearchResult[]> = new Map();
104-
105-
/**
106-
* The page number currently visualized
86+
* The map of the loaded bitstreams
10787
*/
108-
currentPage = 1;
88+
pageToBitstreamsMap: Map<number,ItemSearchResult[]> = new Map();
10989

11090
/**
111-
* Pages displayed in pagination controls
91+
* The page number that drives the bitstreams preload
11292
*/
113-
currentlyVisiblePages$: BehaviorSubject<(string| number)[]> = new BehaviorSubject<(string | number)[]>(null);
93+
currentSliderPage = 1;
11494

11595
/**
11696
* Items contained in currently active page
11797
*/
118-
currentPageItems$: BehaviorSubject<ItemSearchResult[]> = new BehaviorSubject<ItemSearchResult[]>([]);
119-
120-
/**
121-
* Number of pages to be shown in the pagination bar (boundaries excluded)
122-
* @private
123-
*/
98+
carouselItems$: BehaviorSubject<ItemSearchResult[]> = new BehaviorSubject<ItemSearchResult[]>([]);
12499

125-
private pagesToVisualize = 10;
126100

127101
private paginationOptionId: string;
128102

103+
private pageSize = 5;
104+
105+
private slideLoadingBuffer = 2;
106+
129107

130108

131109
constructor(
@@ -144,21 +122,18 @@ export class CarouselComponent implements OnInit {
144122
this.retrieveItems().pipe(
145123
mergeMap((searchResult: SearchObjects<Item>) => {
146124
if (isNotEmpty(searchResult)) {
147-
this.totalPages = searchResult.totalPages;
148-
this.totalItems = searchResult.totalElements;
149125
const items = searchResult.page;
150-
this.itemMap.set(searchResult.currentPage, items);
126+
this.carouselItems$.next(items);
151127
this.isLoading$.next(true);
152-
return this.findAllBitstreamImages(items);
128+
129+
return this.findAllBitstreamImages(items.filter((_,i) => i <= this.pageSize - 1));
153130
} else {
154131
return null;
155132
}
156133
}),
157134
take(1)
158135
).subscribe((res) => {
159136
this.itemToImageHrefMap$.next(res);
160-
this.currentlyVisiblePages$.next(this.getPagesToVisualize());
161-
this.currentPageItems$.next(this.getCurrentPageItems());
162137
this.isLoading$.next(false);
163138
});
164139
}
@@ -188,29 +163,20 @@ export class CarouselComponent implements OnInit {
188163
this.togglePaused();
189164
}
190165

191-
if (this.changePageOnSlide) {
192-
const previousSlideIndex = parseInt(slideEvent.prev.split('-')[2], 10);
193-
const currentSlideIndex = parseInt(slideEvent.current.split('-')[2], 10);
194-
const direction = slideEvent.direction;
195-
const pageSlidesBounds = {
196-
min: (this.carouselOptions.numberOfItems * this.currentPage) - this.carouselOptions.numberOfItems,
197-
max: (this.carouselOptions.numberOfItems * this.currentPage)
198-
};
199-
const isPreviousIndexInCurrentPage = (previousSlideIndex + 1 >= pageSlidesBounds.min) && previousSlideIndex + 1 <= pageSlidesBounds.max;
200-
201-
if (currentSlideIndex < previousSlideIndex && !(this.currentPage === this.totalPages) && direction === 'left' && isPreviousIndexInCurrentPage) {
202-
this.changePage(this.currentPage + 1);
203-
} else if (previousSlideIndex < currentSlideIndex && direction === 'right' && this.currentPage !== 1 && isPreviousIndexInCurrentPage) {
204-
this.changePage(this.currentPage - 1);
205-
}
206-
}
166+
const currentSlideIndex = parseInt(slideEvent.current.split('-')[2], 10);
167+
const currentPage = Math.ceil(currentSlideIndex / this.pageSize);
207168

169+
if (!this.pageToBitstreamsMap.get(currentPage + 1) && currentSlideIndex + this.slideLoadingBuffer === currentPage * this.pageSize) {
170+
this.loadNextPageBitstreams();
171+
}
208172
}
209173

210174
/**
211175
* Find the first image of each item
212176
*/
213177
findAllBitstreamImages(items: ItemSearchResult[]): Observable<Map<string, string>> {
178+
this.pageToBitstreamsMap.set(this.currentSliderPage, items);
179+
214180
return from(items).pipe(
215181
map((itemSR) => itemSR.indexableObject),
216182
mergeMap((item) => this.bitstreamDataService.showableByItem(
@@ -244,20 +210,18 @@ export class CarouselComponent implements OnInit {
244210
/**
245211
* Retrieve items by the given page number
246212
*
247-
* @param currentPage
248213
*/
249-
retrieveItems(currentPage: number = 1): Observable<SearchObjects<Item>> {
214+
retrieveItems(): Observable<SearchObjects<Item>> {
250215
const pagination: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
251216
id: this.paginationOptionId,
252217
pageSize: this.carouselOptions.numberOfItems,
253-
currentPage: currentPage
218+
currentPage: 1
254219
});
255220

256221
const paginatedSearchOptions = new PaginatedSearchOptions({
257222
configuration: this.carouselOptions.discoveryConfiguration,
258223
pagination: pagination,
259224
sort: new SortOptions(this.carouselOptions.sortField, this.carouselOptions.sortDirection),
260-
dsoTypes: [DSpaceObjectType.ITEM],
261225
projection: 'preventMetadataSecurity'
262226
});
263227
return this.searchManager.search(paginatedSearchOptions).pipe(
@@ -272,93 +236,29 @@ export class CarouselComponent implements OnInit {
272236
);
273237
}
274238

275-
getCurrentPageItems(): ItemSearchResult[] {
276-
return this.itemMap.get(this.currentPage);
277-
}
278239

279240
pages = () => {
280-
return Array.from({length: this.totalPages }, (_, i) => i + 1);
281-
};
282-
283-
/**
284-
* return pages in scope for loading
285-
*/
286-
getPagesToVisualize(): (string | number)[] {
287-
const pagesArray = this.pages();
288-
let currentPagesInScope: (string | number)[] = this.currentPage > this.pagesToVisualize ?
289-
(
290-
this.currentPage - 1 + this.pagesToVisualize > this.totalPages ?
291-
[...pagesArray.slice(this.currentPage - this.pagesToVisualize, this.currentPage + this.pagesToVisualize)] :
292-
[...pagesArray.slice(this.currentPage - 1, this.currentPage + this.pagesToVisualize)]
293-
) :
294-
[...pagesArray.slice(0, this.pagesToVisualize)];
295-
296-
if (currentPagesInScope.some(page => page > this.pagesToVisualize)) {
297-
currentPagesInScope = [1, '...', ...currentPagesInScope];
298-
currentPagesInScope = currentPagesInScope.includes(this.totalPages) ? currentPagesInScope : [...currentPagesInScope, '...', this.totalPages];
299-
} else {
300-
currentPagesInScope = [1, ...currentPagesInScope.filter(page => page !== 1), '...', this.totalPages];
301-
}
302-
303-
return currentPagesInScope;
304-
}
305-
previousPage = () => {
306-
if (this.currentPage > 1) {
307-
this.currentPage--;
308-
309-
if (!this.itemMap.get(this.currentPage)) {
310-
this.isLoading$.next(true);
311-
this.retrieveMoreItems(this.currentPage);
312-
}
313-
}
241+
return Array.from({length: this.carouselOptions.numberOfItems / this.pageSize }, (_, i) => i + 1);
314242
};
315243

316-
nextPage = () => {
317-
if (this.currentPage < this.totalPages) {
318-
this.currentPage++;
319-
320-
if (!this.itemMap.get(this.currentPage)) {
321-
this.isLoading$.next(true);
322-
this.retrieveMoreItems(this.currentPage);
323-
}
324-
}
325-
};
326244

327-
changePage = (page: number) => {
328-
this.currentPage = page;
245+
private loadNextPageBitstreams(): void {
246+
const items = this.carouselItems$.value;
247+
const itemsWithLoadedImages = [].concat((Array.from({length: this.currentSliderPage}, (_, i) => i + 1).map(page => this.pageToBitstreamsMap.get(page))));
248+
const itemsWithoutBistreamsInNextPage = difference(items, itemsWithLoadedImages).filter(item => (items.indexOf(item) > itemsWithLoadedImages.length - 1) && items.indexOf(item) < (this.currentSliderPage + 1) * this.pageSize);
329249

330-
if (!this.itemMap.get(this.currentPage)) {
331-
this.isLoading$.next(true);
332-
this.retrieveMoreItems(this.currentPage);
333-
}
334-
};
335-
336-
retrieveMoreItems(page: number) {
337-
of(page).pipe(
338-
concatMap((currentPage: number) => this.retrieveItems(currentPage).pipe(
339-
mergeMap((searchResult: SearchObjects<Item>) => {
340-
if (isNotEmpty(searchResult)) {
341-
const items = searchResult.page;
342-
this.itemMap.set(searchResult.currentPage, items);
343-
344-
return this.findAllBitstreamImages(items);
345-
} else {
346-
return of(null);
347-
}
348-
}),
349-
take(1),
350-
)),
250+
this.findAllBitstreamImages(itemsWithoutBistreamsInNextPage).pipe(
251+
take(1),
351252
reduce((itemToImageHrefMap, value) => {
352253
return new Map([...Array.from(itemToImageHrefMap.entries()), ...Array.from(value.entries())]);
353254
}, new Map()),
354-
).subscribe((itemToImageHrefMap: Map<string,string>) => {
255+
).subscribe(((itemToImageHrefMap: Map<string,string>) => {
256+
this.currentSliderPage += 1;
355257
if (isNotEmpty(itemToImageHrefMap)) {
356258
this.itemToImageHrefMap$.next(new Map([...Array.from(this.itemToImageHrefMap$.value.entries()), ...Array.from(itemToImageHrefMap.entries())]));
357259
}
358-
this.currentlyVisiblePages$.next(this.getPagesToVisualize());
359-
this.currentPageItems$.next(this.getCurrentPageItems());
360-
this.isLoading$.next(false);
361-
});
260+
}));
261+
362262
}
363263

364264
}

src/app/shared/explore/section-component/carousel-section/carousel-section.component.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,7 @@ export class CarouselSectionComponent implements OnInit {
6363
order: this.carouselSection.order,
6464
sortField: this.carouselSection.sortField ?? this.DEFAULT_SORT_FIELD,
6565
sortDirection: this.carouselSection.order && this.carouselSection.order.toUpperCase() === 'ASC' ? SortDirection.ASC : SortDirection.DESC,
66-
//current carousel config has 0 as number of items for the pagination so would just load all items
67-
//TODO: Adjust numberOfItems on rest for desired pagination
68-
numberOfItems: this.carouselSection.numberOfItems && this.carouselSection.numberOfItems > 0 ? this.carouselSection.numberOfItems : 5
66+
numberOfItems: this.carouselSection.numberOfItems && this.carouselSection.numberOfItems > 0 ? this.carouselSection.numberOfItems : 20
6967
};
7068
}
7169

0 commit comments

Comments
 (0)