react-paginate
Version:
A ReactJS component that creates a pagination.
617 lines (553 loc) • 18.8 kB
JavaScript
'use strict';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import PageView from './PageView';
import BreakView from './BreakView';
import { classNameIfDefined } from './utils';
export default class PaginationBoxView extends Component {
static propTypes = {
pageCount: PropTypes.number.isRequired,
pageRangeDisplayed: PropTypes.number,
marginPagesDisplayed: PropTypes.number,
previousLabel: PropTypes.node,
previousAriaLabel: PropTypes.string,
prevPageRel: PropTypes.string,
prevRel: PropTypes.string,
nextLabel: PropTypes.node,
nextAriaLabel: PropTypes.string,
nextPageRel: PropTypes.string,
nextRel: PropTypes.string,
breakLabel: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
breakAriaLabels: PropTypes.shape({
forward: PropTypes.string,
backward: PropTypes.string,
}),
hrefBuilder: PropTypes.func,
hrefAllControls: PropTypes.bool,
onPageChange: PropTypes.func,
onPageActive: PropTypes.func,
onClick: PropTypes.func,
initialPage: PropTypes.number,
forcePage: PropTypes.number,
disableInitialCallback: PropTypes.bool,
containerClassName: PropTypes.string,
className: PropTypes.string,
pageClassName: PropTypes.string,
pageLinkClassName: PropTypes.string,
pageLabelBuilder: PropTypes.func,
activeClassName: PropTypes.string,
activeLinkClassName: PropTypes.string,
previousClassName: PropTypes.string,
nextClassName: PropTypes.string,
previousLinkClassName: PropTypes.string,
nextLinkClassName: PropTypes.string,
disabledClassName: PropTypes.string,
disabledLinkClassName: PropTypes.string,
breakClassName: PropTypes.string,
breakLinkClassName: PropTypes.string,
extraAriaContext: PropTypes.string,
ariaLabelBuilder: PropTypes.func,
eventListener: PropTypes.string,
renderOnZeroPageCount: PropTypes.func,
selectedPageRel: PropTypes.string,
};
static defaultProps = {
pageRangeDisplayed: 2,
marginPagesDisplayed: 3,
activeClassName: 'selected',
previousLabel: 'Previous',
previousClassName: 'previous',
previousAriaLabel: 'Previous page',
prevPageRel: 'prev',
prevRel: 'prev',
nextLabel: 'Next',
nextClassName: 'next',
nextAriaLabel: 'Next page',
nextPageRel: 'next',
nextRel: 'next',
breakLabel: '...',
breakAriaLabels: { forward: 'Jump forward', backward: 'Jump backward' },
disabledClassName: 'disabled',
disableInitialCallback: false,
pageLabelBuilder: (page) => page,
eventListener: 'onClick',
renderOnZeroPageCount: undefined,
selectedPageRel: 'canonical',
hrefAllControls: false,
};
constructor(props) {
super(props);
if (props.initialPage !== undefined && props.forcePage !== undefined) {
console.warn(
`(react-paginate): Both initialPage (${props.initialPage}) and forcePage (${props.forcePage}) props are provided, which is discouraged.` +
' Use exclusively forcePage prop for a controlled component.\nSee https://reactjs.org/docs/forms.html#controlled-components'
);
}
let initialSelected;
if (props.initialPage) {
initialSelected = props.initialPage;
} else if (props.forcePage) {
initialSelected = props.forcePage;
} else {
initialSelected = 0;
}
this.state = {
selected: initialSelected,
};
}
componentDidMount() {
const {
initialPage,
disableInitialCallback,
extraAriaContext,
pageCount,
forcePage,
} = this.props;
// Call the callback with the initialPage item:
if (typeof initialPage !== 'undefined' && !disableInitialCallback) {
this.callCallback(initialPage);
}
if (extraAriaContext) {
console.warn(
'DEPRECATED (react-paginate): The extraAriaContext prop is deprecated. You should now use the ariaLabelBuilder instead.'
);
}
if (!Number.isInteger(pageCount)) {
console.warn(
`(react-paginate): The pageCount prop value provided is not an integer (${pageCount}). Did you forget a Math.ceil()?`
);
}
if (initialPage !== undefined && initialPage > pageCount - 1) {
console.warn(
`(react-paginate): The initialPage prop provided is greater than the maximum page index from pageCount prop (${initialPage} > ${
pageCount - 1
}).`
);
}
if (forcePage !== undefined && forcePage > pageCount - 1) {
console.warn(
`(react-paginate): The forcePage prop provided is greater than the maximum page index from pageCount prop (${forcePage} > ${
pageCount - 1
}).`
);
}
}
componentDidUpdate(prevProps) {
if (
this.props.forcePage !== undefined &&
this.props.forcePage !== prevProps.forcePage
) {
if (this.props.forcePage > this.props.pageCount - 1) {
console.warn(
`(react-paginate): The forcePage prop provided is greater than the maximum page index from pageCount prop (${
this.props.forcePage
} > ${this.props.pageCount - 1}).`
);
}
this.setState({ selected: this.props.forcePage });
}
if (
Number.isInteger(prevProps.pageCount) &&
!Number.isInteger(this.props.pageCount)
) {
console.warn(
`(react-paginate): The pageCount prop value provided is not an integer (${this.props.pageCount}). Did you forget a Math.ceil()?`
);
}
}
handlePreviousPage = (event) => {
const { selected } = this.state;
this.handleClick(event, null, selected > 0 ? selected - 1 : undefined, {
isPrevious: true,
});
};
handleNextPage = (event) => {
const { selected } = this.state;
const { pageCount } = this.props;
this.handleClick(
event,
null,
selected < pageCount - 1 ? selected + 1 : undefined,
{ isNext: true }
);
};
handlePageSelected = (selected, event) => {
if (this.state.selected === selected) {
this.callActiveCallback(selected);
this.handleClick(event, null, undefined, { isActive: true });
return;
}
this.handleClick(event, null, selected);
};
handlePageChange = (selected) => {
if (this.state.selected === selected) {
return;
}
this.setState({ selected });
// Call the callback with the new selected item:
this.callCallback(selected);
};
getEventListener = (handlerFunction) => {
const { eventListener } = this.props;
return {
[eventListener]: handlerFunction,
};
};
getForwardJump() {
const { selected } = this.state;
const { pageCount, pageRangeDisplayed } = this.props;
const forwardJump = selected + pageRangeDisplayed;
return forwardJump >= pageCount ? pageCount - 1 : forwardJump;
}
getBackwardJump() {
const { selected } = this.state;
const { pageRangeDisplayed } = this.props;
const backwardJump = selected - pageRangeDisplayed;
return backwardJump < 0 ? 0 : backwardJump;
}
handleClick = (
event,
index,
nextSelectedPage,
{
isPrevious = false,
isNext = false,
isBreak = false,
isActive = false,
} = {}
) => {
event.preventDefault ? event.preventDefault() : (event.returnValue = false);
const { selected } = this.state;
const { onClick } = this.props;
let newPage = nextSelectedPage;
if (onClick) {
const onClickReturn = onClick({
index,
selected,
nextSelectedPage,
event,
isPrevious,
isNext,
isBreak,
isActive,
});
if (onClickReturn === false) {
// We abord standard behavior and let parent handle
// all behavior.
return;
}
if (Number.isInteger(onClickReturn)) {
// We assume parent want to go to the returned page.
newPage = onClickReturn;
}
}
if (newPage !== undefined) {
this.handlePageChange(newPage);
}
};
handleBreakClick = (index, event) => {
const { selected } = this.state;
this.handleClick(
event,
index,
selected < index ? this.getForwardJump() : this.getBackwardJump(),
{ isBreak: true }
);
};
getElementHref(pageIndex) {
const { hrefBuilder, pageCount, hrefAllControls } = this.props;
if (!hrefBuilder) return;
if (hrefAllControls || (pageIndex >= 0 && pageIndex < pageCount)) {
return hrefBuilder(pageIndex + 1, pageCount, this.state.selected);
}
}
ariaLabelBuilder(pageIndex) {
const selected = pageIndex === this.state.selected;
if (
this.props.ariaLabelBuilder &&
pageIndex >= 0 &&
pageIndex < this.props.pageCount
) {
let label = this.props.ariaLabelBuilder(pageIndex + 1, selected);
// DEPRECATED: The extraAriaContext prop was used to add additional context
// to the aria-label. Users should now use the ariaLabelBuilder instead.
if (this.props.extraAriaContext && !selected) {
label = label + ' ' + this.props.extraAriaContext;
}
return label;
}
}
callCallback = (selectedItem) => {
if (
this.props.onPageChange !== undefined &&
typeof this.props.onPageChange === 'function'
) {
this.props.onPageChange({ selected: selectedItem });
}
};
callActiveCallback = (selectedItem) => {
if (
this.props.onPageActive !== undefined &&
typeof this.props.onPageActive === 'function'
) {
this.props.onPageActive({ selected: selectedItem });
}
};
getElementPageRel = (index) => {
const { selected } = this.state;
const { nextPageRel, prevPageRel, selectedPageRel } = this.props;
if (selected - 1 === index) {
return prevPageRel;
} else if (selected === index) {
return selectedPageRel;
} else if (selected + 1 === index) {
return nextPageRel;
}
return undefined;
};
getPageElement(index) {
const { selected } = this.state;
const {
pageClassName,
pageLinkClassName,
activeClassName,
activeLinkClassName,
extraAriaContext,
pageLabelBuilder,
} = this.props;
return (
<PageView
key={index}
pageSelectedHandler={this.handlePageSelected.bind(null, index)}
selected={selected === index}
rel={this.getElementPageRel(index)}
pageClassName={pageClassName}
pageLinkClassName={pageLinkClassName}
activeClassName={activeClassName}
activeLinkClassName={activeLinkClassName}
extraAriaContext={extraAriaContext}
href={this.getElementHref(index)}
ariaLabel={this.ariaLabelBuilder(index)}
page={index + 1}
pageLabelBuilder={pageLabelBuilder}
getEventListener={this.getEventListener}
/>
);
}
pagination = () => {
const items = [];
const {
pageRangeDisplayed,
pageCount,
marginPagesDisplayed,
breakLabel,
breakClassName,
breakLinkClassName,
breakAriaLabels,
} = this.props;
const { selected } = this.state;
if (pageCount <= pageRangeDisplayed) {
for (let index = 0; index < pageCount; index++) {
items.push(this.getPageElement(index));
}
} else {
let leftSide = pageRangeDisplayed / 2;
let rightSide = pageRangeDisplayed - leftSide;
// If the selected page index is on the default right side of the pagination,
// we consider that the new right side is made up of it (= only one break element).
// If the selected page index is on the default left side of the pagination,
// we consider that the new left side is made up of it (= only one break element).
if (selected > pageCount - pageRangeDisplayed / 2) {
rightSide = pageCount - selected;
leftSide = pageRangeDisplayed - rightSide;
} else if (selected < pageRangeDisplayed / 2) {
leftSide = selected;
rightSide = pageRangeDisplayed - leftSide;
}
let createPageView = (index) => this.getPageElement(index);
let index;
let breakView;
// First pass: process the pages or breaks to display (or not).
const pagesBreaking = [];
for (index = 0; index < pageCount; index++) {
const page = index + 1;
// If the page index is lower than the margin defined,
// the page has to be displayed on the left side of
// the pagination.
if (page <= marginPagesDisplayed) {
pagesBreaking.push({
type: 'page',
index,
display: createPageView(index),
});
continue;
}
// If the page index is greater than the page count
// minus the margin defined, the page has to be
// displayed on the right side of the pagination.
if (page > pageCount - marginPagesDisplayed) {
pagesBreaking.push({
type: 'page',
index,
display: createPageView(index),
});
continue;
}
// If it is the first element of the array the rightSide need to be adjusted,
// otherwise an extra element will be rendered
const adjustedRightSide =
selected === 0 && pageRangeDisplayed > 1 ? rightSide - 1 : rightSide;
// If the page index is near the selected page index
// and inside the defined range (pageRangeDisplayed)
// we have to display it (it will create the center
// part of the pagination).
if (
index >= selected - leftSide &&
index <= selected + adjustedRightSide
) {
pagesBreaking.push({
type: 'page',
index,
display: createPageView(index),
});
continue;
}
// If the page index doesn't meet any of the conditions above,
// we check if the last item of the current "items" array
// is a break element. If not, we add a break element, else,
// we do nothing (because we don't want to display the page).
if (
breakLabel &&
pagesBreaking.length > 0 &&
pagesBreaking[pagesBreaking.length - 1].display !== breakView &&
// We do not show break if only one active page is displayed.
(pageRangeDisplayed > 0 || marginPagesDisplayed > 0)
) {
const useBreakAriaLabel =
index < selected
? breakAriaLabels.backward
: breakAriaLabels.forward;
breakView = (
<BreakView
key={index}
breakAriaLabel={useBreakAriaLabel}
breakLabel={breakLabel}
breakClassName={breakClassName}
breakLinkClassName={breakLinkClassName}
breakHandler={this.handleBreakClick.bind(null, index)}
getEventListener={this.getEventListener}
/>
);
pagesBreaking.push({ type: 'break', index, display: breakView });
}
}
// Second pass: we remove breaks containing one page to the actual page.
pagesBreaking.forEach((pageElement, i) => {
let actualPageElement = pageElement;
// 1 2 3 4 5 6 7 ... 9 10
// |
// 1 2 ... 4 5 6 7 8 9 10
// |
// The break should be replaced by the page.
if (
pageElement.type === 'break' &&
pagesBreaking[i - 1] &&
pagesBreaking[i - 1].type === 'page' &&
pagesBreaking[i + 1] &&
pagesBreaking[i + 1].type === 'page' &&
pagesBreaking[i + 1].index - pagesBreaking[i - 1].index <= 2
) {
actualPageElement = {
type: 'page',
index: pageElement.index,
display: createPageView(pageElement.index),
};
}
// We add the displayed elements in the same pass, to avoid another iteration.
items.push(actualPageElement.display);
});
}
return items;
};
render() {
const { renderOnZeroPageCount } = this.props;
if (this.props.pageCount === 0 && renderOnZeroPageCount !== undefined) {
return renderOnZeroPageCount
? renderOnZeroPageCount(this.props)
: renderOnZeroPageCount;
}
const {
disabledClassName,
disabledLinkClassName,
pageCount,
className,
containerClassName,
previousLabel,
previousClassName,
previousLinkClassName,
previousAriaLabel,
prevRel,
nextLabel,
nextClassName,
nextLinkClassName,
nextAriaLabel,
nextRel,
} = this.props;
const { selected } = this.state;
const isPreviousDisabled = selected === 0;
const isNextDisabled = selected === pageCount - 1;
const previousClasses = `${classNameIfDefined(previousClassName)}${
isPreviousDisabled ? ` ${classNameIfDefined(disabledClassName)}` : ''
}`;
const nextClasses = `${classNameIfDefined(nextClassName)}${
isNextDisabled ? ` ${classNameIfDefined(disabledClassName)}` : ''
}`;
const previousLinkClasses = `${classNameIfDefined(previousLinkClassName)}${
isPreviousDisabled ? ` ${classNameIfDefined(disabledLinkClassName)}` : ''
}`;
const nextLinkClasses = `${classNameIfDefined(nextLinkClassName)}${
isNextDisabled ? ` ${classNameIfDefined(disabledLinkClassName)}` : ''
}`;
const previousAriaDisabled = isPreviousDisabled ? 'true' : 'false';
const nextAriaDisabled = isNextDisabled ? 'true' : 'false';
return (
<ul
className={className || containerClassName}
role="navigation"
aria-label="Pagination"
>
<li className={previousClasses}>
<a
className={previousLinkClasses}
href={this.getElementHref(selected - 1)}
tabIndex={isPreviousDisabled ? '-1' : '0'}
role="button"
onKeyPress={this.handlePreviousPage}
aria-disabled={previousAriaDisabled}
aria-label={previousAriaLabel}
rel={prevRel}
{...this.getEventListener(this.handlePreviousPage)}
>
{previousLabel}
</a>
</li>
{this.pagination()}
<li className={nextClasses}>
<a
className={nextLinkClasses}
href={this.getElementHref(selected + 1)}
tabIndex={isNextDisabled ? '-1' : '0'}
role="button"
onKeyPress={this.handleNextPage}
aria-disabled={nextAriaDisabled}
aria-label={nextAriaLabel}
rel={nextRel}
{...this.getEventListener(this.handleNextPage)}
>
{nextLabel}
</a>
</li>
</ul>
);
}
}