Add pagination components.

This commit is contained in:
Madeorsk 2024-07-14 10:56:04 +02:00
parent 9fadcfb1b2
commit 7bde352ea7
Signed by: Madeorsk
SSH key fingerprint: SHA256:J9G0ofIOLKf7kyS2IfrMqtMaPdfsk1W02+oGueZzDDU
6 changed files with 384 additions and 5 deletions

View file

@ -24,6 +24,7 @@ import {Application} from "../src/Application/Application";
import {Outlet} from "react-router-dom";
import {ToggleSwitch} from "../src/Components/Forms/ToggleSwitch";
import {Step, Steps} from "../src/Components/Steps/Steps";
import {AsyncPaginate, AutoPaginate, Paginate} from "../src/Components/Pagination/Paginate";
export function DemoApp()
{
@ -31,6 +32,8 @@ export function DemoApp()
const [selected, setSelected] = useState(null);
const [page, setPage] = useState(11);
return (
<Application>
<MainMenu>
@ -63,9 +66,7 @@ export function DemoApp()
<h2>TODO</h2>
<ul>
<li>Pagination</li>
<li>Global states</li>
<li>Async</li>
<li>Errors</li>
<li>Subapps</li>
<li>Modals</li>
</ul>
@ -283,7 +284,7 @@ export function DemoApp()
<h3>Simple loaders</h3>
<Card>
<SpinningLoader/>
<SpinningLoader inline={true} />
<ListLoader/>
</Card>
@ -310,7 +311,7 @@ export function DemoApp()
<Basket /> Submenu
</SubmenuItemSubmenu>
</Submenu>
} floatingOptions={{ placement: "right-start" }}>
} floatingOptions={{placement: "right-start"}}>
<button>Submenu on a button</button>
</SubmenuFloat>
@ -410,6 +411,37 @@ export function DemoApp()
</Card>
</Step>
</Steps>
<h2>Pagination</h2>
<h3>Normal pagination</h3>
<Paginate onChange={setPage} count={72} page={page}>
<Card>Page {page}</Card>
</Paginate>
<h3>Auto pagination</h3>
<AutoPaginate count={55}>
{(page) => (
<Card>Page {page}</Card>
)}
</AutoPaginate>
<h3>Async pagination</h3>
<AsyncPaginate count={async () => { return 72; }} getData={async () => (["a", Math.random(), "c"])}>
{(data) => (
<>
{
data.map((value, index) => (
<div key={index}>{value}</div>
))
}
</>
)}
</AsyncPaginate>
</Application>
);
}

View file

@ -0,0 +1,137 @@
import React, {useCallback, useState} from "react";
import {Pagination} from "./Pagination";
import {SpinningLoader} from "../Loaders/SpinningLoader";
import {Await, useAsync} from "../../Async";
/**
* Paginated content component with custom page handling.
*/
export function Paginate({ page, onChange, count, children }: React.PropsWithChildren<{
/**
* The current page.
*/
page: number;
/**
* Called when a new page is selected.
* @param newPage The newly selected page.
*/
onChange: (newPage: number) => void;
/**
* Pages count.
*/
count: number;
}>)
{
return (
<>
{children}
<Pagination page={page} onChange={onChange} count={count} />
</>
);
}
/**
* Paginated content component.
*/
export function AutoPaginate({ count, children }: {
/**
* Pages count.
*/
count: number;
/**
* Show the given page.
* @param page The page to show.
*/
children: (page: number) => React.ReactElement;
})
{
// The current page.
const [page, setPage] = useState<number>(1);
return (
<Paginate page={page} onChange={setPage} count={count}>
{children(page)}
</Paginate>
);
}
/**
* Asynchronous paginated content component.
*/
export function AsyncPaginate<T>({ count, getData, children }: {
/**
* Get pages count.
*/
count: () => Promise<number>;
/**
* Get data for the given page.
* @param page The page for which to get data.
*/
getData: (page: number) => Promise<T>;
/**
* Show the current page with its retrieved data.
* @param data Data of the page to show.
* @param page The page to show.
*/
children: (data: T, page: number) => React.ReactElement;
})
{
// Getting pages count.
const asyncCount = useAsync(count, []);
return (
<Await async={asyncCount} fallback={<SpinningLoader />}>
{
(count) => (
<AutoPaginate count={count}>
{(page) => <AsyncPage page={page} getData={getData} render={children} />}
</AutoPaginate>
)
}
</Await>
);
}
/**
* An async page to render.
*/
export function AsyncPage<T>({page, getData, render}: {
/**
* The page number to show.
*/
page: number;
/**
* Get data for the given page.
* @param page The page for which to get data.
*/
getData: (page: number) => Promise<T>;
/**
* Render the page with its retrieved data.
* @param data Data of the page to show.
* @param page The page to show.
*/
render: (data: T, page: number) => React.ReactElement;
})
{
// Store function to get page data.
const getPageData = useCallback(() => {
return getData(page);
}, [page]);
// Getting page data.
const asyncPageData = useAsync(getPageData, [getPageData]);
return (
<Await async={asyncPageData} fallback={<SpinningLoader />}>
{(pageData) => render(pageData, page)}
</Await>
);
}

