add changes
This commit is contained in:
@@ -0,0 +1,334 @@
|
||||
import { Component, input, output } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { PaginationState } from '../../../core/services/pagination.service';
|
||||
|
||||
/**
|
||||
* Pagination Component
|
||||
* Reusable pagination controls with customizable options
|
||||
*
|
||||
* Features:
|
||||
* - Previous/Next buttons
|
||||
* - First/Last page buttons
|
||||
* - Page number buttons with active state
|
||||
* - "Showing X-Y of Z results" display
|
||||
* - Page size selector
|
||||
* - Responsive design (fewer buttons on mobile)
|
||||
* - Accessible with keyboard navigation and ARIA labels
|
||||
*
|
||||
* @example
|
||||
* <app-pagination
|
||||
* [state]="paginationState()"
|
||||
* [pageSizeOptions]="[10, 25, 50, 100]"
|
||||
* [showPageSizeSelector]="true"
|
||||
* [showFirstLast]="true"
|
||||
* [maxVisiblePages]="5"
|
||||
* (pageChange)="onPageChange($event)"
|
||||
* (pageSizeChange)="onPageSizeChange($event)">
|
||||
* </app-pagination>
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-pagination',
|
||||
imports: [
|
||||
CommonModule,
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
MatTooltipModule,
|
||||
MatSelectModule,
|
||||
MatFormFieldModule
|
||||
],
|
||||
template: `
|
||||
<div class="pagination-container" *ngIf="state()">
|
||||
<!-- Results info -->
|
||||
<div class="pagination-info">
|
||||
<span class="info-text">
|
||||
Showing <strong>{{ state()!.startIndex }}</strong>
|
||||
to <strong>{{ state()!.endIndex }}</strong>
|
||||
of <strong>{{ state()!.totalItems }}</strong> {{ itemLabel() }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="pagination-actions">
|
||||
<!-- Page size selector -->
|
||||
@if (showPageSizeSelector()) {
|
||||
<mat-form-field class="page-size-selector" appearance="outline">
|
||||
<mat-select
|
||||
[value]="state()!.pageSize"
|
||||
(selectionChange)="onPageSizeChange($event.value)"
|
||||
aria-label="Items per page">
|
||||
@for (option of pageSizeOptions(); track option) {
|
||||
<mat-option [value]="option">{{ option }} per page</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
}
|
||||
|
||||
<!-- Pagination controls -->
|
||||
<div class="pagination-controls">
|
||||
<!-- First page -->
|
||||
@if (showFirstLast()) {
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="onPageChange(1)"
|
||||
[disabled]="!state()!.hasPrev"
|
||||
matTooltip="First page"
|
||||
aria-label="Go to first page">
|
||||
<mat-icon>first_page</mat-icon>
|
||||
</button>
|
||||
}
|
||||
|
||||
<!-- Previous page -->
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="onPageChange(state()!.currentPage - 1)"
|
||||
[disabled]="!state()!.hasPrev"
|
||||
matTooltip="Previous page"
|
||||
aria-label="Go to previous page">
|
||||
<mat-icon>chevron_left</mat-icon>
|
||||
</button>
|
||||
|
||||
<!-- Page numbers -->
|
||||
<div class="page-numbers">
|
||||
@for (page of pageNumbers(); track page) {
|
||||
@if (page === '...') {
|
||||
<span class="ellipsis">{{ page }}</span>
|
||||
} @else {
|
||||
<button
|
||||
mat-button
|
||||
class="page-button"
|
||||
[class.active]="page === state()!.currentPage"
|
||||
(click)="handlePageClick(page)"
|
||||
[attr.aria-label]="'Go to page ' + page"
|
||||
[attr.aria-current]="page === state()!.currentPage ? 'page' : null">
|
||||
{{ page }}
|
||||
</button>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Next page -->
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="onPageChange(state()!.currentPage + 1)"
|
||||
[disabled]="!state()!.hasNext"
|
||||
matTooltip="Next page"
|
||||
aria-label="Go to next page">
|
||||
<mat-icon>chevron_right</mat-icon>
|
||||
</button>
|
||||
|
||||
<!-- Last page -->
|
||||
@if (showFirstLast()) {
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="onPageChange(state()!.totalPages)"
|
||||
[disabled]="!state()!.hasNext"
|
||||
matTooltip="Last page"
|
||||
aria-label="Go to last page">
|
||||
<mat-icon>last_page</mat-icon>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
.pagination-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background: var(--surface-color, #fff);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.pagination-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.info-text {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary, #666);
|
||||
}
|
||||
|
||||
.info-text strong {
|
||||
color: var(--text-primary, #333);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.pagination-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.page-size-selector {
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.page-size-selector ::ng-deep .mat-mdc-form-field-subscript-wrapper {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pagination-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.page-numbers {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.page-button {
|
||||
min-width: 40px;
|
||||
height: 40px;
|
||||
padding: 0 8px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.page-button:hover {
|
||||
background-color: var(--hover-color, rgba(0, 0, 0, 0.04));
|
||||
}
|
||||
|
||||
.page-button.active {
|
||||
background-color: var(--primary-color, #1976d2);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.page-button.active:hover {
|
||||
background-color: var(--primary-dark, #1565c0);
|
||||
}
|
||||
|
||||
.ellipsis {
|
||||
padding: 0 8px;
|
||||
color: var(--text-secondary, #666);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.pagination-container {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.pagination-actions {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.page-size-selector {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pagination-controls {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.page-numbers {
|
||||
gap: 0.125rem;
|
||||
}
|
||||
|
||||
.page-button {
|
||||
min-width: 36px;
|
||||
height: 36px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Hide ellipsis on very small screens */
|
||||
@media (max-width: 480px) {
|
||||
.ellipsis {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark mode support */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.pagination-container {
|
||||
background: var(--surface-dark, #1e1e1e);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.info-text {
|
||||
color: var(--text-secondary-dark, #b0b0b0);
|
||||
}
|
||||
|
||||
.info-text strong {
|
||||
color: var(--text-primary-dark, #e0e0e0);
|
||||
}
|
||||
|
||||
.page-button:hover {
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.ellipsis {
|
||||
color: var(--text-secondary-dark, #b0b0b0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Focus styles for accessibility */
|
||||
.page-button:focus-visible,
|
||||
button:focus-visible {
|
||||
outline: 2px solid var(--primary-color, #1976d2);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Disabled state */
|
||||
button:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class PaginationComponent {
|
||||
// Input signals
|
||||
state = input.required<PaginationState | null>();
|
||||
pageNumbers = input<(number | string)[]>([]);
|
||||
pageSizeOptions = input<number[]>([10, 25, 50, 100]);
|
||||
showPageSizeSelector = input<boolean>(true);
|
||||
showFirstLast = input<boolean>(true);
|
||||
maxVisiblePages = input<number>(5);
|
||||
itemLabel = input<string>('results');
|
||||
|
||||
// Output events
|
||||
pageChange = output<number>();
|
||||
pageSizeChange = output<number>();
|
||||
|
||||
/**
|
||||
* Handle page change
|
||||
*/
|
||||
onPageChange(page: number): void {
|
||||
if (page >= 1 && page <= (this.state()?.totalPages ?? 1)) {
|
||||
this.pageChange.emit(page);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle page button click (for page numbers)
|
||||
*/
|
||||
handlePageClick(page: number | string): void {
|
||||
if (typeof page === 'number') {
|
||||
this.onPageChange(page);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle page size change
|
||||
*/
|
||||
onPageSizeChange(pageSize: number): void {
|
||||
this.pageSizeChange.emit(pageSize);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user