/* 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, { ContextValue } 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; 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; disabled?: 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[]; allPageNumbers: number[] } export type PaginationLocale = Locale['Pagination']; export type PopoverPosition = Position; export type { PageList }; export default class Pagination extends BaseComponent { 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, disabled: 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, disabled: false, }; constructor(props: PaginationProps) { super(props); const total = props.total; const 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 const shouldFillAllNumber = props.size === 'small' && props.hoverShowPageSelect && !props.disabled; this.state = { total, showTotal: props.showTotal, currentPage: props.currentPage || props.defaultCurrentPage, pageSize, pageList: [], prevDisabled: false, nextDisabled: false, restLeftPageList: [], restRightPageList: [], quickJumpPage: '', allPageNumbers: shouldFillAllNumber ? Array.from({ length: Math.ceil(total / pageSize) }, (v, i) => i + 1) : [], // only need to count in smallPage mode, when props.size = small }; 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' ); } context: ContextValue; get adapter(): PaginationAdapter { 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 }), updateAllPageNumbers: (allPageNumbers: number[]) => this.setState({ allPageNumbers }), 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; let allPageNumberNeedUpdate = false; if (prevProps.currentPage !== this.props.currentPage) { pagerHasChanged = true; // this.foundation.updatePage(this.props.currentPage); } if (prevProps.total !== this.props.total) { pagerHasChanged = true; allPageNumberNeedUpdate = true; } if (prevProps.pageSize !== this.props.pageSize) { pagerHasChanged = true; allPageNumberNeedUpdate = true; } if (pagerHasChanged) { this.foundation.updatePage(pagerProps.currentPage, pagerProps.total, pagerProps.pageSize); } if (allPageNumberNeedUpdate) { this.foundation.updateAllPageNumbers(pagerProps.total, pagerProps.pageSize); } } renderPrevBtn() { const { prevText, disabled } = this.props; const { prevDisabled } = this.state; const isDisabled = prevDisabled || disabled; const preClassName = classNames({ [`${prefixCls}-item`]: true, [`${prefixCls}-prev`]: true, [`${prefixCls}-item-disabled`]: isDisabled, }); return (
  • !isDisabled && this.foundation.goPrev(e)} className={preClassName} x-semi-prop="prevText" > {prevText || }
  • ); } renderNextBtn() { const { nextText, disabled } = this.props; const { nextDisabled } = this.state; const isDisabled = nextDisabled || disabled; const nextClassName = classNames({ [`${prefixCls}-item`]: true, [`${prefixCls}-item-disabled`]: isDisabled, [`${prefixCls}-next`]: true, }); return (
  • !isDisabled && this.foundation.goNext(e)} className={nextClassName} x-semi-prop="nextText" > {nextText || }
  • ); } renderPageSizeSwitch(locale: PaginationLocale) { // rtl modify the default position const { direction } = this.context; const defaultPopoverPosition = direction === 'rtl' ? 'bottomRight' : 'bottomLeft'; const { showSizeChanger, popoverPosition = defaultPopoverPosition, disabled, popoverZIndex } = this.props; const { pageSize } = this.state; const switchCls = classNames(`${prefixCls}-switch`); if (!showSizeChanger) { return null; } const newPageSizeOpts = this.foundation.pageSizeInOpts(); const pageSizeToken = locale.pageSize; // Display pageSize in a specific language format order const options = newPageSizeOpts.map((size: number) => ( )); return (
    ); } renderQuickJump(locale: PaginationLocale) { const { showQuickJumper, disabled } = this.props; const { quickJumpPage, total, pageSize } = this.state; if (!showQuickJumper) { return null; } const totalPageNum = this.foundation._getTotalPageNumber(total, pageSize); const isDisabled = (totalPageNum === 1) || disabled; const quickJumpCls = classNames({ [`${prefixCls}-quickjump`]: true, [`${prefixCls}-quickjump-disabled`]: isDisabled }); return (
    {locale.jumpTo} this.foundation.handleQuickJumpBlur()} onEnterPress={(e: React.KeyboardEvent) => this.foundation.handleQuickJumpEnterPress((e.target as any).value)} onChange={(v: string | number) => this.foundation.handleQuickJumpNumberChange(v)} /> {locale.page}
    ); } renderPageList() { const { pageList, currentPage, restLeftPageList, restRightPageList, } = this.state; const { popoverPosition, popoverZIndex, disabled } = this.props; return pageList.map((page, i) => { const pageListClassName = classNames(`${prefixCls}-item`, { [`${prefixCls}-item-active`]: currentPage === page, [`${prefixCls}-item-all-disabled`]: disabled, [`${prefixCls}-item-all-disabled-active`]: currentPage === page && disabled, // [`${prefixCls}-item-rest-opening`]: (i < 3 && isLeftRestHover && page ==='...') || (i > 3 && isRightRestHover && page === '...') }); const pageEl = (
  • !disabled && this.foundation.goPage(page, i)} className={pageListClassName} aria-label={page === '...' ? 'More' : `Page ${page}`} aria-current={currentPage === page ? "page" : false} > {page}
  • ); if (page === '...' && !disabled) { let content; i < 3 ? (content = restLeftPageList) : (content = restRightPageList); return ( this.handleRestHover(visible, i < 3 ? 'left' : 'right')} content={this.renderRestPageList(content)} key={`${page}${i}`} position={popoverPosition} zIndex={popoverZIndex} > {pageEl} ); } 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 (
    this.foundation.goPage(page, index)} style={style} aria-label={`${page}`} > {page}
    ); }; const itemHeight = 32; const listHeight = count >= 5 ? itemHeight * 5 : itemHeight * count; return ( // @ts-ignore skip type check cause react-window not update with @types/react 18 {row} ); } renderSmallPageSelect(content: React.ReactNode) { const allPageNumbers = this.state.allPageNumbers; const pageList = this.renderRestPageList(allPageNumbers); return ( {content} ); } renderSmallPage(locale: PaginationLocale) { const { className, style, hideOnSinglePage, hoverShowPageSelect, showSizeChanger, disabled, ...rest } = this.props; const paginationCls = classNames(`${prefixCls}-small`, prefixCls, className, { [`${prefixCls}-disabled`]: disabled }); const { currentPage, total, pageSize } = this.state; const totalPageNum = Math.ceil(total / pageSize); if (totalPageNum < 2 && hideOnSinglePage && !showSizeChanger) { return null; } const pageCls = classNames({ [`${prefixCls}-item`]: true, [`${prefixCls}-item-small`]: true, [`${prefixCls}-item-all-disabled`]: disabled, }); const content = (
    {currentPage}/{totalPageNum}
    ); return (
    {this.renderPrevBtn()} { (hoverShowPageSelect && !disabled) ? this.renderSmallPageSelect(content) : content } {this.renderNextBtn()} {this.renderQuickJump(locale)}
    ); } renderDefaultPage(locale: PaginationLocale) { const { total, pageSize } = this.state; const { showTotal, className, style, hideOnSinglePage, showSizeChanger, disabled, ...rest } = this.props; const paginationCls = classNames(className, `${prefixCls}`, { [`${prefixCls}-disabled`]: disabled }); const showTotalCls = `${prefixCls}-total`; const totalPageNum = Math.ceil(total / pageSize); if (totalPageNum < 2 && hideOnSinglePage && !showSizeChanger) { return null; } const totalNum = Math.ceil(total / pageSize); const totalToken = locale.total.replace('${total}', totalNum.toString()); return (
      {showTotal ? ( {totalToken} ) : null} {this.renderPrevBtn()} {this.renderPageList()} {this.renderNextBtn()} {this.renderPageSizeSwitch(locale)} {this.renderQuickJump(locale)}
    ); } render() { const { size } = this.props; return ( { (locale: PaginationLocale) => ( size === 'small' ? this.renderSmallPage(locale) : this.renderDefaultPage(locale) ) } ); } }