Skip to content

Commit 6460332

Browse files
FrancescoMolinaroatarix83
authored andcommitted
Merged in task/ux-plus-2023_02_x/UXP-34-rework (pull request #23)
[UXP-34] rework carousel pagination Approved-by: Giuseppe Digilio
2 parents a50dece + a5dd98c commit 6460332

5 files changed

Lines changed: 80 additions & 145 deletions

File tree

Lines changed: 15 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
<ngb-carousel #carousel [interval]="2000" (slide)="onSlide($event)" class="ds-carousel">
2-
<ng-template ngbSlide *ngFor="let item of currentPageItems(); let i = index; let last = last">
1+
<ngb-carousel #carousel [interval]="2000" (slide)="onSlide($event)" class="ds-carousel" *ngIf="!(isLoading$ | async)">
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]">
@@ -33,28 +33,6 @@
3333
</ng-template>
3434
</ng-template>
3535
</ngb-carousel>
36-
<div class="text-center play-pause-button">
37-
<button type="button" class="btn btn-sm toggle-paused" (click)="togglePaused()">
38-
<i class="fas fa-play" *ngIf="paused"></i>
39-
<i class="fas fa-pause" *ngIf="!paused"></i>
40-
</button>
41-
</div>
42-
<div class="mt-4 w-100 d-flex justify-content-center align-items-center" *ngIf="totalPages > 1">
43-
<button (click)="previousPage()" [disabled]="currentPage === 1" class="prev border-0 bg-transparent">
44-
<i class="fa fa-arrow-left"></i>
45-
</button>
46-
<button
47-
*ngFor="let page of pages()"
48-
class="number"
49-
(click)="changePage(page)"
50-
style="border: none; background: none; margin: 0 5px"
51-
[style.color]="page === currentPage ? '#000000' : '#7c7c7c'">
52-
{{ page < 10 ? '0' + page : page }}
53-
</button>
54-
<button (click)="nextPage()" [disabled]="currentPage === pages().length" class="next border-0 bg-transparent">
55-
<i class="fa fa-arrow-right"></i>
56-
</button>
57-
</div>
5836

5937
<div
6038
class="carousel-content-wrapper"
@@ -64,13 +42,20 @@
6442
}"
6543
*ngIf="(isLoading$ | async)"
6644
>
67-
<a
68-
href="#"
69-
target="_blank"
70-
class="img-container-el">
45+
<div class="h-100 img-container-el">
7146
<div class="picsum-img-wrapper flex-column">
7247
<img class="img-fluid" src="assets/images/replacement_image.svg">
7348
{{'loading.default' | translate}}
7449
</div>
75-
</a>
50+
</div>
7651
</div>
52+
53+
<div class="text-center play-pause-button">
54+
<button [disabled]="isLoading$ | async" type="button" class="btn btn-sm toggle-paused" (click)="togglePaused()">
55+
<i class="fas fa-play" *ngIf="paused"></i>
56+
<i class="fas fa-pause" *ngIf="!paused"></i>
57+
</button>
58+
</div>
59+
60+
61+

src/app/shared/carousel/carousel.component.scss

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,4 +91,14 @@
9191
box-shadow: none !important
9292
}
9393

94+
.next {
95+
border-top-right-radius: 0.25rem;
96+
border-bottom-right-radius: 0.25rem;
97+
}
98+
99+
.prev {
100+
border-top-left-radius: 0.25rem;
101+
border-bottom-left-radius: 0.25rem;
102+
}
103+
94104
}

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: 49 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
import {Component, Inject, Input, OnInit, ViewChild} from '@angular/core';
1+
import {Component, Input, OnInit, ViewChild} from '@angular/core';
22
import {NgbCarousel, NgbSlideEvent, NgbSlideEventSource} from '@ng-bootstrap/ng-bootstrap';
3-
import { BehaviorSubject, concatMap, from, Observable } 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';
77
import {Bitstream} from '../../core/shared/bitstream.model';
88
import {BitstreamDataService} from '../../core/data/bitstream-data.service';
9-
import {NativeWindowRef, NativeWindowService} from '../../core/services/window.service';
109
import {getFirstCompletedRemoteData} from '../../core/shared/operators';
1110
import { hasValue, isNotEmpty } from '../empty.util';
1211
import {ItemSearchResult} from '../object-collection/shared/item-search-result.model';
@@ -19,8 +18,8 @@ import { SearchObjects } from '../search/models/search-objects.model';
1918
import { SortOptions } from '../../core/cache/models/sort-options.model';
2019
import { PaginationComponentOptions } from '../pagination/pagination-component-options.model';
2120
import { PaginatedSearchOptions } from '../search/models/paginated-search-options.model';
22-
import { DSpaceObjectType } from '../../core/shared/dspace-object-type.model';
2321
import { InternalLinkService } from '../../core/services/internal-link.service';
22+
import difference from 'lodash/difference';
2423

