123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459 |
- /* eslint-disable max-len */
- /* eslint-disable jsx-a11y/no-noninteractive-element-to-interactive-role */
- import React from 'react';
- import classNames from 'classnames';
- import PropTypes from 'prop-types';
- import { FixedSizeList as List } from 'react-window';
- import { noop } from 'lodash';
- import PaginationFoundation, {
- AdapterPageList,
- KeyDownHandler,
- PageList, PaginationAdapter
- } from '@douyinfe/semi-foundation/pagination/foundation';
- import { cssClasses, numbers } from '@douyinfe/semi-foundation/pagination/constants';
- import '@douyinfe/semi-foundation/pagination/pagination.scss';
- import { numbers as popoverNumbers } from '@douyinfe/semi-foundation/popover/constants';
- import { IconChevronLeft, IconChevronRight } from '@douyinfe/semi-icons';
- import warning from '@douyinfe/semi-foundation/utils/warning';
- import ConfigContext from '../configProvider/context';
- import LocaleConsumer from '../locale/localeConsumer';
- import { Locale } from '../locale/interface';
- import Select from '../select/index';
- import InputNumber from '../inputNumber/index';
- import BaseComponent from '../_base/baseComponent';
- import Popover from '../popover/index';
- import { Position } from '../tooltip';
- const prefixCls = cssClasses.PREFIX;
- const { Option } = Select;
- export interface PaginationProps {
- total?: number;
- showTotal?: boolean;
- pageSize?: number;
- pageSizeOpts?: Array<number>;
- size?: 'small' | 'default';
- currentPage?: number;
- defaultCurrentPage?: number;
- onPageChange?: (currentPage: number) => void;
- onPageSizeChange?: (newPageSize: number) => void;
- onChange?: (currentPage: number, pageSize: number) => void;
- prevText?: React.ReactNode;
- nextText?: React.ReactNode;
- showSizeChanger?: boolean;
- showQuickJumper?: boolean;
- popoverZIndex?: number;
- popoverPosition?: PopoverPosition;
- style?: React.CSSProperties;
- className?: string;
- hideOnSinglePage?: boolean;
- hoverShowPageSelect?: boolean;
- }
- export interface PaginationState {
- total: number;
- showTotal: boolean;
- currentPage: number;
- pageSize: number;
- pageList: PageList;
- prevDisabled: boolean;
- quickJumpPage: string | number;
- nextDisabled: boolean;
- restLeftPageList: number[];
- restRightPageList: number[];
- }
- export type PaginationLocale = Locale['Pagination'];
- export type PopoverPosition = Position;
- export { PageList };
- export default class Pagination extends BaseComponent<PaginationProps, PaginationState> {
- static contextType = ConfigContext;
- static propTypes = {
- total: PropTypes.number,
- showTotal: PropTypes.bool,
- pageSize: PropTypes.number,
- pageSizeOpts: PropTypes.array,
- size: PropTypes.string,
- currentPage: PropTypes.number,
- defaultCurrentPage: PropTypes.number,
- onPageChange: PropTypes.func,
- onPageSizeChange: PropTypes.func,
- onChange: PropTypes.func,
- prevText: PropTypes.node,
- nextText: PropTypes.node,
- showSizeChanger: PropTypes.bool,
- popoverZIndex: PropTypes.number,
- popoverPosition: PropTypes.string,
- style: PropTypes.object,
- className: PropTypes.string,
- hideOnSinglePage: PropTypes.bool,
- hoverShowPageSelect: PropTypes.bool,
- showQuickJumper: PropTypes.bool,
- };
- static defaultProps = {
- total: 1,
- popoverZIndex: popoverNumbers.DEFAULT_Z_INDEX,
- showTotal: false,
- pageSize: null as null,
- pageSizeOpts: numbers.PAGE_SIZE_OPTION,
- defaultCurrentPage: 1,
- size: 'default',
- onPageChange: noop,
- onPageSizeChange: noop,
- onChange: noop,
- showSizeChanger: false,
- className: '',
- hideOnSinglePage: false,
- showQuickJumper: false,
- };
- constructor(props: PaginationProps) {
- super(props);
- this.state = {
- total: props.total,
- showTotal: props.showTotal,
- currentPage: props.currentPage || props.defaultCurrentPage,
- pageSize: props.pageSize || props.pageSizeOpts[0] || numbers.DEFAULT_PAGE_SIZE, // Use pageSize first, use the first of pageSizeOpts when not, use the default value when none
- pageList: [],
- prevDisabled: false,
- nextDisabled: false,
- restLeftPageList: [],
- restRightPageList: [],
- quickJumpPage: '',
- };
- this.foundation = new PaginationFoundation(this.adapter);
- this.renderDefaultPage = this.renderDefaultPage.bind(this);
- this.renderSmallPage = this.renderSmallPage.bind(this);
- warning(
- Boolean(props.showSizeChanger && props.hideOnSinglePage),
- '[Semi Pagination] You should not use showSizeChanger and hideOnSinglePage in ths same time. At this time, hideOnSinglePage no longer takes effect, otherwise there may be a problem that the switch entry disappears'
- );
- }
- get adapter(): PaginationAdapter<PaginationProps, PaginationState> {
- return {
- ...super.adapter,
- setPageList: (pageListState: AdapterPageList) => {
- const { pageList, restLeftPageList, restRightPageList } = pageListState;
- this.setState({ pageList, restLeftPageList, restRightPageList });
- },
- setDisabled: (prevIsDisabled: boolean, nextIsDisabled: boolean) => {
- this.setState({ prevDisabled: prevIsDisabled, nextDisabled: nextIsDisabled });
- },
- updateTotal: (total: number) => this.setState({ total }),
- updatePageSize: (pageSize: number) => this.setState({ pageSize }),
- updateQuickJumpPage: (quickJumpPage: string | number) => this.setState({ quickJumpPage }),
- // updateRestPageList: () => {},
- setCurrentPage: (pageIndex: number) => {
- this.setState({ currentPage: pageIndex });
- },
- registerKeyDownHandler: (handler: KeyDownHandler) => {
- document.addEventListener('keydown', handler);
- },
- unregisterKeyDownHandler: (handler: KeyDownHandler) => {
- document.removeEventListener('keydown', handler);
- },
- notifyPageChange: (pageIndex: number) => {
- this.props.onPageChange(pageIndex);
- },
- notifyPageSizeChange: (pageSize: number) => {
- this.props.onPageSizeChange(pageSize);
- },
- notifyChange: (pageIndex: number, pageSize: number) => {
- this.props.onChange(pageIndex, pageSize);
- }
- };
- }
- componentDidMount() {
- this.foundation.init();
- }
- componentWillUnmount() {
- this.foundation.destroy();
- }
- componentDidUpdate(prevProps: PaginationProps) {
- const pagerProps = {
- currentPage: this.props.currentPage,
- total: this.props.total,
- pageSize: this.props.pageSize,
- };
- let pagerHasChanged = false;
- if (prevProps.currentPage !== this.props.currentPage) {
- pagerHasChanged = true;
- // this.foundation.updatePage(this.props.currentPage);
- }
- if (prevProps.total !== this.props.total) {
- pagerHasChanged = true;
- }
- if (prevProps.pageSize !== this.props.pageSize) {
- pagerHasChanged = true;
- }
- if (pagerHasChanged) {
- this.foundation.updatePage(pagerProps.currentPage, pagerProps.total, pagerProps.pageSize);
- }
- }
- renderPrevBtn() {
- const { prevText } = this.props;
- const { prevDisabled } = this.state;
- const preClassName = classNames({
- [`${prefixCls}-item`]: true,
- [`${prefixCls}-prev`]: true,
- [`${prefixCls}-item-disabled`]: prevDisabled,
- });
- return (
- <li role="button" aria-disabled={prevDisabled ? true : false} aria-label="Previous" onClick={e => !prevDisabled && this.foundation.goPrev(e)} className={preClassName}>
- {prevText || <IconChevronLeft size="large" />}
- </li>
- );
- }
- renderNextBtn() {
- const { nextText } = this.props;
- const { nextDisabled } = this.state;
- const nextClassName = classNames({
- [`${prefixCls}-item`]: true,
- [`${prefixCls}-item-disabled`]: nextDisabled,
- [`${prefixCls}-next`]: true,
- });
- return (
- <li role="button" aria-disabled={nextDisabled ? true : false} aria-label="Next" onClick={e => !nextDisabled && this.foundation.goNext(e)} className={nextClassName}>
- {nextText || <IconChevronRight size="large" />}
- </li>
- );
- }
- renderPageSizeSwitch(locale: PaginationLocale) {
- // rtl modify the default position
- const { direction } = this.context;
- const defaultPopoverPosition = direction === 'rtl' ? 'bottomRight' : 'bottomLeft';
- const { showSizeChanger, popoverPosition = defaultPopoverPosition } = this.props;
- const { pageSize } = this.state;
- const switchCls = classNames(`${prefixCls}-switch`);
- if (!showSizeChanger) {
- return null;
- }
- const pageSizeText = locale.pageSize;
- const newPageSizeOpts = this.foundation.pageSizeInOpts();
- const options = newPageSizeOpts.map((size: number) => (
- <Option value={size} key={size}>
- <span>
- {`${size} `}
- {pageSizeText}
- </span>
- </Option>
- ));
- return (
- <div className={switchCls}>
- <Select
- aria-label="Page size selector"
- onChange={newPageSize => this.foundation.changePageSize(newPageSize)}
- value={pageSize}
- key={pageSizeText}
- position={popoverPosition || 'bottomRight'}
- clickToHide
- dropdownClassName={`${prefixCls}-select-dropdown`}
- >
- {options}
- </Select>
- </div>
- );
- }
- renderQuickJump(locale: PaginationLocale) {
- const { showQuickJumper } = this.props;
- const { quickJumpPage, total, pageSize } = this.state;
- if (!showQuickJumper) {
- return null;
- }
- const totalPageNum = this.foundation._getTotalPageNumber(total, pageSize);
- const isDisabled = totalPageNum === 1;
- const quickJumpCls = classNames({
- [`${prefixCls}-quickjump`]: true,
- [`${prefixCls}-quickjump-disabled`]: isDisabled
- });
- return (
- <div className={quickJumpCls}>
- <span>{locale.jumpTo}</span>
- <InputNumber
- value={quickJumpPage}
- className={`${prefixCls}-quickjump-input-number`}
- hideButtons
- disabled={isDisabled}
- onBlur={(e: React.FocusEvent) => this.foundation.handleQuickJumpBlur()}
- onEnterPress={(e: React.KeyboardEvent) => this.foundation.handleQuickJumpEnterPress((e.target as any).value)}
- onChange={(v: string | number) => this.foundation.handleQuickJumpNumberChange(v)}
- />
- <span>{locale.page}</span>
- </div>
- );
- }
- renderPageList() {
- const {
- pageList,
- currentPage,
- restLeftPageList,
- restRightPageList,
- } = this.state;
- const { popoverPosition, popoverZIndex } = this.props;
- return pageList.map((page, i) => {
- const pageListClassName = classNames(`${prefixCls}-item`, {
- [`${prefixCls}-item-active`]: currentPage === page,
- // [`${prefixCls}-item-rest-opening`]: (i < 3 && isLeftRestHover && page ==='...') || (i > 3 && isRightRestHover && page === '...')
- });
- const pageEl = (
- <li
- key={`${page}${i}`}
- onClick={() => this.foundation.goPage(page, i)}
- className={pageListClassName}
- aria-label={page === '...' ? 'More' : `Page ${page}`}
- aria-current={currentPage === page ? "page" : false}
- >
- {page}
- </li>
- );
- if (page === '...') {
- let content;
- i < 3 ? (content = restLeftPageList) : (content = restRightPageList);
- return (
- <Popover
- trigger="hover"
- // onVisibleChange={visible=>this.handleRestHover(visible, i < 3 ? 'left' : 'right')}
- content={this.renderRestPageList(content)}
- key={`${page}${i}`}
- position={popoverPosition}
- zIndex={popoverZIndex}
- >
- {pageEl}
- </Popover>
- );
- }
- return pageEl;
- });
- }
- renderRestPageList(restList: ('...' | number)[]) {
- // The number of pages may be tens of thousands, here is virtualized with the help of react-window
- const { direction } = this.context;
- const className = classNames(`${prefixCls}-rest-item`);
- const count = restList.length;
- const row = (item: { index: number; style: React.CSSProperties }) => {
- const { index, style } = item;
- const page = restList[index];
- return (
- <div
- role="listitem"
- key={`${page}${index}`}
- className={className}
- onClick={() => this.foundation.goPage(page, index)}
- style={style}
- aria-label={`${page}`}
- >
- {page}
- </div>
- );
- };
- const itemHeight = 32;
- const listHeight = count >= 5 ? itemHeight * 5 : itemHeight * count;
- return (
- <List
- className={`${prefixCls}-rest-list`}
- itemData={restList}
- itemSize={itemHeight}
- width={78}
- itemCount={count}
- height={listHeight}
- style={{ direction }}
- >
- {row}
- </List>
- );
- }
- renderSmallPage(locale: PaginationLocale) {
- const { className, style, hideOnSinglePage, hoverShowPageSelect, showSizeChanger } = this.props;
- const paginationCls = classNames(`${prefixCls}-small`, prefixCls, className);
- const { currentPage, total, pageSize } = this.state;
- const totalPageNum = Math.ceil(total / pageSize);
- if (totalPageNum < 2 && hideOnSinglePage && !showSizeChanger) {
- return null;
- }
- const pageNumbers = Array.from({ length: Math.ceil(total / pageSize) }, (v, i) => i + 1);
- const pageList = this.renderRestPageList(pageNumbers);
- const page = (<div className={`${prefixCls}-item ${prefixCls}-item-small`}>{currentPage}/{totalPageNum} </div>);
- return (
- <div className={paginationCls} style={style}>
- {this.renderPrevBtn()}
- {
- hoverShowPageSelect ? (
- <Popover
- content={pageList}
- >
- {page}
- </Popover>
- ) : page
- }
- {this.renderNextBtn()}
- {this.renderQuickJump(locale)}
- </div>
- );
- }
- renderDefaultPage(locale: PaginationLocale) {
- const { total, pageSize } = this.state;
- const { showTotal, className, style, hideOnSinglePage, showSizeChanger } = this.props;
- const paginationCls = classNames(className, `${prefixCls}`);
- const showTotalCls = `${prefixCls}-total`;
- const totalPageNum = Math.ceil(total / pageSize);
- if (totalPageNum < 2 && hideOnSinglePage && !showSizeChanger) {
- return null;
- }
- return (
- <ul className={paginationCls} style={style}>
- {showTotal ? (
- <span className={showTotalCls}>
- {locale.total}
- {` ${Math.ceil(total / pageSize)} `}
- {locale.page}
- </span>
- ) : null}
- {this.renderPrevBtn()}
- {this.renderPageList()}
- {this.renderNextBtn()}
- {this.renderPageSizeSwitch(locale)}
- {this.renderQuickJump(locale)}
- </ul>
- );
- }
- render() {
- const { size } = this.props;
- return (
- <LocaleConsumer componentName="Pagination">
- {
- (locale: PaginationLocale) => (
- size === 'small' ? this.renderSmallPage(locale) : this.renderDefaultPage(locale)
- )
- }
- </LocaleConsumer>
- );
- }
- }
|