import { isNull } from 'lodash';
import React, { useEffect, useRef, useState } from 'react';
import { DoubleLeftOutlined, DoubleRightOutlined, EllipsisOutlined, LeftOutlined, RightOutlined } from '@react-devui/icons';
import { getClassName } from '@react-devui/utils';
import { useDValue } from '../../hooks';
import { cloneHTMLElement, registerComponentMate } from '../../utils';
import { DInput } from '../input';
import { useComponentConfig, usePrefixConfig, useTranslation } from '../root';
import { DSelect } from '../select';
import { getButtonRoleAttributes } from './utils';
export interface DPaginationProps extends Omit<React.HTMLAttributes<HTMLElement>, 'children'> {
dActive?: number;
dTotal: number;
dPageSize?: number;
dPageSizeList?: number[];
dCompose?: ('total' | 'pages' | 'page-size' | 'jump')[];
dCustomRender?: {
total?: (range: [number, number]) => React.ReactNode;
prev?: React.ReactNode;
page?: (page: number) => React.ReactNode;
next?: React.ReactNode;
pageSize?: (pageSize: number) => React.ReactNode;
jump?: (input: React.ReactNode) => React.ReactNode;
};
dMini?: boolean;
onPaginationChange?: (page: number, pageSize: number) => void;
}
const { COMPONENT_NAME } = registerComponentMate({ COMPONENT_NAME: 'DPagination' as const });
export function DPagination(props: DPaginationProps): JSX.Element | null {
const {
dActive,
dTotal,
dPageSize,
dPageSizeList = [10, 20, 50, 100],
dCompose = ['pages'],
dCustomRender,
dMini = false,
onPaginationChange,
...restProps
} = useComponentConfig(COMPONENT_NAME, props);
const dPrefix = usePrefixConfig();
const navRef = useRef<HTMLElement>(null);
const [t] = useTranslation();
const [active, _changeActive] = useDValue<number>(1, dActive);
const changeActive = (_active: number) => {
setIsChange(true);
const newActive = Math.max(Math.min(_active, lastPage), 1);
_changeActive(newActive);
if (!Object.is(newActive, active)) {
onPaginationChange?.(newActive, pageSize);
}
};
const [isChange, setIsChange] = useState(false);
useEffect(() => {
if (isChange) {
setIsChange(false);
}
});
const [pageSize, _changePageSize] = useDValue<number>(dPageSizeList[0] ?? 10, dPageSize);
const changePageSize = (_pageSize: number) => {
_changePageSize(_pageSize);
const lastPage = Math.max(Math.ceil(dTotal / _pageSize), 1);
const newActive = Math.max(Math.min(active, lastPage), 1);
if (!Object.is(newActive, active) || !Object.is(_pageSize, pageSize)) {
onPaginationChange?.(newActive, _pageSize);
}
};
const [jumpValue, setJumpValue] = useState('');
const lastPage = Math.max(Math.ceil(dTotal / pageSize), 1);
return (
<nav
{...restProps}
ref={navRef}
className={getClassName(restProps.className, `${dPrefix}pagination`, {
[`${dPrefix}pagination--mini`]: dMini,
'is-change': isChange,
})}
role="navigation"
aria-label={restProps['aria-label'] ?? 'Pagination Navigation'}
>
{dCompose.map((item) => {
if (item === 'total') {
const totalNode = (() => {
const range: [number, number] = [Math.min((active - 1) * pageSize + 1, dTotal), Math.min(active * pageSize, dTotal)];
if (dCustomRender && dCustomRender.total) {
return dCustomRender.total(range);
} else {
return (
<div>
{t('Pagination', 'Total')} {dTotal} {t('Pagination', 'items')}
</div>
);
}
})();
return (
<div key="total" className={`${dPrefix}pagination__total`}>
{totalNode}
</div>
);
}
if (item === 'pages') {
const [prevNode, pageNode, nextNode] = [
<li
{...getButtonRoleAttributes(() => {
changeActive(active - 1);
}, active === 1)}
className={getClassName(`${dPrefix}pagination__button`, {
'is-disabled': active === 1,
[`${dPrefix}pagination__button--border`]: !(dCustomRender && dCustomRender.prev),
})}
title={t('Pagination', 'Previous page')}
>
{dCustomRender && dCustomRender.prev ? dCustomRender.prev : <LeftOutlined />}
</li>,
(page: number) => {
if (dCustomRender && dCustomRender.page) {
return dCustomRender.page(page);
} else {
return <div>{page}</div>;
}
},
<li
{...getButtonRoleAttributes(() => {
changeActive(active + 1);
}, active === lastPage)}
className={getClassName(`${dPrefix}pagination__button`, {
'is-disabled': active === lastPage,
[`${dPrefix}pagination__button--border`]: !(dCustomRender && dCustomRender.next),
})}
title={t('Pagination', 'Next page')}
>
{dCustomRender && dCustomRender.next ? dCustomRender.next : <RightOutlined />}
</li>,
];
let pages: (number | 'prev5' | 'next5')[] = [];
if (lastPage <= 7) {
pages = Array.from({ length: lastPage }).map((_, index) => index + 1);
} else {
for (let n = -3; n <= 3; n++) {
pages.push(active + n);
}
if (pages[0] < 1) {
pages = (pages as number[]).map((n) => n + (1 - (pages as number[])[0]));
}
if (pages[6] > lastPage) {
pages = (pages as number[]).map((n) => n - ((pages as number[])[6] - lastPage));
}
if (pages[0] > 1) {
pages[0] = 1;
pages[1] = 'prev5';
}
if (pages[6] < lastPage) {
pages[6] = lastPage;
pages[5] = 'next5';
}
}
return (
<React.Fragment key="pages">
{prevNode}
{pages.map((n) => {
if (n === 'prev5') {
return (
<li
{...getButtonRoleAttributes(() => {
changeActive(active - 5);
})}
key="prev5"
className={getClassName(`${dPrefix}pagination__button`, `${dPrefix}pagination__button--jump5`)}
title={t('Pagination', '5 pages forward')}
>
<DoubleLeftOutlined className={`${dPrefix}pagination__jump5-icon`} />
<div className={`${dPrefix}pagination__ellipsis`}>
<EllipsisOutlined />
</div>
</li>
);
} else if (n === 'next5') {
return (
<li
{...getButtonRoleAttributes(() => {
changeActive(active + 5);
})}
key="next5"
className={getClassName(`${dPrefix}pagination__button`, `${dPrefix}pagination__button--jump5`)}
title={t('Pagination', '5 pages backward')}
>
<DoubleRightOutlined className={`${dPrefix}pagination__jump5-icon`} />
<div className={`${dPrefix}pagination__ellipsis`}>
<EllipsisOutlined />
</div>
</li>
);
} else {
return (
<li
key={n}
className={getClassName(
`${dPrefix}pagination__button`,
`${dPrefix}pagination__button--border`,
`${dPrefix}pagination__button--number`,
{
'is-active': active === n,
}
)}
tabIndex={0}
data-number={n}
onClick={() => {
changeActive(n);
}}
onKeyDown={(e) => {
const focusN = (num: number) => {
if (navRef.current) {
const activeEl = navRef.current.querySelector(`li[data-number="${num}"]`) as HTMLElement | null;
activeEl?.focus({ preventScroll: true });
}
changeActive(num);
};
switch (e.code) {
case 'ArrowLeft':
e.preventDefault();
focusN(n - 1);
break;
case 'ArrowRight':
e.preventDefault();
focusN(n + 1);
break;
case 'Home':
e.preventDefault();
focusN(1);
break;
case 'End':
e.preventDefault();
focusN(lastPage);
break;
default:
break;
}
}}
>
{pageNode(n)}
</li>
);
}
})}
{nextNode}
</React.Fragment>
);
}
if (item === 'page-size') {
const list = dPageSizeList.map((size) => ({
label: size.toString(),
value: size,
}));
return (
<DSelect
key="page-size"
className={`${dPrefix}pagination__page-size`}
dList={list}
dModel={pageSize}
dCustomItem={(item) => (dCustomRender && dCustomRender.pageSize ? dCustomRender.pageSize(item.value) : item.label)}
dCustomSelected={(select) => `${select.label}${t('Pagination', ' / Page')}`}
onModelChange={(value) => {
if (!isNull(value)) {
changePageSize(value);
}
}}
/>
);
}
if (item === 'jump') {
const jumpNode = (() => {
const jumpInput = (
<DInput
className={`${dPrefix}pagination__jump-input`}
dType="number"
dMax={lastPage}
dMin={1}
dStep={1}
dModel={jumpValue}
dNumbetButton={!dMini}
dInputRender={(el) =>
cloneHTMLElement(el, {
onKeyDown: (e) => {
el.props.onKeyDown?.(e);
if (e.code === 'Enter') {
e.preventDefault();
const val = Number(jumpValue);
if (!isNaN(val)) {
changeActive(val);
}
}
},
})
}
onModelChange={setJumpValue}
/>
);
if (dCustomRender && dCustomRender.jump) {
return dCustomRender.jump(jumpInput);
} else {
return (
<>
<span>{t('Pagination', 'Go')}</span>
{jumpInput}
<span>{t('Pagination', 'Page')}</span>
</>
);
}
})();
return (
<div key="jump">
<div key="jump" className={`${dPrefix}pagination__jump`}>
{jumpNode}
</div>
</div>
);
}
return null;
})}
</nav>
);
}