diff --git a/public/locales/en.json b/public/locales/en.json index dae6b84..d09c3b6 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -343,6 +343,7 @@ "showRelativeDatesDescription": "Everywhere on the site, if a date is less than 7 days ago, it will show as a relative date instead.", "transactionDefaultRaw": "Default to the 'Raw' tab instead of 'CommonMeta' on the transaction page", "defaultPageSize": "Default page size for table listings", + "tableHotkeys": "Enable table navigation hotkeys (left and right arrows).", "subMenuDebug": "Debug settings", "advancedWalletFormats": "Advanced wallet formats", diff --git a/src/pages/blocks/BlocksTable.tsx b/src/pages/blocks/BlocksTable.tsx index 7de362e..7eac690 100644 --- a/src/pages/blocks/BlocksTable.tsx +++ b/src/pages/blocks/BlocksTable.tsx @@ -38,7 +38,7 @@ order: lowest ? "ASC" : "DESC" }); - const { paginationTableProps } = useMalleablePagination( + const { paginationTableProps, hotkeys } = useMalleablePagination( res, res?.blocks, "blocks.tableTotal", options, setOptions, setPagination @@ -57,7 +57,7 @@ debug("results? %b res.blocks.length: %d res.count: %d res.total: %d", !!res, res?.blocks?.length, res?.count, res?.total); - return + const tbl = className="blocks-table" size="small" @@ -141,4 +141,9 @@ } ]} />; + + return <> + {tbl} + {hotkeys} + ; } diff --git a/src/pages/names/NamesTable.tsx b/src/pages/names/NamesTable.tsx index 97ef21b..f653f8d 100644 --- a/src/pages/names/NamesTable.tsx +++ b/src/pages/names/NamesTable.tsx @@ -40,7 +40,7 @@ order: sortNew ? "DESC" : "ASC" }); - const { paginationTableProps } = useMalleablePagination( + const { paginationTableProps, hotkeys } = useMalleablePagination( res, res?.names, "names.tableTotal", options, setOptions, setPagination @@ -59,7 +59,7 @@ debug("results? %b res.names.length: %d res.count: %d res.total: %d", !!res, res?.names?.length, res?.count, res?.total); - return + const tbl = className="names-table" size="small" @@ -162,4 +162,9 @@ } ]} />; + + return <> + {tbl} + {hotkeys} + ; } diff --git a/src/pages/settings/SettingsPage.tsx b/src/pages/settings/SettingsPage.tsx index 80b8ad6..ca211e4 100644 --- a/src/pages/settings/SettingsPage.tsx +++ b/src/pages/settings/SettingsPage.tsx @@ -107,6 +107,11 @@ + + {/* Enable table navigation hotkeys (left and right arrows) */} + + + {/* Debug settings */} diff --git a/src/pages/transactions/TransactionsTable.tsx b/src/pages/transactions/TransactionsTable.tsx index 4825e26..931b58a 100644 --- a/src/pages/transactions/TransactionsTable.tsx +++ b/src/pages/transactions/TransactionsTable.tsx @@ -84,7 +84,7 @@ order: "DESC" }); - const { paginationTableProps } = useMalleablePagination( + const { paginationTableProps, hotkeys } = useMalleablePagination( res, res?.transactions, "transactions.tableTotal", options, setOptions, setPagination @@ -118,7 +118,7 @@ debug("results? %b res.transactions.length: %d res.count: %d res.total: %d", !!res, res?.transactions?.length, res?.count, res?.total); - return + const tbl = className="transactions-table" size="small" @@ -229,4 +229,9 @@ } ]} />; + + return <> + {tbl} + {hotkeys} + ; } diff --git a/src/utils/settings.ts b/src/utils/settings.ts index f38e89a..f9ac8fc 100644 --- a/src/utils/settings.ts +++ b/src/utils/settings.ts @@ -48,6 +48,8 @@ readonly transactionDefaultRaw: boolean; /** Default page size for table listings. */ readonly defaultPageSize: number; + /** Enable table navigation hotkeys (left and right arrows). */ + readonly tableHotkeys: boolean; // =========================================================================== // DEBUG SETTINGS @@ -69,6 +71,7 @@ showRelativeDates: false, transactionDefaultRaw: false, defaultPageSize: 15, + tableHotkeys: true, walletFormats: false }; diff --git a/src/utils/table.tsx b/src/utils/table.tsx index 7563865..7080f08 100644 --- a/src/utils/table.tsx +++ b/src/utils/table.tsx @@ -1,13 +1,15 @@ // Copyright (c) 2020-2021 Drew Lemmy // This file is part of KristWeb 2 under GPL-3.0. // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt -import { useState, useEffect, useMemo, Dispatch, SetStateAction } from "react"; +import { useState, useEffect, useCallback, useMemo, Dispatch, SetStateAction } from "react"; import { TablePaginationConfig, TableProps, Pagination } from "antd"; import { SorterResult } from "antd/lib/table/interface"; import usePagination from "antd/lib/table/hooks/usePagination"; import { useTranslation, TFunction } from "react-i18next"; -import { useIntegerSetting } from "./settings"; +import { useIntegerSetting, useBooleanSetting } from "./settings"; + +import { GlobalHotKeys } from "react-hotkeys"; import { useHistory, useLocation } from "react-router-dom"; @@ -97,6 +99,7 @@ setPagination?: Dispatch> ): { paginationTableProps: Pick, "onChange" | "pagination">; + hotkeys: JSX.Element | null; } { const { t } = useTranslation(); @@ -131,6 +134,11 @@ } ); + const { hotkeys } = usePaginationHotkeys( + currentPageSize, res?.total || 0, + options, setOptions, setPaginationPos + ); + // Update the pagination useEffect(() => { if (setPagination) { @@ -145,7 +153,8 @@ paginationTableProps: { onChange: handleLookupTableChange(defaultPageSize, setOptions, setPaginationPos), pagination: paginationConfig - } + }, + hotkeys }; } @@ -233,3 +242,64 @@ return { options, setOptions: wrappedSetOptions }; } + +/** Provides a GlobalHotKeys component that will add the left and right arrow + * key hotkeys, allowing keyboard control of the table's pagination. */ +function usePaginationHotkeys( + defaultPageSize: number, + total: number | undefined, + options: LookupFilterOptionsBase, + setOptions: (opts: LookupFilterOptionsBase) => void, + setPaginationPos?: Dispatch> +): { hotkeys: JSX.Element | null } { + const enableHotkeys = useBooleanSetting("tableHotkeys"); + + const navigate = useCallback((direction: "prev" | "next") => { + const mul = direction === "next" ? 1 : -1; + const pageSize = options?.limit ?? defaultPageSize; + + // The offset for the lookup options + const minOffset = 0; + const maxOffset = (total || 1) - 1; // TODO: this isn't quite correct + const newOffsetRaw = (options?.offset || 0) + (pageSize * mul); + const newOffset = Math.min(Math.max(newOffsetRaw, minOffset), maxOffset); + + // The page number for paginationPos + const newPage = Math.max(Math.floor(newOffset / pageSize) + 1, 1); + + debug( + "hotkeys navigating %s (%d) across %d entries (%d total) ||| " + + "old offset: %d new offset: %d (%d) new page: %d ||| " + + "min is: %d max is: %d", + direction, mul, pageSize, total, + options?.offset, newOffset, newOffsetRaw, newPage, + minOffset, maxOffset + ); + + // Update the table and pagination + setOptions({ ...options, offset: newOffset }); + setPaginationPos?.({ current: newPage, pageSize }); + }, [defaultPageSize, total, options, setOptions, setPaginationPos]); + + // Enforce that the hotkeys get the newest `navigate` function, especially + // when the `total` changes + const hotkeys = useMemo(() => ( + { e?.preventDefault(); navigate("prev"); }, + NEXT: e => { e?.preventDefault(); navigate("next"); } + }} + /> + ), [navigate]); + + return { + hotkeys: enableHotkeys ? hotkeys : null + }; +}