feat(ui): better pagination buttons

This commit is contained in:
psychedelicious 2024-06-26 23:21:42 +10:00
parent 2dd172c2c6
commit 32ebf82d1a
2 changed files with 95 additions and 75 deletions

View File

@ -1,56 +1,45 @@
import { Button, Flex, IconButton, Spacer, Text } from '@invoke-ai/ui-library'; import { Button, Flex, IconButton, Spacer } from '@invoke-ai/ui-library';
import { useGalleryPagination } from 'features/gallery/hooks/useGalleryPagination'; import { ELLIPSIS, useGalleryPagination } from 'features/gallery/hooks/useGalleryPagination';
import { PiCaretDoubleLeftBold, PiCaretDoubleRightBold, PiCaretLeftBold, PiCaretRightBold } from 'react-icons/pi'; import { PiCaretLeftBold, PiCaretRightBold } from 'react-icons/pi';
export const GalleryPagination = () => { export const GalleryPagination = () => {
const { const { goPrev, goNext, isPrevEnabled, isNextEnabled, pageButtons, goToPage, currentPage, total } =
goPrev, useGalleryPagination();
goNext,
goToFirst,
goToLast,
isFirstEnabled,
isLastEnabled,
isPrevEnabled,
isNextEnabled,
pageButtons,
goToPage,
currentPage,
rangeDisplay,
total,
} = useGalleryPagination();
if (!total) { if (!total) {
return <Flex flexDir="column" alignItems="center" gap="2" height="48px"></Flex>; return null;
} }
return ( return (
<Flex flexDir="column" alignItems="center" gap="2" height="48px">
<Flex gap={2} alignItems="center" w="full"> <Flex gap={2} alignItems="center" w="full">
<IconButton
size="sm"
aria-label="prev"
icon={<PiCaretDoubleLeftBold />}
onClick={goToFirst}
isDisabled={!isFirstEnabled}
/>
<IconButton <IconButton
size="sm" size="sm"
aria-label="prev" aria-label="prev"
icon={<PiCaretLeftBold />} icon={<PiCaretLeftBold />}
onClick={goPrev} onClick={goPrev}
isDisabled={!isPrevEnabled} isDisabled={!isPrevEnabled}
variant="ghost"
/> />
<Spacer /> <Spacer />
{pageButtons.map((page) => ( {pageButtons.map((page, i) => {
if (page === ELLIPSIS) {
return (
<Button size="sm" key={`ellipsis_${i}`} variant="link" isDisabled>
...
</Button>
);
}
return (
<Button <Button
size="sm" size="sm"
key={page} key={page}
onClick={goToPage.bind(null, page)} onClick={goToPage.bind(null, page - 1)}
variant={currentPage === page ? 'solid' : 'outline'} variant={currentPage === page - 1 ? 'solid' : 'outline'}
> >
{page + 1} {page}
</Button> </Button>
))} );
})}
<Spacer /> <Spacer />
<IconButton <IconButton
size="sm" size="sm"
@ -58,16 +47,8 @@ export const GalleryPagination = () => {
icon={<PiCaretRightBold />} icon={<PiCaretRightBold />}
onClick={goNext} onClick={goNext}
isDisabled={!isNextEnabled} isDisabled={!isNextEnabled}
variant="ghost"
/> />
<IconButton
size="sm"
aria-label="next"
icon={<PiCaretDoubleRightBold />}
onClick={goToLast}
isDisabled={!isLastEnabled}
/>
</Flex>
<Text>{rangeDisplay}</Text>
</Flex> </Flex>
); );
}; };

View File

@ -4,7 +4,58 @@ import { offsetChanged } from 'features/gallery/store/gallerySlice';
import { useCallback, useEffect, useMemo } from 'react'; import { useCallback, useEffect, useMemo } from 'react';
import { useListImagesQuery } from 'services/api/endpoints/images'; import { useListImagesQuery } from 'services/api/endpoints/images';
export const useGalleryPagination = (pageButtonsPerSide: number = 2) => { // Some logic copied from https://github.com/chakra-ui/zag/blob/1925b7342dc76fb06a7ec59a5a4c0063a4620422/packages/machines/pagination/src/pagination.utils.ts
export const range = (start: number, end: number) => {
const length = end - start + 1;
return Array.from({ length }, (_, idx) => idx + start);
};
export const ELLIPSIS = 'ellipsis' as const;
export const getRange = (currentPage: number, totalPages: number, siblingCount: number) => {
/**
* `2 * ctx.siblingCount + 5` explanation:
* 2 * ctx.siblingCount for left/right siblings
* 5 for 2x left/right ellipsis, 2x first/last page + 1x current page
*
* For some page counts (e.g. totalPages: 8, siblingCount: 2),
* calculated max page is higher than total pages,
* so we need to take the minimum of both.
*/
const totalPageNumbers = Math.min(2 * siblingCount + 5, totalPages);
const firstPageIndex = 1;
const lastPageIndex = totalPages;
const leftSiblingIndex = Math.max(currentPage - siblingCount, firstPageIndex);
const rightSiblingIndex = Math.min(currentPage + siblingCount, lastPageIndex);
const showLeftEllipsis = leftSiblingIndex > firstPageIndex + 1;
const showRightEllipsis = rightSiblingIndex < lastPageIndex - 1;
const itemCount = totalPageNumbers - 2; // 2 stands for one ellipsis and either first or last page
if (!showLeftEllipsis && showRightEllipsis) {
const leftRange = range(1, itemCount);
return [...leftRange, ELLIPSIS, lastPageIndex];
}
if (showLeftEllipsis && !showRightEllipsis) {
const rightRange = range(lastPageIndex - itemCount + 1, lastPageIndex);
return [firstPageIndex, ELLIPSIS, ...rightRange];
}
if (showLeftEllipsis && showRightEllipsis) {
const middleRange = range(leftSiblingIndex, rightSiblingIndex);
return [firstPageIndex, ELLIPSIS, ...middleRange, ELLIPSIS, lastPageIndex];
}
const fullRange = range(firstPageIndex, lastPageIndex);
return fullRange as (number | 'ellipsis')[];
};
export const useGalleryPagination = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { offset, limit } = useAppSelector((s) => s.gallery); const { offset, limit } = useAppSelector((s) => s.gallery);
const queryArgs = useAppSelector(selectListImagesQueryArgs); const queryArgs = useAppSelector(selectListImagesQueryArgs);
@ -57,24 +108,12 @@ export const useGalleryPagination = (pageButtonsPerSide: number = 2) => {
} }
}, [currentPage, pages, goToLast]); }, [currentPage, pages, goToLast]);
// calculate the page buttons to display - current page with 3 around it
const pageButtons = useMemo(() => { const pageButtons = useMemo(() => {
const buttons = []; if (pages > 7) {
const maxPageButtons = pageButtonsPerSide * 2 + 1; return getRange(currentPage + 1, pages, 1);
let startPage = Math.max(currentPage - Math.floor(maxPageButtons / 2), 0);
const endPage = Math.min(startPage + maxPageButtons - 1, pages - 1);
if (endPage - startPage < maxPageButtons - 1) {
startPage = Math.max(endPage - maxPageButtons + 1, 0);
} }
return range(1, pages);
for (let i = startPage; i <= endPage; i++) { }, [currentPage, pages]);
buttons.push(i);
}
return buttons;
}, [currentPage, pageButtonsPerSide, pages]);
const isFirstEnabled = useMemo(() => currentPage > 0, [currentPage]); const isFirstEnabled = useMemo(() => currentPage > 0, [currentPage]);
const isLastEnabled = useMemo(() => currentPage < pages - 1, [currentPage, pages]); const isLastEnabled = useMemo(() => currentPage < pages - 1, [currentPage, pages]);