2524
/**
2625
* Component representing the Carousel component section.
@@ -84,56 +83,49 @@ export class CarouselComponent implements OnInit {
8483
isLoading$ = new BehaviorSubject(true);
8584

8685
/**
87-
* The total search item pages
86+
* The map of the loaded bitstreams
8887
*/
89-
totalPages = 0;
90-
/**
91-
* the total number of item available
92-
*/
93-
totalItems = 0;
88+
pageToBitstreamsMap: Map<number,ItemSearchResult[]> = new Map();
9489

9590
/**
96-
* The list of the item to show
91+
* The page number that drives the bitstreams preload
9792
*/
98-
itemList: ItemSearchResult[] = [];
93+
currentSliderPage = 1;
9994

10095
/**
101-
* A boolean representing if there are more items to be loaded
102-
*/
103-
hasMoreToLoad: boolean;
104-
/**
105-
* The page number currently visualized
96+
* Items contained in currently active page
10697
*/
107-
currentPage = 1;
98+
carouselItems$: BehaviorSubject<ItemSearchResult[]> = new BehaviorSubject<ItemSearchResult[]>([]);
10899

109-
itemPlaceholderList: number[];
100+
private paginationOptionId: string;
101+
102+
private pageSize = 5;
103+
104+
private slideLoadingBuffer = 2;
110105

111106

112107

113108
constructor(
114109
protected bitstreamDataService: BitstreamDataService,
115110
private searchManager: SearchManager,
116111
public internalLinkService: InternalLinkService,
117-
@Inject(NativeWindowService) private _window: NativeWindowRef,
118-
) {
119-
}
112+
) {}
120113

