1
0

index.tsx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  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 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. get adapter(): PaginationAdapter<PaginationProps, PaginationState> {
  128. return {
  129. ...super.adapter,
  130. setPageList: (pageListState: AdapterPageList) => {
  131. const { pageList, restLeftPageList, restRightPageList } = pageListState;
  132. this.setState({ pageList, restLeftPageList, restRightPageList });
  133. },
  134. setDisabled: (prevIsDisabled: boolean, nextIsDisabled: boolean) => {
  135. this.setState({ prevDisabled: prevIsDisabled, nextDisabled: nextIsDisabled });
  136. },
  137. updateTotal: (total: number) => this.setState({ total }),
  138. updatePageSize: (pageSize: number) => this.setState({ pageSize }),
  139. updateQuickJumpPage: (quickJumpPage: string | number) => this.setState({ quickJumpPage }),
  140. // updateRestPageList: () => {},
  141. setCurrentPage: (pageIndex: number) => {
  142. this.setState({ currentPage: pageIndex });
  143. },
  144. registerKeyDownHandler: (handler: KeyDownHandler) => {
  145. document.addEventListener('keydown', handler);
  146. },
  147. unregisterKeyDownHandler: (handler: KeyDownHandler) => {
  148. document.removeEventListener('keydown', handler);
  149. },
  150. notifyPageChange: (pageIndex: number) => {
  151. this.props.onPageChange(pageIndex);
  152. },
  153. notifyPageSizeChange: (pageSize: number) => {
  154. this.props.onPageSizeChange(pageSize);
  155. },
  156. notifyChange: (pageIndex: number, pageSize: number) => {
  157. this.props.onChange(pageIndex, pageSize);
  158. }
  159. };
  160. }
  161. componentDidMount() {
  162. this.foundation.init();
  163. }
  164. componentWillUnmount() {
  165. this.foundation.destroy();
  166. }
  167. componentDidUpdate(prevProps: PaginationProps) {
  168. const pagerProps = {
  169. currentPage: this.props.currentPage,
  170. total: this.props.total,
  171. pageSize: this.props.pageSize,
  172. };
  173. let pagerHasChanged = false;
  174. if (prevProps.currentPage !== this.props.currentPage) {
  175. pagerHasChanged = true;
  176. // this.foundation.updatePage(this.props.currentPage);
  177. }
  178. if (prevProps.total !== this.props.total) {
  179. pagerHasChanged = true;
  180. }
  181. if (prevProps.pageSize !== this.props.pageSize) {
  182. pagerHasChanged = true;
  183. }
  184. if (pagerHasChanged) {
  185. this.foundation.updatePage(pagerProps.currentPage, pagerProps.total, pagerProps.pageSize);
  186. }
  187. }
  188. renderPrevBtn() {
  189. const { prevText } = this.props;
  190. const { prevDisabled } = this.state;
  191. const preClassName = classNames({
  192. [`${prefixCls}-item`]: true,
  193. [`${prefixCls}-prev`]: true,
  194. [`${prefixCls}-item-disabled`]: prevDisabled,
  195. });
  196. return (
  197. <li role="button" aria-disabled={prevDisabled ? true : false} aria-label="Previous" onClick={e => !prevDisabled && this.foundation.goPrev(e)} className={preClassName}>
  198. {prevText || <IconChevronLeft size="large" />}
  199. </li>
  200. );
  201. }
  202. renderNextBtn() {
  203. const { nextText } = this.props;
  204. const { nextDisabled } = this.state;
  205. const nextClassName = classNames({
  206. [`${prefixCls}-item`]: true,
  207. [`${prefixCls}-item-disabled`]: nextDisabled,
  208. [`${prefixCls}-next`]: true,
  209. });
  210. return (
  211. <li role="button" aria-disabled={nextDisabled ? true : false} aria-label="Next" onClick={e => !nextDisabled && this.foundation.goNext(e)} className={nextClassName}>
  212. {nextText || <IconChevronRight size="large" />}
  213. </li>
  214. );
  215. }
  216. renderPageSizeSwitch(locale: PaginationLocale) {
  217. // rtl modify the default position
  218. const { direction } = this.context;
  219. const defaultPopoverPosition = direction === 'rtl' ? 'bottomRight' : 'bottomLeft';
  220. const { showSizeChanger, popoverPosition = defaultPopoverPosition } = this.props;
  221. const { pageSize } = this.state;
  222. const switchCls = classNames(`${prefixCls}-switch`);
  223. if (!showSizeChanger) {
  224. return null;
  225. }
  226. const pageSizeText = locale.pageSize;
  227. const newPageSizeOpts = this.foundation.pageSizeInOpts();
  228. const options = newPageSizeOpts.map((size: number) => (
  229. <Option value={size} key={size}>
  230. <span>
  231. {`${size} `}
  232. {pageSizeText}
  233. </span>
  234. </Option>
  235. ));
  236. return (
  237. <div className={switchCls}>
  238. <Select
  239. aria-label="Page size selector"
  240. onChange={newPageSize => this.foundation.changePageSize(newPageSize)}
  241. value={pageSize}
  242. key={pageSizeText}
  243. position={popoverPosition || 'bottomRight'}
  244. clickToHide
  245. dropdownClassName={`${prefixCls}-select-dropdown`}
  246. >
  247. {options}
  248. </Select>
  249. </div>
  250. );
  251. }
  252. renderQuickJump(locale: PaginationLocale) {
  253. const { showQuickJumper } = this.props;
  254. const { quickJumpPage, total, pageSize } = this.state;
  255. if (!showQuickJumper) {
  256. return null;
  257. }
  258. const totalPageNum = this.foundation._getTotalPageNumber(total, pageSize);
  259. const isDisabled = totalPageNum === 1;
  260. const quickJumpCls = classNames({
  261. [`${prefixCls}-quickjump`]: true,
  262. [`${prefixCls}-quickjump-disabled`]: isDisabled
  263. });
  264. return (
  265. <div className={quickJumpCls}>
  266. <span>{locale.jumpTo}</span>
  267. <InputNumber
  268. value={quickJumpPage}
  269. className={`${prefixCls}-quickjump-input-number`}
  270. hideButtons
  271. disabled={isDisabled}
  272. onBlur={(e: React.FocusEvent) => this.foundation.handleQuickJumpBlur()}
  273. onEnterPress={(e: React.KeyboardEvent) => this.foundation.handleQuickJumpEnterPress((e.target as any).value)}
  274. onChange={(v: string | number) => this.foundation.handleQuickJumpNumberChange(v)}
  275. />
  276. <span>{locale.page}</span>
  277. </div>
  278. );
  279. }
  280. renderPageList() {
  281. const {
  282. pageList,
  283. currentPage,
  284. restLeftPageList,
  285. restRightPageList,
  286. } = this.state;
  287. const { popoverPosition, popoverZIndex } = this.props;
  288. return pageList.map((page, i) => {
  289. const pageListClassName = classNames(`${prefixCls}-item`, {
  290. [`${prefixCls}-item-active`]: currentPage === page,
  291. // [`${prefixCls}-item-rest-opening`]: (i < 3 && isLeftRestHover && page ==='...') || (i > 3 && isRightRestHover && page === '...')
  292. });
  293. const pageEl = (
  294. <li
  295. key={`${page}${i}`}
  296. onClick={() => this.foundation.goPage(page, i)}
  297. className={pageListClassName}
  298. aria-label={page === '...' ? 'More' : `Page ${page}`}
  299. aria-current={currentPage === page ? "page" : false}
  300. >
  301. {page}
  302. </li>
  303. );
  304. if (page === '...') {
  305. let content;
  306. i < 3 ? (content = restLeftPageList) : (content = restRightPageList);
  307. return (
  308. <Popover
  309. trigger="hover"
  310. // onVisibleChange={visible=>this.handleRestHover(visible, i < 3 ? 'left' : 'right')}
  311. content={this.renderRestPageList(content)}
  312. key={`${page}${i}`}
  313. position={popoverPosition}
  314. zIndex={popoverZIndex}
  315. >
  316. {pageEl}
  317. </Popover>
  318. );
  319. }
  320. return pageEl;
  321. });
  322. }
  323. renderRestPageList(restList: ('...' | number)[]) {
  324. // The number of pages may be tens of thousands, here is virtualized with the help of react-window
  325. const { direction } = this.context;
  326. const className = classNames(`${prefixCls}-rest-item`);
  327. const count = restList.length;
  328. const row = (item: { index: number; style: React.CSSProperties }) => {
  329. const { index, style } = item;
  330. const page = restList[index];
  331. return (
  332. <div
  333. role="listitem"
  334. key={`${page}${index}`}
  335. className={className}
  336. onClick={() => this.foundation.goPage(page, index)}
  337. style={style}
  338. aria-label={`${page}`}
  339. >
  340. {page}
  341. </div>
  342. );
  343. };
  344. const itemHeight = 32;
  345. const listHeight = count >= 5 ? itemHeight * 5 : itemHeight * count;
  346. return (
  347. <List
  348. className={`${prefixCls}-rest-list`}
  349. itemData={restList}
  350. itemSize={itemHeight}
  351. width={78}
  352. itemCount={count}
  353. height={listHeight}
  354. style={{ direction }}
  355. >
  356. {row}
  357. </List>
  358. );
  359. }
  360. renderSmallPage(locale: PaginationLocale) {
  361. const { className, style, hideOnSinglePage, hoverShowPageSelect, showSizeChanger } = this.props;
  362. const paginationCls = classNames(`${prefixCls}-small`, prefixCls, className);
  363. const { currentPage, total, pageSize } = this.state;
  364. const totalPageNum = Math.ceil(total / pageSize);
  365. if (totalPageNum < 2 && hideOnSinglePage && !showSizeChanger) {
  366. return null;
  367. }
  368. const pageNumbers = Array.from({ length: Math.ceil(total / pageSize) }, (v, i) => i + 1);
  369. const pageList = this.renderRestPageList(pageNumbers);
  370. const page = (<div className={`${prefixCls}-item ${prefixCls}-item-small`}>{currentPage}/{totalPageNum} </div>);
  371. return (
  372. <div className={paginationCls} style={style}>
  373. {this.renderPrevBtn()}
  374. {
  375. hoverShowPageSelect ? (
  376. <Popover
  377. content={pageList}
  378. >
  379. {page}
  380. </Popover>
  381. ) : page
  382. }
  383. {this.renderNextBtn()}
  384. {this.renderQuickJump(locale)}
  385. </div>
  386. );
  387. }
  388. renderDefaultPage(locale: PaginationLocale) {
  389. const { total, pageSize } = this.state;
  390. const { showTotal, className, style, hideOnSinglePage, showSizeChanger } = this.props;
  391. const paginationCls = classNames(className, `${prefixCls}`);
  392. const showTotalCls = `${prefixCls}-total`;
  393. const totalPageNum = Math.ceil(total / pageSize);
  394. if (totalPageNum < 2 && hideOnSinglePage && !showSizeChanger) {
  395. return null;
  396. }
  397. return (
  398. <ul className={paginationCls} style={style}>
  399. {showTotal ? (
  400. <span className={showTotalCls}>
  401. {locale.total}
  402. {` ${Math.ceil(total / pageSize)} `}
  403. {locale.page}
  404. </span>
  405. ) : null}
  406. {this.renderPrevBtn()}
  407. {this.renderPageList()}
  408. {this.renderNextBtn()}
  409. {this.renderPageSizeSwitch(locale)}
  410. {this.renderQuickJump(locale)}
  411. </ul>
  412. );
  413. }
  414. render() {
  415. const { size } = this.props;
  416. return (
  417. <LocaleConsumer componentName="Pagination">
  418. {
  419. (locale: PaginationLocale) => (
  420. size === 'small' ? this.renderSmallPage(locale) : this.renderDefaultPage(locale)
  421. )
  422. }
  423. </LocaleConsumer>
  424. );
  425. }
  426. }