Skip to content

Commit 48ff9f9

Browse files
author
Andrea Barbasso
committed
[DURACOM-386] add custom input for page number
1 parent 841a46c commit 48ff9f9

File tree

4 files changed

+195
-14
lines changed

4 files changed

+195
-14
lines changed

src/app/shared/pagination/pagination.component.html

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,18 +54,53 @@
5454
@if (showBottomPager$ | async) {
5555
<div>
5656
@if (showPaginator) {
57-
<div class="pagination justify-content-center clearfix bottom">
57+
<div class="pagination justify-content-center clearfix bottom flex-wrap gapy-1">
5858
<ngb-pagination [attr.aria-label]="('pagination-control.page-number-bar' | translate) + paginationOptions.id"
5959
[boundaryLinks]="paginationOptions.boundaryLinks"
6060
[collectionSize]="collectionSize"
6161
[disabled]="paginationOptions.disabled"
6262
[ellipses]="paginationOptions.ellipses"
63-
[maxSize]="(isXs)?5:paginationOptions.maxSize"
63+
[maxSize]="(isXs)?(paginationOptions.maxSize/2):paginationOptions.maxSize"
6464
[page]="(currentPage$|async)"
6565
(pageChange)="doPageChange($event)"
6666
[pageSize]="(pageSize$ |async)"
67-
[rotate]="paginationOptions.rotate"
67+
[rotate]="enablePaginationInput || paginationOptions.rotate"
6868
[size]="(isXs)?'sm':paginationOptions.size">
69+
@if (enablePaginationInput) {
70+
<ng-template ngbPaginationPages let-page let-pages="pages">
71+
@for (p of pages; track p) {
72+
<li class="page-item" [ngClass]="{disabled: p === -1}" [attr.data-test]="'page-' + p">
73+
@if (p === -1) {
74+
<a class="page-link" disabled>...</a>
75+
} @else if (p !== -1 && p !== page) {
76+
<button class="page-link"
77+
[attr.aria-label]="'pagination-control.page-number-bar' | translate: {page: p}"
78+
(click)="selectPage(p)">{{p}}</button>
79+
} @else if (p === page) {
80+
<div class="px-1 input-group flex-nowrap h-100">
81+
<input
82+
#input
83+
type="text"
84+
inputmode="numeric"
85+
pattern="[0-9]*"
86+
class="form-control border-info page-input"
87+
id="paginationInput"
88+
[value]="page"
89+
(keyup.enter)="selectPage(input.value)"
90+
(blur)="selectPage(input.value)"
91+
(input)="formatInput($any($event).target)"
92+
aria-label="Page input"
93+
[ngStyle]="{'width': ((pages[pages.length - 1] + '').length + 3) + 'ch'}"
94+
/>
95+
<button class="btn btn-outline-info search-button" type="button" (click)="selectPage(input.value)" tabindex="0" aria-label="Go to page">
96+
<i class="fas fa-search"></i>
97+
</button>
98+
</div>
99+
}
100+
</li>
101+
}
102+
</ng-template>
103+
}
69104
</ngb-pagination>
70105
</div>
71106
}
Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,29 @@
11
:host {
2-
.dropdown-toggle::after {
3-
display: none;
4-
}
5-
.dropdown-item {
6-
padding-left: 20px;
2+
.dropdown-toggle::after {
3+
display: none;
4+
}
5+
6+
.dropdown-item {
7+
padding-left: 20px;
8+
}
9+
10+
.search-button {
11+
border-left: 0;
12+
border-top-left-radius: 0;
13+
border-bottom-left-radius: 0;
14+
}
15+
16+
div.input-group {
17+
@include media-breakpoint-down(xs) {
18+
input {
19+
padding: 0.34rem 0.5rem 0.28rem;
20+
font-size: 0.875rem;
21+
height: 100%;
22+
}
23+
button.search-button {
24+
padding: 0.25rem 0.5rem;
25+
font-size: 0.875rem;
26+
}
727
}
28+
}
829
}

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

Lines changed: 102 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,10 @@ import { FindListOptions } from '../../core/data/find-list-options.model';
3838
import { PaginationService } from '../../core/pagination/pagination.service';
3939
import { HostWindowService } from '../host-window.service';
4040
import { MockActivatedRoute } from '../mocks/active-router.mock';
41-
import { HostWindowServiceMock } from '../mocks/host-window-service.mock';
4241
import { RouterMock } from '../mocks/router.mock';
4342
import { TranslateLoaderMock } from '../mocks/translate-loader.mock';
4443
import { RSSComponent } from '../rss-feed/rss.component';
44+
import { HostWindowServiceStub } from '../testing/host-window-service.stub';
4545
import { createTestComponent } from '../testing/utils.test';
4646
import { EnumKeysPipe } from '../utils/enum-keys-pipe';
4747
import { PaginationComponent } from './pagination.component';
@@ -100,7 +100,10 @@ function changePage(fixture: ComponentFixture<any>, idx: number): void {
100100
const de = fixture.debugElement.query(By.css('.pagination'));
101101
const buttons = de.nativeElement.querySelectorAll('li');
102102

103-
buttons[idx].querySelector('a').click();
103+
const clickableElement = buttons[idx].querySelector('a') || buttons[idx].querySelector('button');
104+
if (clickableElement) {
105+
clickableElement.click();
106+
}
104107
fixture.detectChanges();
105108
}
106109

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

120123
let activatedRouteStub: MockActivatedRoute;
121124
let routerStub: RouterMock;
@@ -128,6 +131,7 @@ describe('Pagination component', () => {
128131
const pagination = new PaginationComponentOptions();
129132
pagination.currentPage = 1;
130133
pagination.pageSize = 10;
134+
pagination.maxSize = 10;
131135

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

144148
currentPagination = new BehaviorSubject<PaginationComponentOptions>(pagination);
145149
currentSort = new BehaviorSubject<SortOptions>(sort);
@@ -199,6 +203,7 @@ describe('Pagination component', () => {
199203
[paginationOptions]='paginationOptions'
200204
[sortOptions]='sortOptions'
201205
[collectionSize]='collectionSize'
206+
[enablePaginationInput]="false"
202207
(pageChange)='pageChanged($event)'
203208
(pageSizeChange)='pageSizeChanged($event)'
204209
>
@@ -209,6 +214,9 @@ describe('Pagination component', () => {
209214
</ds-pagination>`;
210215
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
211216
testComp = testFixture.componentInstance;
217+
testComp.paginationOptions.maxSize = 10;
218+
219+
testFixture.detectChanges();
212220
});
213221

214222
it('should create Pagination Component', inject([PaginationComponent], (app: PaginationComponent) => {
@@ -330,6 +338,7 @@ describe('Pagination component', () => {
330338
(pageChange)='pageChanged($event)'
331339
(pageSizeChange)='pageSizeChanged($event)'
332340
[showPaginator]='false'
341+
[enablePaginationInput]="false"
333342
[objects]='objects'
334343
(prev)="goPrev()"
335344
(next)="goNext()"
@@ -391,6 +400,95 @@ describe('Pagination component', () => {
391400
});
392401
});
393402

403+
describe('Pagination input field', () => {
404+
let fixture: ComponentFixture<PaginationComponent>;
405+
let component: PaginationComponent;
406+
407+
beforeEach(waitForAsync(() => {
408+
TestBed.configureTestingModule({
409+
imports: [
410+
CommonModule,
411+
NgbModule,
412+
PaginationComponent,
413+
EnumKeysPipe,
414+
RouterTestingModule,
415+
TranslateModule.forRoot({
416+
loader: { provide: TranslateLoader, useClass: TranslateLoaderMock },
417+
}),
418+
StoreModule.forRoot({}, {}),
419+
],
420+
providers: [
421+
{ provide: HostWindowService, useValue: hostWindowServiceStub },
422+
{ provide: PaginationService, useValue: {
423+
getCurrentPagination: () => new BehaviorSubject({ currentPage: 5, pageSize: 10 }),
424+
getCurrentSort: () => new BehaviorSubject({ direction: SortDirection.ASC, field: 'name' }),
425+
updateRoute: () => {
426+
//
427+
},
428+
} },
429+
],
430+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
431+
}).compileComponents();
432+
}));
433+
434+
beforeEach(() => {
435+
fixture = TestBed.createComponent(PaginationComponent);
436+
component = fixture.componentInstance;
437+
component.enablePaginationInput = true;
438+
component.collectionSize = 100;
439+
component.paginationOptions = {
440+
id: 'test',
441+
currentPage: 5,
442+
pageSize: 10,
443+
pageSizeOptions: [10, 20, 50],
444+
directionLinks: true,
445+
boundaryLinks: true,
446+
ellipses: true,
447+
maxSize: 10,
448+
rotate: false,
449+
size: 'lg',
450+
disabled: false,
451+
};
452+
fixture.detectChanges();
453+
});
454+
455+
it('should render the input field on the active page', () => {
456+
fixture.detectChanges();
457+
const input = fixture.debugElement.nativeElement.querySelector('input#paginationInput');
458+
expect(input).toBeTruthy();
459+
expect(input.type).toBe('text');
460+
});
461+
462+
it('should set the input width to match the number of digits of the last page plus 3 (in ch)', () => {
463+
fixture.detectChanges();
464+
const input = fixture.debugElement.nativeElement.querySelector('input#paginationInput');
465+
// 10 pages, so 2 digits + 3 = 5ch
466+
expect(input.style.width).toContain('ch');
467+
expect(parseInt(input.style.width, 10)).toBeGreaterThanOrEqual(5);
468+
});
469+
470+
it('should call selectPage when clicking the search button', () => {
471+
spyOn(component, 'selectPage');
472+
fixture.detectChanges();
473+
const button = fixture.debugElement.nativeElement.querySelector('button.search-button');
474+
const input = fixture.debugElement.nativeElement.querySelector('input#paginationInput');
475+
input.value = '3';
476+
button.click();
477+
expect(component.selectPage).toHaveBeenCalledWith('3');
478+
});
479+
480+
it('should call selectPage when pressing enter in the input', () => {
481+
spyOn(component, 'selectPage');
482+
fixture.detectChanges();
483+
const input = fixture.debugElement.nativeElement.querySelector('input#paginationInput');
484+
input.value = '7';
485+
const event = new KeyboardEvent('keyup', { key: 'Enter' });
486+
input.dispatchEvent(event);
487+
fixture.detectChanges();
488+
expect(component.selectPage).toHaveBeenCalledWith('7');
489+
});
490+
});
491+
394492
});
395493

396494
// declare a test component

src/app/shared/pagination/pagination.component.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
AsyncPipe,
33
NgClass,
4+
NgStyle,
45
} from '@angular/common';
56
import {
67
ChangeDetectionStrategy,
@@ -77,6 +78,7 @@ interface PaginationDetails {
7778
NgbPaginationModule,
7879
NgbTooltipModule,
7980
NgClass,
81+
NgStyle,
8082
RSSComponent,
8183
TranslateModule,
8284
],
@@ -122,6 +124,11 @@ export class PaginationComponent implements OnChanges, OnDestroy, OnInit {
122124
*/
123125
@Input() sortConfig: SortOptions;
124126

127+
/**
128+
* Whether or not the pagination should show an input field to select the page number.
129+
*/
130+
@Input() enablePaginationInput = false;
131+
125132
/**
126133
* An event fired when the page is changed.
127134
* Event's payload equals to the newly selected page.
@@ -451,13 +458,33 @@ export class PaginationComponent implements OnChanges, OnDestroy, OnInit {
451458
/**
452459
* Update page when next or prev button is clicked
453460
* @param value
461+
* @param isPageInsteadOfDelta
454462
*/
455-
updatePagination(value: number) {
463+
updatePagination(value: number, isPageInsteadOfDelta = false) {
456464
this.paginationService.getCurrentPagination(this.id, this.paginationOptions).pipe(take(1)).subscribe((currentPaginationOptions) => {
457-
this.updateParams({ page: (currentPaginationOptions.currentPage + value) });
465+
const page = isPageInsteadOfDelta ? value : currentPaginationOptions.currentPage + value;
466+
this.updateParams({ page });
458467
});
459468
}
460469

470+
/**
471+
* Select any given page.
472+
* @param page
473+
*/
474+
selectPage(page: string) {
475+
const pageNumber = parseInt(page, 10);
476+
this.pageChange.emit(pageNumber);
477+
this.updatePagination(pageNumber, true);
478+
}
479+
480+
/**
481+
* Format the input value to only allow numbers
482+
* @param input
483+
*/
484+
formatInput(input: HTMLInputElement) {
485+
input.value = input.value.replace(/[^0-9]/g, '');
486+
}
487+
461488
/**
462489
* Get the sort options to use for the RSS feed. Defaults to the sort options used for this pagination component
463490
* so it matches the search/browse context, but also allows more flexibility if, for example a top-level community

0 commit comments

Comments
 (0)