1
0

index.tsx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  1. /* eslint-disable max-len */
  2. /* eslint-disable jsx-a11y/no-noninteractive-element-to-interactive-role */
  3. import React from 'react';
  4. import classNames from 'classnames';
  5. import PropTypes from 'prop-types';
  6. import { FixedSizeList as List } from 'react-window';
  7. import { noop } from 'lodash';
  8. import PaginationFoundation, {
  9. AdapterPageList,
  10. KeyDownHandler,
  11. PageList, PaginationAdapter
  12. } from '@douyinfe/semi-foundation/pagination/foundation';
  13. import { cssClasses, numbers } from '@douyinfe/semi-foundation/pagination/constants';
  14. import '@douyinfe/semi-foundation/pagination/pagination.scss';
  15. import { numbers as popoverNumbers } from '@douyinfe/semi-foundation/popover/constants';
  16. import { IconChevronLeft, IconChevronRight } from '@douyinfe/semi-icons';
  17. import warning from '@douyinfe/semi-foundation/utils/warning';
  18. import ConfigContext, { ContextValue } from '../configProvider/context';
  19. import LocaleConsumer from '../locale/localeConsumer';
  20. import { Locale } from '../locale/interface';
  21. import Select from '../select/index';
  22. import InputNumber from '../inputNumber/index';
  23. import BaseComponent from '../_base/baseComponent';
  24. import Popover from '../popover/index';
  25. import { Position } from '../tooltip';
  26. const prefixCls = cssClasses.PREFIX;
  27. const { Option } = Select;
  28. export interface PaginationProps {
  29. total?: number;
  30. showTotal?: boolean;
  31. pageSize?: number;
  32. pageSizeOpts?: Array<number>;
  33. size?: 'small' | 'default';
  34. currentPage?: number;
  35. defaultCurrentPage?: number;
  36. onPageChange?: (currentPage: number) => void;
  37. onPageSizeChange?: (newPageSize: number) => void;
  38. onChange?: (currentPage: number, pageSize: number) => void;
  39. prevText?: React.ReactNode;
  40. nextText?: React.ReactNode;
  41. showSizeChanger?: boolean;
  42. showQuickJumper?: boolean;
  43. popoverZIndex?: number;
  44. popoverPosition?: PopoverPosition;
  45. style?: React.CSSProperties;
  46. className?: string;
  47. hideOnSinglePage?: boolean;
  48. hoverShowPageSelect?: boolean;
  49. }
  50. export interface PaginationState {
  51. total: number;
  52. showTotal: boolean;
  53. currentPage: number;
  54. pageSize: number;
  55. pageList: PageList;
  56. prevDisabled: boolean;
  57. quickJumpPage: string | number;
  58. nextDisabled: boolean;
  59. restLeftPageList: number[];
  60. restRightPageList: number[];
  61. }
  62. export type PaginationLocale = Locale['Pagination'];
  63. export type PopoverPosition = Position;
  64. export { PageList };
  65. export default class Pagination extends BaseComponent<PaginationProps, PaginationState> {
  66. static contextType = ConfigContext;
  67. static propTypes = {
  68. total: PropTypes.number,
  69. showTotal: PropTypes.bool,
  70. pageSize: PropTypes.number,
  71. pageSizeOpts: PropTypes.array,
  72. size: PropTypes.string,
  73. currentPage: PropTypes.number,
  74. defaultCurrentPage: PropTypes.number,
  75. onPageChange: PropTypes.func,
  76. onPageSizeChange: PropTypes.func,
  77. onChange: PropTypes.func,
  78. prevText: PropTypes.node,
  79. nextText: PropTypes.node,
  80. showSizeChanger: PropTypes.bool,
  81. popoverZIndex: PropTypes.number,
  82. popoverPosition: PropTypes.string,
  83. style: PropTypes.object,
  84. className: PropTypes.string,
  85. hideOnSinglePage: PropTypes.bool,
  86. hoverShowPageSelect: PropTypes.bool,
  87. showQuickJumper: PropTypes.bool,
  88. };
  89. static defaultProps = {
  90. total: 1,
  91. popoverZIndex: popoverNumbers.DEFAULT_Z_INDEX,
  92. showTotal: false,
  93. pageSize: null as null,
  94. pageSizeOpts: numbers.PAGE_SIZE_OPTION,
  95. defaultCurrentPage: 1,
  96. size: 'default',
  97. onPageChange: noop,
  98. onPageSizeChange: noop,
  99. onChange: noop,
  100. showSizeChanger: false,
  101. className: '',
  102. hideOnSinglePage: false,
  103. showQuickJumper: false,
  104. };
  105. constructor(props: PaginationProps) {
  106. super(props);
  107. this.state = {
  108. total: props.total,
  109. showTotal: props.showTotal,
  110. currentPage: props.currentPage || props.defaultCurrentPage,
  111. 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
  112. pageList: [],
  113. prevDisabled: false,
  114. nextDisabled: false,
  115. restLeftPageList: [],
  116. restRightPageList: [],
  117. quickJumpPage: '',
  118. };
  119. this.foundation = new PaginationFoundation(this.adapter);
  120. this.renderDefaultPage = this.renderDefaultPage.bind(this);
  121. this.renderSmallPage = this.renderSmallPage.bind(this);
  122. warning(
  123. Boolean(props.showSizeChanger && props.hideOnSinglePage),
  124. '[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'
  125. );
  126. }
  127. context: ContextValue;
  128. get adapter(): PaginationAdapter<PaginationProps, PaginationState> {
  129. return {
  130. ...super.adapter,
  131. setPageList: (pageListState: AdapterPageList) => {
  132. const { pageList, restLeftPageList, restRightPageList } = pageListState;
  133. this.setState({ pageList, restLeftPageList, restRightPageList });
  134. },
  135. setDisabled: (prevIsDisabled: boolean, nextIsDisabled: boolean) => {
  136. this.setState({ prevDisabled: prevIsDisabled, nextDisabled: nextIsDisabled });
  137. },
  138. updateTotal: (total: number) => this.setState({ total }),
  139. updatePageSize: (pageSize: number) => this.setState({ pageSize }),
  140. updateQuickJumpPage: (quickJumpPage: string | number) => this.setState({ quickJumpPage }),
  141. // updateRestPageList: () => {},
  142. setCurrentPage: (pageIndex: number) => {
  143. this.setState({ currentPage: pageIndex });
  144. },
  145. registerKeyDownHandler: (handler: KeyDownHandler) => {
  146. document.addEventListener('keydown', handler);
  147. },
  148. unregisterKeyDownHandler: (handler: KeyDownHandler) => {
  149. document.removeEventListener('keydown', handler);
  150. },
  151. notifyPageChange: (pageIndex: number) => {
  152. this.props.onPageChange(pageIndex);
  153. },
  154. notifyPageSizeChange: (pageSize: number) => {
  155. this.props.onPageSizeChange(pageSize);
  156. },
  157. notifyChange: (pageIndex: number, pageSize: number) => {
  158. this.props.onChange(pageIndex, pageSize);
  159. }
  160. };
  161. }
  162. componentDidMount() {
  163. this.foundation.init();
  164. }
  165. componentWillUnmount() {
  166. this.foundation.destroy();
  167. }
  168. componentDidUpdate(prevProps: PaginationProps) {
  169. const pagerProps = {
  170. currentPage: this.props.currentPage,
  171. total: this.props.total,
  172. pageSize: this.props.pageSize,
  173. };
  174. let pagerHasChanged = false;
  175. if (prevProps.currentPage !== this.props.currentPage) {
  176. pagerHasChanged = true;
  177. // this.foundation.updatePage(this.props.currentPage);
  178. }
  179. if (prevProps.total !== this.props.total) {
  180. pagerHasChanged = true;
  181. }
  182. if (prevProps.pageSize !== this.props.pageSize) {
  183. pagerHasChanged = true;
  184. }
  185. if (pagerHasChanged) {
  186. this.foundation.updatePage(pagerProps.currentPage, pagerProps.total, pagerProps.pageSize);
  187. }
  188. }
  189. renderPrevBtn() {
  190. const { prevText } = this.props;
  191. const { prevDisabled } = this.state;
  192. const preClassName = classNames({
  193. [`${prefixCls}-item`]: true,
  194. [`${prefixCls}-prev`]: true,
  195. [`${prefixCls}-item-disabled`]: prevDisabled,
  196. });
  197. return (
  198. <li role="button" aria-disabled={prevDisabled ? true : false} aria-label="Previous" onClick={e => !prevDisabled && this.foundation.goPrev(e)} className={preClassName}>
  199. {prevText || <IconChevronLeft size="large" />}
  200. </li>
  201. );
  202. }
  203. renderNextBtn() {
  204. const { nextText } = this.props;
  205. const { nextDisabled } = this.state;
  206. const nextClassName = classNames({
  207. [`${prefixCls}-item`]: true,
  208. [`${prefixCls}-item-disabled`]: nextDisabled,
  209. [`${prefixCls}-next`]: true,
  210. });
  211. return (
  212. <li role="button" aria-disabled={nextDisabled ? true : false} aria-label="Next" onClick={e => !nextDisabled && this.foundation.goNext(e)} className={nextClassName}>
  213. {nextText || <IconChevronRight size="large" />}
  214. </li>
  215. );
  216. }
  217. renderPageSizeSwitch(locale: PaginationLocale) {
  218. // rtl modify the default position
  219. const { direction } = this.context;
  220. const defaultPopoverPosition = direction === 'rtl' ? 'bottomRight' : 'bottomLeft';
  221. const { showSizeChanger, popoverPosition = defaultPopoverPosition } = this.props;
  222. const { pageSize } = this.state;
  223. const switchCls = classNames(`${prefixCls}-switch`);
  224. if (!showSizeChanger) {
  225. return null;
  226. }
  227. const pageSizeText = locale.pageSize;
  228. const newPageSizeOpts = this.foundation.pageSizeInOpts();
  229. const options = newPageSizeOpts.map((size: number) => (
  230. <Option value={size} key={size}>
  231. <span>
  232. {`${size} `}
  233. {pageSizeText}
  234. </span>
  235. </Option>
  236. ));
  237. return (
  238. <div className={switchCls}>
  239. <Select
  240. aria-label="Page size selector"
  241. onChange={newPageSize => this.foundation.changePageSize(newPageSize)}
  242. value={pageSize}
  243. key={pageSizeText}
  244. position={popoverPosition || 'bottomRight'}
  245. clickToHide
  246. dropdownClassName={`${prefixCls}-select-dropdown`}
  247. >
  248. {options}
  249. </Select>
  250. </div>
  251. );
  252. }
  253. renderQuickJump(locale: PaginationLocale) {
  254. const { showQuickJumper } = this.props;
  255. const { quickJumpPage, total, pageSize } = this.state;
  256. if (!showQuickJumper) {
  257. return null;
  258. }
  259. const totalPageNum = this.foundation._getTotalPageNumber(total, pageSize);
  260. const isDisabled = totalPageNum === 1;
  261. const quickJumpCls = classNames({
  262. [`${prefixCls}-quickjump`]: true,
  263. [`${prefixCls}-quickjump-disabled`]: isDisabled
  264. });
  265. return (
  266. <div className={quickJumpCls}>
  267. <span>{locale.jumpTo}</span>
  268. <InputNumber
  269. value={quickJumpPage}
  270. className={`${prefixCls}-quickjump-input-number`}
  271. hideButtons
  272. disabled={isDisabled}
  273. onBlur={(e: React.FocusEvent) => this.foundation.handleQuickJumpBlur()}
  274. onEnterPress={(e: React.KeyboardEvent) => this.foundation.handleQuickJumpEnterPress((e.target as any).value)}
  275. onChange={(v: string | number) => this.foundation.handleQuickJumpNumberChange(v)}
  276. />
  277. <span>{locale.page}</span>
  278. </div>
  279. );
  280. }
  281. renderPageList() {
  282. const {
  283. pageList,
  284. currentPage,
  285. restLeftPageList,
  286. restRightPageList,
  287. } = this.state;
  288. const { popoverPosition, popoverZIndex } = this.props;
  289. return pageList.map((page, i) => {
  290. const pageListClassName = classNames(`${prefixCls}-item`, {
  291. [`${prefixCls}-item-active`]: currentPage === page,
  292. // [`${prefixCls}-item-rest-opening`]: (i < 3 && isLeftRestHover && page ==='...') || (i > 3 && isRightRestHover && page === '...')
  293. });
  294. const pageEl = (
  295. <li
  296. key={`${page}${i}`}
  297. onClick={() => this.foundation.goPage(page, i)}
  298. className={pageListClassName}
  299. aria-label={page === '...' ? 'More' : `Page ${page}`}
  300. aria-current={currentPage === page ? "page" : false}
  301. >
  302. {page}
  303. </li>
  304. );
  305. if (page === '...') {
  306. let content;
  307. i < 3 ? (content = restLeftPageList) : (content = restRightPageList);
  308. return (
  309. <Popover
  310. trigger="hover"
  311. // onVisibleChange={visible=>this.handleRestHover(visible, i < 3 ? 'left' : 'right')}
  312. content={this.renderRestPageList(content)}
  313. key={`${page}${i}`}
  314. position={popoverPosition}
  315. zIndex={popoverZIndex}
  316. >
  317. {pageEl}
  318. </Popover>
  319. );
  320. }
  321. return pageEl;
  322. });
  323. }
  324. renderRestPageList(restList: ('...' | number)[]) {
  325. // The number of pages may be tens of thousands, here is virtualized with the help of react-window
  326. const { direction } = this.context;
  327. const className = classNames(`${prefixCls}-rest-item`);
  328. const count = restList.length;
  329. const row = (item: { index: number; style: React.CSSProperties }) => {
  330. const { index, style } = item;
  331. const page = restList[index];
  332. return (
  333. <div
  334. role="listitem"
  335. key={`${page}${index}`}
  336. className={className}
  337. onClick={() => this.foundation.goPage(page, index)}
  338. style={style}
  339. aria-label={`${page}`}
  340. >
  341. {page}
  342. </div>
  343. );
  344. };
  345. const itemHeight = 32;
  346. const listHeight = count >= 5 ? itemHeight * 5 : itemHeight * count;
  347. return (
  348. // @ts-ignore skip type check cause react-window not update with @types/react 18
  349. <List
  350. className={`${prefixCls}-rest-list`}
  351. itemData={restList}
  352. itemSize={itemHeight}
  353. width={78}
  354. itemCount={count}
  355. height={listHeight}
  356. style={{ direction }}
  357. >
  358. {row}
  359. </List>
  360. );
  361. }
  362. renderSmallPage(locale: PaginationLocale) {
  363. const { className, style, hideOnSinglePage, hoverShowPageSelect, showSizeChanger } = this.props;
  364. const paginationCls = classNames(`${prefixCls}-small`, prefixCls, className);
  365. const { currentPage, total, pageSize } = this.state;
  366. const totalPageNum = Math.ceil(total / pageSize);
  367. if (totalPageNum < 2 && hideOnSinglePage && !showSizeChanger) {
  368. return null;
  369. }
  370. const pageNumbers = Array.from({ length: Math.ceil(total / pageSize) }, (v, i) => i + 1);
  371. const pageList = this.renderRestPageList(pageNumbers);
  372. const page = (<div className={`${prefixCls}-item ${prefixCls}-item-small`}>{currentPage}/{totalPageNum} </div>);
  373. return (
  374. <div className={paginationCls} style={style}>
  375. {this.renderPrevBtn()}
  376. {
  377. hoverShowPageSelect ? (
  378. <Popover
  379. content={pageList}
  380. >
  381. {page}
  382. </Popover>
  383. ) : page
  384. }
  385. {this.renderNextBtn()}
  386. {this.renderQuickJump(locale)}
  387. </div>
  388. );
  389. }
  390. renderDefaultPage(locale: PaginationLocale) {
  391. const { total, pageSize } = this.state;
  392. const { showTotal, className, style, hideOnSinglePage, showSizeChanger } = this.props;
  393. const paginationCls = classNames(className, `${prefixCls}`);
  394. const showTotalCls = `${prefixCls}-total`;
  395. const totalPageNum = Math.ceil(total / pageSize);
  396. if (totalPageNum < 2 && hideOnSinglePage && !showSizeChanger) {
  397. return null;
  398. }
  399. return (
  400. <ul className={paginationCls} style={style}>
  401. {showTotal ? (
  402. <span className={showTotalCls}>
  403. {locale.total}
  404. {` ${Math.ceil(total / pageSize)} `}
  405. {locale.page}
  406. </span>
  407. ) : null}
  408. {this.renderPrevBtn()}
  409. {this.renderPageList()}
  410. {this.renderNextBtn()}
  411. {this.renderPageSizeSwitch(locale)}
  412. {this.renderQuickJump(locale)}
  413. </ul>
  414. );
  415. }
  416. render() {
  417. const { size } = this.props;
  418. return (
  419. <LocaleConsumer componentName="Pagination">
  420. {
  421. (locale: PaginationLocale) => (
  422. size === 'small' ? this.renderSmallPage(locale) : this.renderDefaultPage(locale)
  423. )
  424. }
  425. </LocaleConsumer>
  426. );
  427. }
  428. }