175 lines
5.8 KiB
JavaScript
175 lines
5.8 KiB
JavaScript
import React, {Component, Fragment} from 'react';
|
|
|
|
const LEFT_PAGE = 'LEFT';
|
|
const RIGHT_PAGE = 'RIGHT';
|
|
|
|
/**
|
|
* Helper method for creating a range of numbers
|
|
* range(1, 5) => [1, 2, 3, 4, 5]
|
|
*/
|
|
const range = (from, to, step = 1) => {
|
|
let i = from;
|
|
const range = [];
|
|
|
|
while (i <= to) {
|
|
range.push(i);
|
|
i += step;
|
|
}
|
|
|
|
return range;
|
|
}
|
|
|
|
export default class Paginate extends Component {
|
|
constructor(props) {
|
|
super(props);
|
|
|
|
this.loadProps = this.loadProps.bind(this);
|
|
this.gotoPage = this.gotoPage.bind(this);
|
|
this.handleClick = this.handleClick.bind(this);
|
|
this.handleMoveLeft = this.handleMoveLeft.bind(this);
|
|
this.handleMoveRight = this.handleMoveRight.bind(this);
|
|
this.makePageLink = this.makePageLink.bind(this);
|
|
this.fetchPageNumbers = this.fetchPageNumbers.bind(this);
|
|
|
|
this.loadProps();
|
|
this.state = {currentPage: this.currentPage};
|
|
}
|
|
|
|
loadProps() {
|
|
const {totalRecords = 0, pageLimit = 30, pageNeighbours = 0, currentPage = 1} = this.props;
|
|
|
|
this.pageLimit = typeof pageLimit === 'number' ? pageLimit : 30;
|
|
this.totalRecords = typeof totalRecords === 'number' ? totalRecords : 0;
|
|
this.pageNeighbours = typeof pageNeighbours === 'number' ? Math.max(0, Math.min(pageNeighbours, 2)) : 0;
|
|
this.totalPages = Math.ceil(this.totalRecords / this.pageLimit);
|
|
this.currentPage = typeof currentPage === 'number' ? currentPage : 1
|
|
}
|
|
|
|
gotoPage(page) {
|
|
const {onPageChanged = f => f} = this.props;
|
|
const currentPage = Math.max(0, Math.min(page, this.totalPages));
|
|
this.setState({currentPage: currentPage}, () => onPageChanged(page));
|
|
}
|
|
|
|
handleClick(page) {
|
|
return (evt) => {
|
|
evt.preventDefault();
|
|
this.gotoPage(page);
|
|
}
|
|
}
|
|
|
|
handleMoveLeft(evt) {
|
|
evt.preventDefault();
|
|
this.gotoPage(this.state.currentPage - 1);
|
|
}
|
|
|
|
handleMoveRight(evt) {
|
|
evt.preventDefault();
|
|
this.gotoPage(this.state.currentPage + 1);
|
|
}
|
|
|
|
makePageLink(page) {
|
|
const {makeLink = f => f} = this.props;
|
|
return makeLink(page);
|
|
}
|
|
|
|
/**
|
|
* Let's say we have 10 pages and we set pageNeighbours to 2
|
|
* Given that the current page is 6
|
|
* The pagination control will look like the following:
|
|
*
|
|
* (1) < {4 5} [6] {7 8} > (10)
|
|
*
|
|
* (x) => terminal pages: first and last page(always visible)
|
|
* [x] => represents current page
|
|
* {...x} => represents page neighbours
|
|
*/
|
|
fetchPageNumbers() {
|
|
const totalPages = this.totalPages;
|
|
const currentPage = this.state.currentPage;
|
|
const pageNeighbours = this.pageNeighbours;
|
|
|
|
/**
|
|
* totalNumbers: the total page numbers to show on the control
|
|
* totalBlocks: totalNumbers + 2 to cover for the left(<) and right(>) controls
|
|
*/
|
|
const totalNumbers = (this.pageNeighbours * 2) + 3;
|
|
const totalBlocks = totalNumbers + 2;
|
|
|
|
if (totalPages > totalBlocks) {
|
|
const startPage = Math.max(2, currentPage - pageNeighbours);
|
|
const endPage = Math.min(totalPages - 1, currentPage + pageNeighbours);
|
|
|
|
let pages = range(startPage, endPage);
|
|
|
|
/**
|
|
* hasLeftSpill: has hidden pages to the left
|
|
* hasRightSpill: has hidden pages to the right
|
|
* spillOffset: number of hidden pages either to the left or to the right
|
|
*/
|
|
const hasLeftSpill = startPage > 2;
|
|
const hasRightSpill = (totalPages - endPage) > 1;
|
|
const spillOffset = totalNumbers - (pages.length + 1);
|
|
|
|
// handle: (1) < {5 6} [7] {8 9} (10)
|
|
if (hasLeftSpill && !hasRightSpill) {
|
|
const extraPages = range(startPage - spillOffset, startPage - 1);
|
|
pages = [LEFT_PAGE, ...extraPages, ...pages];
|
|
// handle: (1) {2 3} [4] {5 6} > (10)
|
|
} else if (!hasLeftSpill && hasRightSpill) {
|
|
const extraPages = range(endPage + 1, endPage + spillOffset);
|
|
pages = [...pages, ...extraPages, RIGHT_PAGE];
|
|
// handle: (1) < {4 5} [6] {7 8} > (10)
|
|
} else if (hasLeftSpill && hasRightSpill) {
|
|
pages = [LEFT_PAGE, ...pages, RIGHT_PAGE];
|
|
}
|
|
|
|
return [1, ...pages, totalPages];
|
|
|
|
}
|
|
|
|
return range(1, totalPages);
|
|
|
|
}
|
|
|
|
render() {
|
|
this.loadProps();
|
|
|
|
if (!this.totalRecords || this.totalPages === 1) return null;
|
|
|
|
const currentPage = this.state.currentPage;
|
|
const pages = this.fetchPageNumbers();
|
|
const blocks = pages.map((page, index) => {
|
|
if (page === LEFT_PAGE) return (
|
|
<li key={page} className="page-item">
|
|
<a className="page-link" href={this.makePageLink(this.state.currentPage - 1)} onClick={this.handleMoveLeft}>
|
|
<span>«</span>
|
|
</a>
|
|
</li>
|
|
);
|
|
if (page === RIGHT_PAGE) return (
|
|
<li key={page} className="page-item">
|
|
<a className="page-link" href={this.makePageLink(this.state.currentPage + 1)} onClick={this.handleMoveRight}>
|
|
<span>»</span>
|
|
</a>
|
|
</li>
|
|
);
|
|
return (
|
|
<li key={page} className={`page-item ${currentPage === page ? 'active' : ''}`}>
|
|
<a className="page-link" href={this.makePageLink(page)} onClick={this.handleClick(page)}>{page}</a>
|
|
</li>
|
|
);
|
|
})
|
|
|
|
return (
|
|
<Fragment>
|
|
<ul className="pagination">
|
|
{blocks}
|
|
</ul>
|
|
</Fragment>
|
|
);
|
|
|
|
}
|
|
}
|
|
|