Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/app/core/pagination/pagination-component-options.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,14 @@ export class PaginationComponentOptions extends NgbPaginationConfig {
*/
pageSize: number;

/**
* Show boundary links.
*/
boundaryLinks = false;

/**
* Show an input field to enter the page number directly.
*/
enablePaginationInput? = true;

}
51 changes: 48 additions & 3 deletions src/app/shared/pagination/pagination.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,18 +54,63 @@
@if (showBottomPager$ | async) {
<div>
@if (showPaginator) {
<div class="pagination justify-content-center clearfix bottom">
<div class="pagination justify-content-center clearfix bottom flex-wrap gapy-1">
<ngb-pagination [attr.aria-label]="('pagination-control.page-number-bar' | translate) + paginationOptions.id"
[boundaryLinks]="paginationOptions.boundaryLinks"
[collectionSize]="collectionSize"
[disabled]="paginationOptions.disabled"
[ellipses]="paginationOptions.ellipses"
[maxSize]="(isXs)?5:paginationOptions.maxSize"
[maxSize]="isXs ? (paginationOptions.maxSize/2) : paginationOptions.maxSize"
[page]="(currentPage$|async)"
(pageChange)="doPageChange($event)"
[pageSize]="(pageSize$ |async)"
[rotate]="paginationOptions.rotate"
[size]="(isXs)?'sm':paginationOptions.size">
[size]="isXs ? 'sm' : paginationOptions.size">
@if (paginationOptions.enablePaginationInput) {
<ng-template ngbPaginationPages let-page let-pages="pages">
@for (p of pages; track p) {
<li class="page-item" [ngClass]="{disabled: !paginationOptions.enablePaginationInput && p === -1, active: p === page}" [attr.data-test]="'page-' + p">
@if (p === -1) {
<button class="page-link"
[attr.data-test]="'page-input-button'"
[dsBtnDisabled]="!paginationOptions.enablePaginationInput"
[ngbPopover]="paginationInputPopover"
[autoClose]="'outside'"
#pop="ngbPopover"
>
...
</button>
<ng-template #paginationInputPopover>
<span class="px-1 input-group flex-nowrap h-100">
<input
#input
type="text"
inputmode="numeric"
pattern="[0-9]*"
class="form-control border-info page-input"
id="paginationInput"
[placeholder]="'pagination.page' | translate"
(keyup.enter)="selectPage(input.value, pop)"
(input)="formatInput($any($event).target)"
aria-label="Page input"
/>
<button class="btn btn-outline-info search-button" type="button" (click)="selectPage(input.value, pop)" tabindex="0" [attr.aria-label]="'paginator.go-to-page' | translate">
{{'pagination.go-to-page' | translate}}
</button>
</span>
</ng-template>
} @else if (page !== p) {
<button class="page-link"
[dsBtnDisabled]="p === page"
[attr.aria-label]="'pagination-control.page-number-bar' | translate: {page: p}"
(click)="selectPage(p)">{{p}}</button>
} @else {
<a class="page-link" aria-current="page">{{p}}</a>
}
</li>
}
</ng-template>
}
</ngb-pagination>
</div>
}
Expand Down
35 changes: 30 additions & 5 deletions src/app/shared/pagination/pagination.component.scss
Original file line number Diff line number Diff line change
@@ -1,8 +1,33 @@
:host {
.dropdown-toggle::after {
display: none;
}
.dropdown-item {
padding-left: 20px;
.dropdown-toggle::after {
display: none;
}

.dropdown-item {
padding-left: 20px;
}

.search-button {
border-left: 0;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}

div.input-group {
@include media-breakpoint-down(xs) {
input {
padding: 0.34rem 0.5rem 0.28rem;
font-size: 0.875rem;
height: 100%;
}
button.search-button {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
}
}
}

.page-input {
width: 4rem;
}
}
136 changes: 130 additions & 6 deletions src/app/shared/pagination/pagination.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { FindListOptions } from '@dspace/core/data/find-list-options.model';
import { PaginationService } from '@dspace/core/pagination/pagination.service';
import { PaginationComponentOptions } from '@dspace/core/pagination/pagination-component-options.model';
import { MockActivatedRoute } from '@dspace/core/testing/active-router.mock';
import { HostWindowServiceMock } from '@dspace/core/testing/host-window-service.mock';
import { HostWindowServiceStub } from '@dspace/core/testing/host-window-service.stub';
import { RouterMock } from '@dspace/core/testing/router.mock';
import { TranslateLoaderMock } from '@dspace/core/testing/translate-loader.mock';
import { createTestComponent } from '@dspace/core/testing/utils.test';
Expand Down Expand Up @@ -66,14 +66,14 @@ function expectPages(fixture: ComponentFixture<any>, pagesDef: string[]): void {
expect(pages[i].classList.contains('disabled')).toBeTruthy();
expect(normalizeText(pages[i].textContent)).toEqual(normalizeText(pageDef));
if (normalizeText(pages[i].textContent) !== '...') {
expect(pages[i].querySelector('a').getAttribute('tabindex')).toEqual('-1');
expect((pages[i].querySelector('a') || pages[i].querySelector('button')).getAttribute('tabindex')).toEqual('-1');
}
} else {
expect(pages[i].classList.contains('active')).toBeFalsy();
expect(pages[i].classList.contains('disabled')).toBeFalsy();
expect(normalizeText(pages[i].textContent)).toEqual(normalizeText(pageDef));
if (normalizeText(pages[i].textContent) !== '...') {
expect(pages[i].querySelector('a').hasAttribute('tabindex')).toBeFalsy();
expect((pages[i].querySelector('a') || pages[i].querySelector('button')).hasAttribute('tabindex')).toBeFalsy();
}
}
}
Expand All @@ -100,7 +100,10 @@ function changePage(fixture: ComponentFixture<any>, idx: number): void {
const de = fixture.debugElement.query(By.css('.pagination'));
const buttons = de.nativeElement.querySelectorAll('li');

buttons[idx].querySelector('a').click();
const clickableElement = buttons[idx].querySelector('a') || buttons[idx].querySelector('button');
if (clickableElement) {
clickableElement.click();
}
fixture.detectChanges();
}

Expand All @@ -115,7 +118,7 @@ describe('Pagination component', () => {
let testFixture: ComponentFixture<TestComponent>;
let de: DebugElement;
let html;
let hostWindowServiceStub: HostWindowServiceMock;
let hostWindowServiceStub: HostWindowServiceStub;

let activatedRouteStub: MockActivatedRoute;
let routerStub: RouterMock;
Expand All @@ -128,6 +131,7 @@ describe('Pagination component', () => {
const pagination = new PaginationComponentOptions();
pagination.currentPage = 1;
pagination.pageSize = 10;
pagination.maxSize = 10;

const sort = new SortOptions('score', SortDirection.DESC);
const findlistOptions = Object.assign(new FindListOptions(), { currentPage: 1, elementsPerPage: 10 });
Expand All @@ -139,7 +143,7 @@ describe('Pagination component', () => {
beforeEach(waitForAsync(() => {
activatedRouteStub = new MockActivatedRoute();
routerStub = new RouterMock();
hostWindowServiceStub = new HostWindowServiceMock(_initialState.width);
hostWindowServiceStub = new HostWindowServiceStub(_initialState.width);

currentPagination = new BehaviorSubject<PaginationComponentOptions>(pagination);
currentSort = new BehaviorSubject<SortOptions>(sort);
Expand Down Expand Up @@ -209,6 +213,10 @@ describe('Pagination component', () => {
</ds-pagination>`;
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
testComp = testFixture.componentInstance;
testComp.paginationOptions.enablePaginationInput = false;
testComp.paginationOptions.maxSize = 10;

testFixture.detectChanges();
});

it('should create Pagination Component', inject([PaginationComponent], (app: PaginationComponent) => {
Expand Down Expand Up @@ -283,11 +291,15 @@ describe('Pagination component', () => {

it('should call the updateRoute method on the paginationService with the correct params', fakeAsync(() => {
testComp.collectionSize = 60;
testFixture.detectChanges();

changePage(testFixture, 3);
tick();
expect(paginationService.updateRoute).toHaveBeenCalledWith('test', Object.assign({ page: 3 }), {}, false);

currentPagination.next(Object.assign(new PaginationComponentOptions(), pagination, { currentPage: 3 }));
testFixture.detectChanges();

changePage(testFixture, 0);
tick();
expect(paginationService.updateRoute).toHaveBeenCalledWith('test', Object.assign({ page: 2 }), {}, false);
Expand Down Expand Up @@ -416,6 +428,118 @@ describe('Pagination component', () => {
});
});

describe('Pagination input field', () => {
let fixture: ComponentFixture<PaginationComponent>;
let component: PaginationComponent;

beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [
CommonModule,
NgbModule,
PaginationComponent,
EnumKeysPipe,
RouterTestingModule,
TranslateModule.forRoot({
loader: { provide: TranslateLoader, useClass: TranslateLoaderMock },
}),
StoreModule.forRoot({}, {}),
],
providers: [
{ provide: HostWindowService, useValue: hostWindowServiceStub },
{ provide: PaginationService, useValue: {
getCurrentPagination: () => new BehaviorSubject({ currentPage: 5, pageSize: 10 }),
getCurrentSort: () => new BehaviorSubject({ direction: SortDirection.ASC, field: 'name' }),
updateRoute: () => {
//
},
} },
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
}).compileComponents();
}));

beforeEach(() => {
fixture = TestBed.createComponent(PaginationComponent);
component = fixture.componentInstance;
component.collectionSize = 200;
component.paginationOptions = {
id: 'test',
currentPage: 5,
pageSize: 10,
pageSizeOptions: [10, 20, 50],
directionLinks: true,
boundaryLinks: true,
ellipses: true,
maxSize: 10,
rotate: false,
size: 'lg',
disabled: false,
enablePaginationInput: true,
};
fixture.detectChanges();
});

it('should enable click on "..." button', () => {
fixture.detectChanges();
const input = fixture.debugElement.query(By.css('[data-test="page-input-button"]'));
expect(input).toBeDefined();
expect(input.nativeElement.disabled).toBeFalse();
});

describe('selectPage', () => {
// collectionSize (200) / pageSize (10) => 20 pages
let paginationServiceStub: any;

beforeEach(() => {
paginationServiceStub = TestBed.inject(PaginationService);
spyOn(paginationServiceStub, 'updateRoute').and.callThrough();
spyOn(component.pageChange, 'emit');
spyOn(component.paginationChange, 'emit');
});

it('should navigate to the requested page when it is within range', () => {
component.selectPage('7');
expect(paginationServiceStub.updateRoute).toHaveBeenCalledWith('test', { page: 7 }, {}, false);
expect(component.pageChange.emit).toHaveBeenCalledWith(7);
});

it('should clamp a page number that is too high to the last page', () => {
component.selectPage('999');
expect(paginationServiceStub.updateRoute).toHaveBeenCalledWith('test', { page: 20 }, {}, false);
expect(component.pageChange.emit).toHaveBeenCalledWith(20);
});

it('should clamp a page number below 1 to the first page', () => {
component.selectPage('0');
expect(paginationServiceStub.updateRoute).toHaveBeenCalledWith('test', { page: 1 }, {}, false);
expect(component.pageChange.emit).toHaveBeenCalledWith(1);
});

it('should ignore non-numeric input', () => {
component.selectPage('abc');
expect(paginationServiceStub.updateRoute).not.toHaveBeenCalled();
expect(component.pageChange.emit).not.toHaveBeenCalled();
});

it('should emit a paginationChange event so the results are reloaded', () => {
component.selectPage('3');
expect(component.paginationChange.emit).toHaveBeenCalled();
});

it('should not emit a paginationChange event for invalid input', () => {
component.selectPage('abc');
expect(component.paginationChange.emit).not.toHaveBeenCalled();
});

it('should close the popover when one is provided', () => {
const popover = jasmine.createSpyObj('NgbPopover', ['close']);
component.selectPage('3', popover);
expect(popover.close).toHaveBeenCalled();
});
});
});

});

// declare a test component
Expand Down
Loading
Loading