Skip to content

Commit 25759e6

Browse files
committed
feat: Add pagination component
1 parent ecca749 commit 25759e6

File tree

7 files changed

+439
-19
lines changed

7 files changed

+439
-19
lines changed
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
@import '../../../scss/fonts';
2+
@import '../../../scss/media-queries';
3+
@import '../../../scss/variables';
4+
5+
6+
.pagination {
7+
display: flex;
8+
align-self: center;
9+
list-style-type: none;
10+
justify-content: center;
11+
margin-top: 24px;
12+
13+
.pagination-item {
14+
@include font-size(14px);
15+
16+
padding: 0 12px;
17+
height: 32px;
18+
min-width: 32px;
19+
text-align: center;
20+
margin: auto 4px;
21+
color: var(--accent-primary);
22+
display: flex;
23+
box-sizing: border-box;
24+
align-items: center;
25+
border-radius: 6px;
26+
font-family: $font-mono;
27+
28+
&.dots:hover {
29+
background-color: transparent;
30+
cursor: default;
31+
}
32+
&:hover {
33+
background-color: var(--panel-background-highlight);
34+
cursor: pointer;
35+
}
36+
37+
&.selected {
38+
background-color: var(--background);
39+
}
40+
41+
.arrow {
42+
&::before {
43+
position: relative;
44+
content: '';
45+
display: inline-block;
46+
width: 0.4em;
47+
height: 0.4em;
48+
border-right: 0.12em solid;
49+
border-top: 0.12em solid;
50+
border-right-color: var(--text-color-primary);
51+
border-top-color: var(--text-color-primary);
52+
}
53+
54+
&.left {
55+
transform: rotate(-135deg) translate(-25%);
56+
}
57+
58+
&.right {
59+
transform: rotate(45deg);
60+
}
61+
}
62+
63+
&.disabled {
64+
pointer-events: none;
65+
66+
.arrow::before {
67+
border-right: 0.12em solid;
68+
border-top: 0.12em solid;
69+
border-right-color: var(--text-color-secondary);
70+
border-top-color: var(--text-color-secondary);
71+
}
72+
73+
&:hover {
74+
background-color: transparent;
75+
cursor: default;
76+
}
77+
}
78+
}
79+
}
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
import classNames from "classnames";
2+
import React, { ReactNode, Component } from "react";
3+
import "./Pagination.scss";
4+
import { PaginationProps } from "./PaginationProps";
5+
import { PaginationState } from "./PaginationState";
6+
7+
/**
8+
* Component which will display pagination.
9+
*/
10+
class Pagination extends Component<PaginationProps, PaginationState> {
11+
/**
12+
* Dots for pagination.
13+
*/
14+
private static readonly DOTS: string = "...";
15+
16+
/**
17+
* Is the component mounted.
18+
*/
19+
private _isMounted?: boolean;
20+
21+
/**
22+
* Create a new instance of Pagination.
23+
* @param props The props.
24+
*/
25+
constructor(props: PaginationProps) {
26+
super(props);
27+
this.state = {
28+
paginationRange: [],
29+
lastPage: 0,
30+
isMobile: false
31+
};
32+
}
33+
34+
/**
35+
* The component updated.
36+
* @param prevProps previous props
37+
*/
38+
public componentDidUpdate(prevProps: PaginationProps): void {
39+
if (this.props !== prevProps) {
40+
this.setState(
41+
{ paginationRange: this.updatePaginationRange() },
42+
() => this.setState(
43+
{ lastPage: this.state.paginationRange[this.state.paginationRange.length - 1] as number }
44+
)
45+
);
46+
}
47+
}
48+
49+
/**
50+
* The component mounted.
51+
*/
52+
public componentDidMount(): void {
53+
this._isMounted = true;
54+
window.addEventListener("resize", this.resize.bind(this));
55+
this.resize();
56+
}
57+
58+
public resize() {
59+
const isMobileViewPort = window.innerWidth < 768;
60+
61+
if (this.state.isMobile !== isMobileViewPort && this._isMounted) {
62+
this.setState(
63+
{ isMobile: isMobileViewPort },
64+
() => this.setState({ paginationRange: this.updatePaginationRange() })
65+
);
66+
}
67+
}
68+
69+
/**
70+
* The component will unmounted.
71+
*/
72+
public async componentWillUnmount(): Promise<void> {
73+
this._isMounted = false;
74+
window.removeEventListener("resize", this.resize.bind(this));
75+
}
76+
77+
/**
78+
* Render the component.
79+
* @returns The node to render.
80+
*/
81+
public render(): ReactNode {
82+
return (
83+
<ul
84+
className={classNames("pagination", {
85+
[this.props.classNames as string]: this.props.classNames !== undefined,
86+
hidden: (this.props.currentPage === 0 || this.state.paginationRange.length < 2)
87+
})}
88+
>
89+
<li
90+
className={classNames("pagination-item", {
91+
disabled: this.props.currentPage < 11,
92+
hidden: this.state.isMobile
93+
})}
94+
onClick={() => {
95+
this.props.onPageChange(this.props.currentPage - 10);
96+
}}
97+
>
98+
<div className="arrow left" />
99+
<div className="arrow left" />
100+
</li>
101+
<li
102+
className={classNames("pagination-item", {
103+
disabled: this.props.currentPage === 1
104+
})}
105+
onClick={() => {
106+
this.props.onPageChange(this.props.currentPage - 1);
107+
}}
108+
>
109+
<div className="arrow left" />
110+
</li>
111+
{this.state.paginationRange.map((pageNumber: (number|string), idx: number) => {
112+
if (pageNumber === Pagination.DOTS) {
113+
return <li key={idx} className="pagination-item dots">{pageNumber}</li>;
114+
}
115+
116+
return (
117+
<li
118+
key={idx}
119+
className={classNames("pagination-item", {
120+
selected: pageNumber === this.props.currentPage
121+
})}
122+
onClick={() => this.props.onPageChange(pageNumber as number)}
123+
>
124+
{pageNumber}
125+
</li>
126+
);
127+
})}
128+
<li
129+
className={classNames("pagination-item", {
130+
disabled: this.props.currentPage === this.state.lastPage
131+
})}
132+
onClick={() => {
133+
this.props.onPageChange(this.props.currentPage + 1);
134+
}}
135+
>
136+
<div className="arrow right" />
137+
</li>
138+
<li
139+
className={classNames("pagination-item", {
140+
disabled: this.props.currentPage > this.state.lastPage - 10,
141+
hidden: this.state.isMobile
142+
})}
143+
onClick={() => {
144+
this.props.onPageChange(this.props.currentPage + 10);
145+
}}
146+
>
147+
<div className="arrow right" />
148+
<div className="arrow right" />
149+
</li>
150+
</ul>
151+
);
152+
}
153+
154+
155+
/**
156+
* Update pagination range.
157+
* @returns The range of available pages.
158+
*/
159+
protected updatePaginationRange(): (string|number)[] {
160+
let paginationRange: (string|number)[] = [];
161+
162+
const totalPageCount: number = Math.ceil(this.props.totalCount / this.props.pageSize);
163+
164+
// Min page range is determined as siblingsCount + firstPage + lastPage + currentPage + 2*DOTS
165+
const minPageRangeCount: number = this.props.siblingsCount + 5;
166+
167+
if (minPageRangeCount >= totalPageCount) {
168+
paginationRange = this.range(1, totalPageCount);
169+
}
170+
171+
const leftSiblingIndex = Math.max(this.props.currentPage - this.props.siblingsCount, 1);
172+
const rightSiblingIndex = Math.min(
173+
this.props.currentPage + this.props.siblingsCount,
174+
totalPageCount
175+
);
176+
177+
/*
178+
* Do not show dots if there is only one position left
179+
* after/before the left/right page count.
180+
*/
181+
const shouldShowLeftDots = leftSiblingIndex > 2;
182+
const shouldShowRightDots = rightSiblingIndex < totalPageCount - 2;
183+
184+
const firstPageIndex = 1;
185+
const lastPageIndex = totalPageCount;
186+
187+
if (!shouldShowLeftDots && shouldShowRightDots) {
188+
const leftItemCount = 3 + (2 * this.props.siblingsCount);
189+
const leftRange = this.range(1, leftItemCount);
190+
191+
paginationRange = [...leftRange, Pagination.DOTS, totalPageCount];
192+
}
193+
194+
if (shouldShowLeftDots && !shouldShowRightDots) {
195+
const rightItemCount = 3 + (2 * this.props.siblingsCount);
196+
const rightRange = this.range(
197+
totalPageCount - rightItemCount + 1,
198+
totalPageCount
199+
);
200+
201+
paginationRange = [firstPageIndex, Pagination.DOTS, ...rightRange];
202+
}
203+
204+
if (shouldShowLeftDots && shouldShowRightDots) {
205+
const middleRange = this.range(leftSiblingIndex, rightSiblingIndex);
206+
207+
paginationRange = [firstPageIndex, Pagination.DOTS, ...middleRange, Pagination.DOTS, lastPageIndex];
208+
}
209+
210+
/*
211+
* Add extra range for large number of pages
212+
*/
213+
const rightRemainingPages = totalPageCount - (this.props.currentPage + this.props.siblingsCount);
214+
const leftRemainingPages = this.props.currentPage - this.props.siblingsCount;
215+
216+
if (!this.state.isMobile &&
217+
this.props.extraPageRangeLimit &&
218+
rightRemainingPages > this.props.extraPageRangeLimit) {
219+
const remainderMidPoint = Math.floor((rightRemainingPages) / 2) + this.props.currentPage;
220+
const rMiddleRange: (string|number)[] = this.range(remainderMidPoint - 1, remainderMidPoint + 1);
221+
rMiddleRange.push(Pagination.DOTS);
222+
const lastItemIndex = paginationRange.length - 1;
223+
paginationRange.splice(lastItemIndex, 0, ...rMiddleRange);
224+
}
225+
226+
if (!this.state.isMobile &&
227+
this.props.extraPageRangeLimit &&
228+
leftRemainingPages > this.props.extraPageRangeLimit) {
229+
const remainderMidPoint = Math.floor(leftRemainingPages / 2);
230+
const lMiddleRange: (string|number)[] = this.range(remainderMidPoint - 1, remainderMidPoint + 1);
231+
lMiddleRange.unshift(Pagination.DOTS);
232+
paginationRange.splice(1, 0, ...lMiddleRange);
233+
}
234+
235+
return paginationRange;
236+
}
237+
238+
/**
239+
* Creates an array of elements from start value to end value.
240+
* @param start Start value.
241+
* @param end End value.
242+
* @returns Array of elements from start to end value.
243+
*/
244+
private range(start: number, end: number): number[] {
245+
const length = end - start + 1;
246+
return Array.from({ length }, (_, idx) => idx + start);
247+
}
248+
}
249+
250+
export default Pagination;
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
export interface PaginationProps {
2+
3+
/**
4+
* The total number of pages.
5+
*/
6+
totalCount: number;
7+
8+
/**
9+
* The number of current page.
10+
*/
11+
currentPage: number;
12+
13+
/**
14+
* The total number of sibling pages.
15+
*/
16+
siblingsCount: number;
17+
18+
/**
19+
* Number of results per page.
20+
*/
21+
pageSize: number;
22+
23+
/**
24+
* Define limit of remaining pages above which the extra page range will be shown.
25+
*/
26+
extraPageRangeLimit?: number;
27+
28+
/**
29+
* The optional additional CSS classes.
30+
*/
31+
classNames?: string;
32+
33+
/**
34+
* Page changed.
35+
* @param page Page navigated to.
36+
*/
37+
onPageChange(page: number): void;
38+
39+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export interface PaginationState {
2+
/**
3+
* Pagination last page.
4+
*/
5+
lastPage: number;
6+
7+
/**
8+
* Pagination range.
9+
*/
10+
paginationRange: (number|string)[];
11+
12+
/**
13+
* Is mobile view.
14+
*/
15+
isMobile: boolean;
16+
}

0 commit comments

Comments
 (0)