View file

@ -0,0 +1,141 @@
import React, {useEffect, useState} from "react";
import {CaretLeft, CaretRight} from "@phosphor-icons/react";
import {Tooltip} from "../Floating/Tooltip";
import {usePreviousValue} from "../../Utils";
/**
* Pagination component.
*/
export function Pagination({ page, onChange, count }: {
/**
* The current page.
*/
page: number;
/**
* Called when a new page is selected.
* @param newPage The newly selected page.
*/
onChange: (newPage: number) => void;
/**
* Pages count.
*/
count: number;
})
{
// Memorize previous page.
const previousPage = usePreviousValue(page);
// The input text to use, when the currently entered value is not a number.
const [inputText, setInputText] = useState(undefined);
useEffect(() => {
if (page != previousPage)
// If the page has changed, resetting the input text.
setInputText(undefined);
}, [previousPage, page]);
return (
<nav className={"pages"}>
<Tooltip content={"Previous"}>
<button className={"flat icon-only"} disabled={page <= 1} onClick={() => onChange(page - 1)}>
<CaretLeft/>
</button>
</Tooltip>
<ul>
{ // First page, only show when the current page isn't the first page.
page > 1 &&
<li>
<button className={"flat"} onClick={() => onChange(1)}>1</button>
</li>
}
{ // Space between first page and shown pages.
// Only show when the page isn't the first page.
page - 4 > 1 &&
<li>
<button className={"flat"} onClick={page - 4 > 2 ? undefined : () => onChange(page - 4)}>{page - 4 > 2 ? "···" : page - 4}</button>
</li>
}
{ // Only show when the page isn't the first page.
page - 3 > 1 &&
<li>
<button className={"flat"} onClick={() => onChange(page - 3)}>{page - 3}</button>
</li>
}
{ // Only show when the page isn't the first page.
page - 2 > 1 &&
<li>
<button className={"flat"} onClick={() => onChange(page - 2)}>{page - 2}</button>
</li>
}
{ // Only show when the page isn't the first page.
page - 1 > 1 &&
<li>
<button className={"flat"} onClick={() => onChange(page - 1)}>{page - 1}</button>
</li>
}
{ // Current page input.
<li className={"current"}>
<input type={"number"} step={1} min={1} max={count} value={inputText ?? page} onChange={(event) => {
// Try to get the new value, between 1 and page count.
const newValue = Math.max(Math.min(event.currentTarget.valueAsNumber, count), 1);
if (isNaN(newValue))
// Not a numeric value, keep the current input text without changing the page number.
setInputText(event.currentTarget.value);
else
{ // The value has been read successfully, changing it and resetting input text.
onChange(newValue);
setInputText(undefined);
}
}} />
</li>
}
{ // Only show when the page isn't the last page.
page + 1 < count &&
<li>
<button className={"flat"} onClick={() => onChange(page + 1)}>{page + 1}</button>
</li>
}
{ // Only show when the page isn't the last page.
page + 2 < count &&
<li>
<button className={"flat"} onClick={() => onChange(page + 2)}>{page + 2}</button>
</li>
}
{ // Only show when the page isn't the last page.
page + 3 < count &&
<li>
<button className={"flat"} onClick={() => onChange(page + 3)}>{page + 3}</button>
</li>
}
{ // Space between shown pages and last page.
// Only show when the page isn't the last page.
page + 4 < count &&
<li>
<button className={"flat"} onClick={page + 4 < (count - 1) ? undefined : () => onChange(page + 4)}>{page + 4 < (count - 1) ? "···" : page + 4}</button>
</li>
}
{ // Last page, only show when the current page isn't the last page.
page < count &&
<li>
<button className={"flat"} onClick={() => onChange(count)}>{count}</button>
</li>
}
</ul>
<Tooltip content={"Next"}>
<button className={"flat icon-only"} disabled={page >= count} onClick={() => onChange(page + 1)}>
<CaretRight/>
</button>
</Tooltip>
</nav>
);
}

View file

@ -9,6 +9,7 @@
@import "components/_list";
@import "components/_loaders";
@import "components/_menus";
@import "components/_pagination";
@import "components/_select";
@import "components/_steps";
@import "components/_steps-counter";

View file

@ -0,0 +1 @@
@import "pagination/_nav";

View file

@ -0,0 +1,67 @@
nav.pages
{
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
margin: auto;
padding: 0.25em;
width: 50em;
max-width: 95%;
box-sizing: border-box;
border-radius: 0.25em;
border: solid var(--background-darkest) thin;
background: var(--background-lighter);
button, .button
{
margin: auto;
border: none;
&:hover
{
background: var(--background-darker);
}
}
input
{
transition: background 0.2s ease;
width: 5em;
border-color: var(--background-darker);
outline: none;
text-align: center;
&:focus
{
background: var(--background-darker);
}
}
> ul
{
flex: 1;
margin: 0;
padding: 0;
list-style: none;
text-align: center;
vertical-align: middle;
> li
{
display: inline-block;
padding: 0;
vertical-align: middle;
button, .button
{
margin: auto 0.25em;
}
}
}
}