121114
ngOnInit() {
122115
this.title = this.carouselOptions.title;
123116
this.link = this.carouselOptions.link;
124117
this.description = this.carouselOptions.description;
125118
this.bundle = this.carouselOptions.bundle ?? 'ORIGINAL';
119+
this.paginationOptionId = 'carousel-search-' + this.carouselOptions.discoveryConfiguration;
120+
126121
this.retrieveItems().pipe(
127122
mergeMap((searchResult: SearchObjects<Item>) => {
128123
if (isNotEmpty(searchResult)) {
129-
this.totalPages = searchResult.totalPages;
130-
this.totalItems = searchResult.totalElements;
131-
this.itemPlaceholderList = Array(searchResult.totalElements).fill(1).map((x, i) => i + 1);
132124
const items = searchResult.page;
133-
this.itemList = [...this.itemList, ...items];
134-
this.hasMoreToLoad = this.itemList.length < searchResult.totalElements;
125+
this.carouselItems$.next(items);
135126
this.isLoading$.next(true);
136-
return this.findAllBitstreamImages(items);
127+
128+
return this.findAllBitstreamImages(items.filter((_,i) => i <= this.pageSize - 1));
137129
} else {
138130
return null;
139131
}
@@ -161,36 +153,37 @@ export class CarouselComponent implements OnInit {
161153
* function to call on slide
162154
*/
163155
onSlide(slideEvent: NgbSlideEvent) {
164-
const previousSlideIndex = parseInt(slideEvent.prev.split(('_'))[1], 10);
165-
const direction = slideEvent.direction;
166-
167156
if (this.unpauseOnArrow && slideEvent.paused &&
168157
(slideEvent.source === NgbSlideEventSource.ARROW_LEFT || slideEvent.source === NgbSlideEventSource.ARROW_RIGHT)) {
169158
this.togglePaused();
170159
}
160+
171161
if (this.pauseOnIndicator && !slideEvent.paused && slideEvent.source === NgbSlideEventSource.INDICATOR) {
172162
this.togglePaused();
173163
}
174164

175-
if (previousSlideIndex === (this.carouselOptions.numberOfItems - 1) && direction === 'left' && (this.hasMoreToLoad || this.currentPage < this.totalPages)) {
176-
this.changePage(this.currentPage + 1);
177-
} else if (previousSlideIndex === 0 && direction === 'right' && this.currentPage !== 1) {
178-
this.changePage(this.currentPage - 1);
179-
} else if (previousSlideIndex === 0 && direction === 'right' && this.currentPage === 1) {
180-
this.changePage(this.totalPages);
181-
} else if (previousSlideIndex === (this.currentPageItems().length - 1) && direction === 'left' && (!this.hasMoreToLoad || this.currentPage === this.totalPages)) {
182-
this.changePage(1);
165+
const currentSlideIndex = parseInt(slideEvent.current.split('-')[2], 10);
166+
const currentPage = Math.ceil(currentSlideIndex / this.pageSize);
167+
168+
if (!this.pageToBitstreamsMap.get(currentPage + 1) && currentSlideIndex + this.slideLoadingBuffer === currentPage * this.pageSize) {
169+
this.loadNextPageBitstreams();
170+
} else if (slideEvent.source === 'indicator' && currentSlideIndex > this.pageSize * this.currentSliderPage) {
171+
this.isLoading$.next(true);
172+
this.currentSliderPage = currentPage;
173+
this.loadNextPageBitstreams();
183174
}
184175
}
185176

186177
/**
187178
* Find the first image of each item
188179
*/
189180
findAllBitstreamImages(items: ItemSearchResult[]): Observable<Map<string, string>> {
181+
this.pageToBitstreamsMap.set(this.currentSliderPage, items);
182+
190183
return from(items).pipe(
191184
map((itemSR) => itemSR.indexableObject),
192-
mergeMap((item) => this.bitstreamDataService.findAllByItemAndBundleName(
193-
item, this.bundle, {}, true, true, followLink('format'),
185+
mergeMap((item) => this.bitstreamDataService.showableByItem(
186+
item.uuid, this.bundle, [], {}, true, true, followLink('format'),
194187
).pipe(
195188
getFirstCompletedRemoteData(),
196189
switchMap((rd: RemoteData<PaginatedList<Bitstream>>) => rd.hasSucceeded ? rd.payload.page : []),
@@ -217,34 +210,22 @@ export class CarouselComponent implements OnInit {
217210
return item.firstMetadataValue(this.link);
218211
}
219212

220-
221-
/**
222-
* to open a link of an item
223-
*/
224-
openLinkUrl(url) {
225-
if (url && url[0].value) {
226-
this._window.nativeWindow.open(url[0].value, '_blank');
227-
}
228-
}
229-
230213
/**
231214
* Retrieve items by the given page number
232215
*
233-
* @param currentPage
234216
*/
235-
retrieveItems(currentPage: number = 1): Observable<SearchObjects<Item>> {
217+
retrieveItems(): Observable<SearchObjects<Item>> {
236218
const pagination: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
237-
id: 'sop',
219+
id: this.paginationOptionId,
238220
pageSize: this.carouselOptions.numberOfItems,
239-
currentPage: currentPage
221+
currentPage: 1
240222
});
241223

242224
const paginatedSearchOptions = new PaginatedSearchOptions({
243225
configuration: this.carouselOptions.discoveryConfiguration,
244226
pagination: pagination,
245227
sort: new SortOptions(this.carouselOptions.sortField, this.carouselOptions.sortDirection),
246-
dsoTypes: [DSpaceObjectType.ITEM],
247-
forcedEmbeddedKeys: ['bundles']
228+
projection: 'preventMetadataSecurity'
248229
});
249230
return this.searchManager.search(paginatedSearchOptions).pipe(
250231
getFirstCompletedRemoteData(),
@@ -258,69 +239,30 @@ export class CarouselComponent implements OnInit {
258239
);
259240
}
260241

261-
currentPageItems(): ItemSearchResult[] {
262-
return this.itemList.slice((this.currentPage - 1) * this.carouselOptions.numberOfItems, this.currentPage * this.carouselOptions.numberOfItems);
263-
}
264242

265243
pages = () => {
266-
return Array.from({length: Math.ceil(this.itemPlaceholderList.length / this.carouselOptions.numberOfItems)}, (_, i) => i + 1);
267-
};
268-
previousPage = () => {
269-
if (this.currentPage > 1) {
270-
this.currentPage--;
271-
}
244+
return Array.from({length: this.carouselOptions.numberOfItems / this.pageSize }, (_, i) => i + 1);
272245
};
273246

274-
nextPage = () => {
275-
if (this.currentPage < this.pages().length) {
276-
if (this.hasMoreToLoad) {
277-
this.isLoading$.next(true);
278-
this.currentPage++;
279-
this.retrieveMoreItems(this.currentPage);
280-
} else {
281-
this.currentPage++;
282-
}
283-
}
284-
};
285247

286-
changePage = (page) => {
287-
if (page > this.currentPage && this.hasMoreToLoad) {
288-
this.isLoading$.next(true);
289-
const startIndex = this.pages().indexOf(this.currentPage) + 1;
290-
const endIndex = this.pages().indexOf(page) + 1;
291-
const pagesToFetch: number[] = this.pages().slice(startIndex, endIndex);
292-
this.currentPage = page;
293-
this.retrieveMoreItems(...pagesToFetch);
294-
} else {
295-
this.currentPage = page;
296-
}
297-
};
248+
private loadNextPageBitstreams(): void {
249+
const items = this.carouselItems$.value;
250+
const itemsWithLoadedImages = [].concat((Array.from({length: this.currentSliderPage}, (_, i) => i + 1).map(page => this.pageToBitstreamsMap.get(page))));
251+
const itemsWithoutBistreamsInNextPage = difference(items, itemsWithLoadedImages).filter(item => (items.indexOf(item) > itemsWithLoadedImages.length - 1) && items.indexOf(item) < (this.currentSliderPage + 1) * this.pageSize);
298252

299-
retrieveMoreItems(...page: number[]) {
300-
from(page).pipe(
301-
concatMap((currentPage: number) => this.retrieveItems(currentPage).pipe(
302-
mergeMap((searchResult: SearchObjects<Item>) => {
303-
if (isNotEmpty(searchResult)) {
304-
const items = searchResult.page;
305-
this.itemList = [...this.itemList, ...items];
306-
this.hasMoreToLoad = this.itemList.length < searchResult.totalElements;
307-
return this.findAllBitstreamImages(items);
308-
} else {
309-
return null;
310-
}
311-
}),
312-
take(1),
313-
// tap((itemToImageHrefMap) => this.itemToImageHrefMap$.next(new Map([...Array.from(this.itemToImageHrefMap$.value.entries()), ...Array.from(itemToImageHrefMap.entries())]))),
314-
)),
253+
this.findAllBitstreamImages(itemsWithoutBistreamsInNextPage).pipe(
254+
take(1),
315255
reduce((itemToImageHrefMap, value) => {
316256
return new Map([...Array.from(itemToImageHrefMap.entries()), ...Array.from(value.entries())]);
317257
}, new Map()),
318-
).subscribe((itemToImageHrefMap: Map<string,string>) => {
258+
).subscribe(((itemToImageHrefMap: Map<string,string>) => {
259+
this.currentSliderPage += 1;
319260
if (isNotEmpty(itemToImageHrefMap)) {
320261
this.itemToImageHrefMap$.next(new Map([...Array.from(this.itemToImageHrefMap$.value.entries()), ...Array.from(itemToImageHrefMap.entries())]));
321262
}
322263
this.isLoading$.next(false);
323-
});
264+
}));
265+
324266
}
325267

326268
}

0 commit comments

Comments
 (0)