index.tsx 16 KB

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