mirror of
https://github.com/stashapp/stash.git
synced 2025-12-18 21:04:37 +03:00
Make pagination more compact (#4882)
* Make pagination more compact Support entering page number or clicking from drop down * Fix border radius in dropdown in btn group * Separate page count control
This commit is contained in:
@@ -1,6 +1,132 @@
|
|||||||
import React from "react";
|
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { Button, ButtonGroup } from "react-bootstrap";
|
import {
|
||||||
import { FormattedMessage, FormattedNumber, useIntl } from "react-intl";
|
Button,
|
||||||
|
ButtonGroup,
|
||||||
|
Dropdown,
|
||||||
|
Form,
|
||||||
|
InputGroup,
|
||||||
|
Overlay,
|
||||||
|
Popover,
|
||||||
|
} from "react-bootstrap";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
|
import useFocus from "src/utils/focus";
|
||||||
|
import { Icon } from "../Shared/Icon";
|
||||||
|
import { faCheck, faChevronDown } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
|
const PageCount: React.FC<{
|
||||||
|
totalPages: number;
|
||||||
|
currentPage: number;
|
||||||
|
onChangePage: (page: number) => void;
|
||||||
|
}> = ({ totalPages, currentPage, onChangePage }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const currentPageCtrl = useRef(null);
|
||||||
|
|
||||||
|
const [pageInput, pageFocus] = useFocus();
|
||||||
|
|
||||||
|
const [showSelectPage, setShowSelectPage] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (showSelectPage) {
|
||||||
|
pageFocus();
|
||||||
|
}
|
||||||
|
}, [showSelectPage, pageFocus]);
|
||||||
|
|
||||||
|
const pageOptions = useMemo(() => {
|
||||||
|
const maxPagesToShow = 10;
|
||||||
|
const min = Math.max(1, currentPage - maxPagesToShow / 2);
|
||||||
|
const max = Math.min(min + maxPagesToShow, totalPages);
|
||||||
|
const pages = [];
|
||||||
|
for (let i = min; i <= max; i++) {
|
||||||
|
pages.push(i);
|
||||||
|
}
|
||||||
|
return pages;
|
||||||
|
}, [totalPages, currentPage]);
|
||||||
|
|
||||||
|
function onCustomChangePage() {
|
||||||
|
const newPage = Number.parseInt(pageInput.current?.value ?? "0");
|
||||||
|
if (newPage) {
|
||||||
|
onChangePage(newPage);
|
||||||
|
}
|
||||||
|
setShowSelectPage(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="page-count-container">
|
||||||
|
<ButtonGroup>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
className="page-count"
|
||||||
|
ref={currentPageCtrl}
|
||||||
|
onClick={() => {
|
||||||
|
setShowSelectPage(true);
|
||||||
|
pageFocus();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id="pagination.current_total"
|
||||||
|
values={{
|
||||||
|
current: intl.formatNumber(currentPage),
|
||||||
|
total: intl.formatNumber(totalPages),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
<Dropdown>
|
||||||
|
<Dropdown.Toggle variant="secondary" className="page-count-dropdown">
|
||||||
|
<Icon size="xs" icon={faChevronDown} />
|
||||||
|
</Dropdown.Toggle>
|
||||||
|
<Dropdown.Menu>
|
||||||
|
{pageOptions.map((s) => (
|
||||||
|
<Dropdown.Item
|
||||||
|
key={s}
|
||||||
|
active={s === currentPage}
|
||||||
|
onClick={() => onChangePage(s)}
|
||||||
|
>
|
||||||
|
{s}
|
||||||
|
</Dropdown.Item>
|
||||||
|
))}
|
||||||
|
</Dropdown.Menu>
|
||||||
|
</Dropdown>
|
||||||
|
</ButtonGroup>
|
||||||
|
<Overlay
|
||||||
|
target={currentPageCtrl.current}
|
||||||
|
show={showSelectPage}
|
||||||
|
placement="bottom"
|
||||||
|
rootClose
|
||||||
|
onHide={() => setShowSelectPage(false)}
|
||||||
|
>
|
||||||
|
<Popover id="select_page_popover">
|
||||||
|
<Form inline>
|
||||||
|
<InputGroup>
|
||||||
|
<Form.Control
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={totalPages}
|
||||||
|
className="text-input"
|
||||||
|
ref={pageInput}
|
||||||
|
defaultValue={currentPage}
|
||||||
|
onKeyPress={(e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
onCustomChangePage();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onFocus={(e: React.FocusEvent<HTMLInputElement>) =>
|
||||||
|
e.target.select()
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<InputGroup.Append>
|
||||||
|
<Button variant="primary" onClick={() => onCustomChangePage()}>
|
||||||
|
<Icon icon={faCheck} />
|
||||||
|
</Button>
|
||||||
|
</InputGroup.Append>
|
||||||
|
</InputGroup>
|
||||||
|
</Form>
|
||||||
|
</Popover>
|
||||||
|
</Overlay>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
interface IPaginationProps {
|
interface IPaginationProps {
|
||||||
itemsPerPage: number;
|
itemsPerPage: number;
|
||||||
@@ -23,91 +149,55 @@ export const Pagination: React.FC<IPaginationProps> = ({
|
|||||||
totalItems,
|
totalItems,
|
||||||
onChangePage,
|
onChangePage,
|
||||||
}) => {
|
}) => {
|
||||||
const totalPages = Math.ceil(totalItems / itemsPerPage);
|
const intl = useIntl();
|
||||||
|
|
||||||
let startPage: number;
|
const totalPages = useMemo(
|
||||||
let endPage: number;
|
() => Math.ceil(totalItems / itemsPerPage),
|
||||||
if (totalPages <= 10) {
|
[totalItems, itemsPerPage]
|
||||||
// less than 10 total pages so show all
|
|
||||||
startPage = 1;
|
|
||||||
endPage = totalPages;
|
|
||||||
} else if (currentPage <= 6) {
|
|
||||||
startPage = 1;
|
|
||||||
endPage = 10;
|
|
||||||
} else if (currentPage + 4 >= totalPages) {
|
|
||||||
startPage = totalPages - 9;
|
|
||||||
endPage = totalPages;
|
|
||||||
} else {
|
|
||||||
startPage = currentPage - 5;
|
|
||||||
endPage = currentPage + 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pages = [...Array(endPage + 1 - startPage).keys()].map(
|
|
||||||
(i) => startPage + i
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const calculatePageClass = (buttonPage: number) => {
|
if (totalPages <= 1) return <div />;
|
||||||
if (pages.length <= 4) return "";
|
|
||||||
|
|
||||||
if (currentPage === 1 && buttonPage <= 4) return "";
|
|
||||||
const maxPage = pages[pages.length - 1];
|
|
||||||
if (currentPage === maxPage && buttonPage > maxPage - 3) return "";
|
|
||||||
if (Math.abs(buttonPage - currentPage) <= 1) return "";
|
|
||||||
return "d-none d-sm-block";
|
|
||||||
};
|
|
||||||
|
|
||||||
const pageButtons = pages.map((page: number) => (
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
className={calculatePageClass(page)}
|
|
||||||
key={page}
|
|
||||||
active={currentPage === page}
|
|
||||||
onClick={() => onChangePage(page)}
|
|
||||||
>
|
|
||||||
<FormattedNumber value={page} />
|
|
||||||
</Button>
|
|
||||||
));
|
|
||||||
|
|
||||||
if (pages.length <= 1) return <div />;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ButtonGroup className="filter-container pagination">
|
<ButtonGroup className="pagination">
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
disabled={currentPage === 1}
|
disabled={currentPage === 1}
|
||||||
onClick={() => onChangePage(1)}
|
onClick={() => onChangePage(1)}
|
||||||
|
title={intl.formatMessage({ id: "pagination.first" })}
|
||||||
>
|
>
|
||||||
<span className="d-none d-sm-inline">
|
<span>«</span>
|
||||||
<FormattedMessage id="pagination.first" />
|
|
||||||
</span>
|
|
||||||
<span className="d-inline d-sm-none">《</span>
|
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className="d-none d-sm-block"
|
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
disabled={currentPage === 1}
|
disabled={currentPage === 1}
|
||||||
onClick={() => onChangePage(currentPage - 1)}
|
onClick={() => onChangePage(currentPage - 1)}
|
||||||
|
title={intl.formatMessage({ id: "pagination.previous" })}
|
||||||
>
|
>
|
||||||
<FormattedMessage id="pagination.previous" />
|
<
|
||||||
</Button>
|
</Button>
|
||||||
{pageButtons}
|
|
||||||
|
<PageCount
|
||||||
|
totalPages={totalPages}
|
||||||
|
currentPage={currentPage}
|
||||||
|
onChangePage={onChangePage}
|
||||||
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
className="d-none d-sm-block"
|
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
disabled={currentPage === totalPages}
|
disabled={currentPage === totalPages}
|
||||||
onClick={() => onChangePage(currentPage + 1)}
|
onClick={() => onChangePage(currentPage + 1)}
|
||||||
|
title={intl.formatMessage({ id: "pagination.next" })}
|
||||||
>
|
>
|
||||||
<FormattedMessage id="pagination.next" />
|
>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
disabled={currentPage === totalPages}
|
disabled={currentPage === totalPages}
|
||||||
onClick={() => onChangePage(totalPages)}
|
onClick={() => onChangePage(totalPages)}
|
||||||
|
title={intl.formatMessage({ id: "pagination.last" })}
|
||||||
>
|
>
|
||||||
<span className="d-none d-sm-inline">
|
<span>»</span>
|
||||||
<FormattedMessage id="pagination.last" />
|
|
||||||
</span>
|
|
||||||
<span className="d-inline d-sm-none">》</span>
|
|
||||||
</Button>
|
</Button>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,14 +7,27 @@
|
|||||||
padding-right: 15px;
|
padding-right: 15px;
|
||||||
transition: none;
|
transition: none;
|
||||||
|
|
||||||
|
&.page-count {
|
||||||
|
padding-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.page-count-dropdown {
|
||||||
|
padding-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
&:first-child {
|
&:first-child {
|
||||||
border-left: none;
|
border-left: none;
|
||||||
|
border-right: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
border-right: none;
|
border-right: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page-count-container .btn {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.center-text {
|
.center-text {
|
||||||
|
|||||||
@@ -713,7 +713,8 @@ div.dropdown-menu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.filter-container,
|
.filter-container,
|
||||||
.operation-container {
|
.operation-container,
|
||||||
|
.pagination {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -1330,6 +1331,11 @@ $detailTabWidth: calc(100% / 3);
|
|||||||
border-top-right-radius: 0;
|
border-top-right-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-group > .dropdown:not(:first-child) > .btn {
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
dl.details-list {
|
dl.details-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-column-gap: 10px;
|
grid-column-gap: 10px;
|
||||||
|
|||||||
@@ -1160,6 +1160,7 @@
|
|||||||
"version": "Version"
|
"version": "Version"
|
||||||
},
|
},
|
||||||
"pagination": {
|
"pagination": {
|
||||||
|
"current_total": "{current} of {total}",
|
||||||
"first": "First",
|
"first": "First",
|
||||||
"last": "Last",
|
"last": "Last",
|
||||||
"next": "Next",
|
"next": "Next",
|
||||||
|
|||||||
Reference in New Issue
Block a user