@vinorcola/dynamic-table 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/DefaultTheme.css +6 -0
- package/lib/ColumnDefinition.d.ts +22 -0
- package/lib/ColumnDefinition.js +3 -0
- package/lib/DefaultTheme.d.ts +110 -0
- package/lib/DefaultTheme.js +197 -0
- package/lib/Dictionary.d.ts +10 -0
- package/lib/Dictionary.js +7 -0
- package/lib/Icon.d.ts +13 -0
- package/lib/Icon.js +40 -0
- package/lib/UniquePopupProvider.d.ts +22 -0
- package/lib/UniquePopupProvider.js +33 -0
- package/lib/index.d.ts +52 -0
- package/lib/index.js +43 -0
- package/lib/useColumns.d.ts +36 -0
- package/lib/useColumns.js +60 -0
- package/lib/useFilterState.d.ts +61 -0
- package/lib/useFilterState.js +85 -0
- package/lib/useItems.d.ts +41 -0
- package/lib/useItems.js +88 -0
- package/lib/useMaskableColumns.d.ts +34 -0
- package/lib/useMaskableColumns.js +34 -0
- package/lib/usePagination.d.ts +22 -0
- package/lib/usePagination.js +29 -0
- package/lib/useSortState.d.ts +41 -0
- package/lib/useSortState.js +75 -0
- package/package.json +53 -0
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
@theme {
|
|
2
|
+
--color-table-header: var(--color-stone-300);
|
|
3
|
+
--color-table-line-even: color-mix(in oklab, var(--color-stone-300), transparent 70%);
|
|
4
|
+
--color-table-line-odd: color-mix(in oklab, var(--color-stone-100), transparent 70%);
|
|
5
|
+
--color-table-line-hover: color-mix(in oklab, var(--color-stone-500), transparent 50%);
|
|
6
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { BaseItem, Dictionary, ItemKey, Primitive } from ".";
|
|
2
|
+
export type ValueResolver<Item extends BaseItem, Value extends Primitive> = (item: Item) => Value | null;
|
|
3
|
+
interface BaseColumneDefinition<Value extends Primitive> {
|
|
4
|
+
readonly title: string;
|
|
5
|
+
readonly dictionary?: Dictionary<Value> | Promise<Dictionary<Value>>;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* A column that will access an item's attribute.
|
|
9
|
+
*/
|
|
10
|
+
interface AccessorColumnDefinition<Item extends BaseItem, Value extends Primitive> extends BaseColumneDefinition<Value> {
|
|
11
|
+
readonly id: ItemKey<Item>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A column where the value must be resolved.
|
|
15
|
+
*/
|
|
16
|
+
interface ResolvedColumnDefinition<Item extends BaseItem, Value extends Primitive> extends BaseColumneDefinition<Value> {
|
|
17
|
+
readonly id: string;
|
|
18
|
+
readonly resolveValue: (item: Item) => Value | null;
|
|
19
|
+
}
|
|
20
|
+
export type ColumnDefinition<Item extends BaseItem, Value extends Primitive> = AccessorColumnDefinition<Item, Value> | ResolvedColumnDefinition<Item, Value>;
|
|
21
|
+
export declare function isAccessorColumnDefinition<Item extends BaseItem, Value extends Primitive>(definition: ColumnDefinition<Item, Value>): definition is AccessorColumnDefinition<Item, Value>;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { InternalColumn, InternalColumns } from "./useColumns";
|
|
3
|
+
import { type FilterType } from "./useFilterState";
|
|
4
|
+
import { type InternalSortableColumn } from "./useSortState";
|
|
5
|
+
import type { BaseItem, Dictionary, Primitive } from ".";
|
|
6
|
+
export interface Props {
|
|
7
|
+
children?: ReactNode;
|
|
8
|
+
}
|
|
9
|
+
export declare function TableContainer(props: Props): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
export interface ControllerProps<Item extends BaseItem> {
|
|
11
|
+
columns: InternalColumns<Item>;
|
|
12
|
+
clearFilterState: () => void;
|
|
13
|
+
clearSortState: () => void;
|
|
14
|
+
}
|
|
15
|
+
export declare function Controller<Item extends BaseItem>(props: ControllerProps<Item>): import("react/jsx-runtime").JSX.Element;
|
|
16
|
+
export interface ColumnsPopupProps<Item extends BaseItem> {
|
|
17
|
+
columns: InternalColumns<Item>;
|
|
18
|
+
onDismiss: () => void;
|
|
19
|
+
}
|
|
20
|
+
export declare function ColumnsPopup<Item extends BaseItem>(props: ColumnsPopupProps<Item>): import("react/jsx-runtime").JSX.Element;
|
|
21
|
+
export declare function Table(props: Props): import("react/jsx-runtime").JSX.Element;
|
|
22
|
+
export declare function HeaderContainer(props: Props): import("react/jsx-runtime").JSX.Element;
|
|
23
|
+
export declare function HeaderLine(props: Props): import("react/jsx-runtime").JSX.Element;
|
|
24
|
+
export interface HeaderProps<Item extends BaseItem, Value extends Primitive> extends Props {
|
|
25
|
+
column: InternalColumn<Item, Value>;
|
|
26
|
+
}
|
|
27
|
+
export declare function Header<Item extends BaseItem, Value extends Primitive>(props: HeaderProps<Item, Value>): import("react/jsx-runtime").JSX.Element;
|
|
28
|
+
export interface HeaderActionsProps {
|
|
29
|
+
filterType?: FilterType;
|
|
30
|
+
onFilterPopupToggle?: () => void;
|
|
31
|
+
onDisplayToggle?: () => void;
|
|
32
|
+
}
|
|
33
|
+
export declare function HeaderActions(props: HeaderActionsProps): import("react/jsx-runtime").JSX.Element;
|
|
34
|
+
export interface HeaderStatusProps {
|
|
35
|
+
filterType?: FilterType;
|
|
36
|
+
sortState?: NonNullable<InternalSortableColumn<BaseItem, Primitive>["sorted"]>;
|
|
37
|
+
}
|
|
38
|
+
export declare function HeaderStatus(props: HeaderStatusProps): import("react/jsx-runtime").JSX.Element | null;
|
|
39
|
+
export interface SearchFilterPopupProps {
|
|
40
|
+
searchText: string;
|
|
41
|
+
onSearchTextChange: (searchText: string) => void;
|
|
42
|
+
onDismiss: () => void;
|
|
43
|
+
}
|
|
44
|
+
export declare function SearchFilterPopup(props: SearchFilterPopupProps): import("react/jsx-runtime").JSX.Element;
|
|
45
|
+
export interface SelectionFilterPopupProps<Value extends Primitive> {
|
|
46
|
+
dictionary: Dictionary<Value> | null;
|
|
47
|
+
hiddenValues: Value[];
|
|
48
|
+
onHiddenValuesChange: (hiddenValues: Value[]) => void;
|
|
49
|
+
onDismiss: () => void;
|
|
50
|
+
}
|
|
51
|
+
export declare function SelectionFilterPopup<Value extends Primitive>(props: SelectionFilterPopupProps<Value>): import("react/jsx-runtime").JSX.Element;
|
|
52
|
+
export declare function BodyContainer(props: Props): import("react/jsx-runtime").JSX.Element;
|
|
53
|
+
export declare function Line(props: Props): import("react/jsx-runtime").JSX.Element;
|
|
54
|
+
export declare function Cell(props: Props): import("react/jsx-runtime").JSX.Element;
|
|
55
|
+
export interface ClickableProps extends Props {
|
|
56
|
+
target: string;
|
|
57
|
+
}
|
|
58
|
+
export declare function ClickableCell(props: ClickableProps): import("react/jsx-runtime").JSX.Element;
|
|
59
|
+
export interface FooterContainerProps {
|
|
60
|
+
totalColums: number;
|
|
61
|
+
pageSelector: ReactNode;
|
|
62
|
+
itemsPerPageSelector: ReactNode;
|
|
63
|
+
}
|
|
64
|
+
export declare function FooterContainer(props: FooterContainerProps): import("react/jsx-runtime").JSX.Element;
|
|
65
|
+
export interface ItemsPerPageSelectorProps extends Props {
|
|
66
|
+
itemsPerPage: number;
|
|
67
|
+
onItemsPerPageChange: (itemsPerPage: number) => void;
|
|
68
|
+
}
|
|
69
|
+
export declare function ItemsPerPageSelector(props: ItemsPerPageSelectorProps): import("react/jsx-runtime").JSX.Element;
|
|
70
|
+
export interface PageSelectorProps extends Props {
|
|
71
|
+
totalPages: number;
|
|
72
|
+
currentPage: number;
|
|
73
|
+
onCurrentPageChange: (page: number) => void;
|
|
74
|
+
}
|
|
75
|
+
export declare function PageSelector(props: PageSelectorProps): import("react/jsx-runtime").JSX.Element | null;
|
|
76
|
+
export interface PageButtonProps extends Props {
|
|
77
|
+
enabled: boolean;
|
|
78
|
+
active?: boolean;
|
|
79
|
+
onClick: () => void;
|
|
80
|
+
}
|
|
81
|
+
export declare function PageButton(props: PageButtonProps): import("react/jsx-runtime").JSX.Element;
|
|
82
|
+
export interface ButtonProps extends Props {
|
|
83
|
+
className?: string;
|
|
84
|
+
disabled?: boolean;
|
|
85
|
+
onClick: () => void;
|
|
86
|
+
hoverChildren?: ReactNode;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* A customizable button.
|
|
90
|
+
*
|
|
91
|
+
* The button text color is defined using the `--text-color` css property. It uses by default `--color-base-content`
|
|
92
|
+
* css property or fallback to black if `--color-base-content` is not defined. You can override this value by giving an
|
|
93
|
+
* appropriate className, for example:
|
|
94
|
+
* `<DynamicTable.Button className="[--text-color:var(--color-stone-400)]">...</DynamicTable.Button>`
|
|
95
|
+
*
|
|
96
|
+
* The button background color is defined using the `--bg-color` css property. It uses by default `--color-stone-400`
|
|
97
|
+
* css property. You can override this value by giving an appropriate className, for example:
|
|
98
|
+
* `<DynamicTable.Button className="[--bg-color:var(--color-orange-400)]">...</DynamicTable.Button>`
|
|
99
|
+
* Note that the color defined by `--bg-color` will be used when the button is hovered. A less saturated color will be
|
|
100
|
+
* used for non-hovered button (by default, --bg-color mix with 50% transparent). You can also override the default
|
|
101
|
+
* background color (when button is non-hovered) by defining a custom `--bg-default-color`, for example:
|
|
102
|
+
* `<DynamicTable.Button className="[--bg-default-color:transparent]">...</DynamicTable.Button>`
|
|
103
|
+
*/
|
|
104
|
+
export declare function Button(props: ButtonProps): import("react/jsx-runtime").JSX.Element;
|
|
105
|
+
export interface PopupProps extends Props {
|
|
106
|
+
className?: string;
|
|
107
|
+
onDismiss: () => void;
|
|
108
|
+
}
|
|
109
|
+
export declare function Popup(props: PopupProps): import("react/jsx-runtime").JSX.Element;
|
|
110
|
+
export declare function Loader(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import cx from "@vinorcola/utils/classNames";
|
|
3
|
+
import { useEffect, useMemo } from "react";
|
|
4
|
+
import { AscSortIcon, CheckedIcon, CloseIcon, ColumnsIcon, DescSortIcon, FilterIcon, HideIcon, NextIcon, PreviousIcon, SearchIcon, SortIcon, UncheckedIcon, } from "./Icon";
|
|
5
|
+
import { usePopup } from "./UniquePopupProvider";
|
|
6
|
+
import { isSearchable, isSelectable } from "./useFilterState";
|
|
7
|
+
import { isMaskable } from "./useMaskableColumns";
|
|
8
|
+
import { isSortable } from "./useSortState";
|
|
9
|
+
import DynamicTable from ".";
|
|
10
|
+
export function TableContainer(props) {
|
|
11
|
+
return _jsx("div", { children: props.children });
|
|
12
|
+
}
|
|
13
|
+
export function Controller(props) {
|
|
14
|
+
const columnListPopup = usePopup();
|
|
15
|
+
const hasHiddenColumns = props.columns.some((column) => isMaskable(column) && !column.displayed);
|
|
16
|
+
const hasFilteredColumns = props.columns.some((column) => (isSearchable(column) && column.searchText !== null) ||
|
|
17
|
+
(isSelectable(column) && column.hiddenValues.length > 0));
|
|
18
|
+
const hasHiddenFilteredColumns = hasHiddenColumns &&
|
|
19
|
+
hasFilteredColumns &&
|
|
20
|
+
props.columns.some((column) => isMaskable(column) &&
|
|
21
|
+
!column.displayed &&
|
|
22
|
+
((isSearchable(column) && column.searchText !== null) ||
|
|
23
|
+
(isSelectable(column) && column.hiddenValues.length > 0)));
|
|
24
|
+
const hasSortedColumns = props.columns.some((column) => isSortable(column) && column.sorted !== null);
|
|
25
|
+
const hasHiddenSortedColumns = hasHiddenColumns &&
|
|
26
|
+
hasSortedColumns &&
|
|
27
|
+
props.columns.some((column) => isMaskable(column) && !column.displayed && isSortable(column) && column.sorted !== null);
|
|
28
|
+
return (_jsxs("div", { className: "relative", children: [_jsxs("ul", { className: "flex justify-end", children: [_jsx("li", { children: _jsx(DynamicTable.Button, { className: cx(hasHiddenFilteredColumns
|
|
29
|
+
? "[--bg-color:var(--color-orange-400)]"
|
|
30
|
+
: hasFilteredColumns && "[--bg-color:var(--color-lime-400)]"), disabled: !hasFilteredColumns, hoverChildren: _jsx(CloseIcon, {}), onClick: props.clearFilterState, children: _jsx(FilterIcon, {}) }) }), _jsx("li", { children: _jsx(DynamicTable.Button, { className: cx(hasHiddenSortedColumns
|
|
31
|
+
? "[--bg-color:var(--color-orange-400)]"
|
|
32
|
+
: hasSortedColumns && "[--bg-color:var(--color-sky-400)]"), disabled: !hasSortedColumns, hoverChildren: _jsx(CloseIcon, {}), onClick: props.clearSortState, children: _jsx(SortIcon, {}) }) }), _jsx("li", { children: _jsx(DynamicTable.Button, { className: cx(hasHiddenColumns
|
|
33
|
+
? "[--bg-color:var(--color-orange-400)]"
|
|
34
|
+
: "[--bg-default-color:transparent]"), onClick: columnListPopup.show, children: _jsx(ColumnsIcon, {}) }) })] }), columnListPopup.display && (_jsx(DynamicTable.ColumnsPopup, { columns: props.columns, onDismiss: columnListPopup.dismiss }))] }));
|
|
35
|
+
}
|
|
36
|
+
export function ColumnsPopup(props) {
|
|
37
|
+
return (_jsx(DynamicTable.Popup, { className: "right-2", onDismiss: props.onDismiss, children: _jsx("main", { children: _jsx("ul", { className: "min-w-40", children: props.columns.map((column) => {
|
|
38
|
+
const maskable = isMaskable(column);
|
|
39
|
+
const displayed = !maskable || column.displayed;
|
|
40
|
+
const sorted = isSortable(column) && column.sorted !== null;
|
|
41
|
+
const searched = isSearchable(column) && column.searchText !== null;
|
|
42
|
+
const selected = isSelectable(column) && column.hiddenValues.length > 0;
|
|
43
|
+
return (_jsxs("li", { className: cx("flex items-center gap-1 pl-2 pr-6 py-0.5", maskable && "cursor-pointer", searched || selected
|
|
44
|
+
? displayed
|
|
45
|
+
? "bg-lime-200 hover:bg-lime-300"
|
|
46
|
+
: "bg-orange-200 hover:bg-orange-300"
|
|
47
|
+
: sorted
|
|
48
|
+
? displayed
|
|
49
|
+
? "bg-sky-200 hover:bg-sky-300"
|
|
50
|
+
: "bg-orange-200 hover:bg-orange-300"
|
|
51
|
+
: "hover:bg-stone-500/40"), onClick: maskable ? () => column.onDisplayToggle() : undefined, children: [displayed ? _jsx(CheckedIcon, {}) : _jsx(UncheckedIcon, {}), column.title, (sorted || searched || selected) && (_jsxs("span", { className: "absolute right-1", children: [sorted &&
|
|
52
|
+
(column.sorted.direction === "asc" ? _jsx(AscSortIcon, {}) : _jsx(DescSortIcon, {})), searched && (_jsx("span", { className: "text-xs", children: _jsx(SearchIcon, {}) })), selected && (_jsx("span", { className: "text-xs", children: _jsx(FilterIcon, {}) }))] }))] }, column.id));
|
|
53
|
+
}) }) }) }));
|
|
54
|
+
}
|
|
55
|
+
export function Table(props) {
|
|
56
|
+
return _jsx("table", { className: "table table-pin-rows", children: props.children });
|
|
57
|
+
}
|
|
58
|
+
export function HeaderContainer(props) {
|
|
59
|
+
return _jsx("thead", { children: props.children });
|
|
60
|
+
}
|
|
61
|
+
export function HeaderLine(props) {
|
|
62
|
+
return _jsx("tr", { className: "bg-table-header", children: props.children });
|
|
63
|
+
}
|
|
64
|
+
export function Header(props) {
|
|
65
|
+
const filterPopup = usePopup();
|
|
66
|
+
const column = props.column;
|
|
67
|
+
const sortable = isSortable(column);
|
|
68
|
+
const searchable = isSearchable(column);
|
|
69
|
+
const selectable = isSelectable(column);
|
|
70
|
+
const maskable = isMaskable(column);
|
|
71
|
+
return (_jsxs("th", { className: cx("relative pr-8 hover:[&_.column-actions]:inline", sortable && "hover:bg-stone-500/20 cursor-pointer"), onClick: sortable ? () => column.onSortToggle() : undefined, children: [props.children, _jsxs("span", { className: "absolute top-0 bottom-0 right-0 px-1 flex items-center gap-1.5", children: [_jsx(DynamicTable.HeaderActions, { filterType: searchable ? "search" : selectable ? "selection" : undefined, onFilterPopupToggle: searchable || selectable ? filterPopup.show : undefined, onDisplayToggle: maskable ? column.onDisplayToggle : undefined }), _jsx(DynamicTable.HeaderStatus, { filterType: searchable && column.searchText !== null
|
|
72
|
+
? "search"
|
|
73
|
+
: selectable && column.hiddenValues.length > 0
|
|
74
|
+
? "selection"
|
|
75
|
+
: undefined, sortState: sortable && column.sorted !== null ? column.sorted : undefined })] }), filterPopup.display &&
|
|
76
|
+
(searchable ? (_jsx(DynamicTable.SearchFilterPopup, { searchText: column.searchText ?? "", onSearchTextChange: column.onSearchTextChange, onDismiss: filterPopup.dismiss })) : (selectable && (_jsx(DynamicTable.SelectionFilterPopup, { dictionary: column.dictionary instanceof Map ? column.dictionary : null, hiddenValues: column.hiddenValues ?? [], onHiddenValuesChange: column.onSelectionChange, onDismiss: filterPopup.dismiss }))))] }));
|
|
77
|
+
}
|
|
78
|
+
export function HeaderActions(props) {
|
|
79
|
+
return (_jsxs("span", { className: "column-actions hidden", children: [props.filterType !== undefined && props.onFilterPopupToggle !== undefined && (_jsx(DynamicTable.Button, { className: "[--text-color:color-mix(in_oklab,var(--color-base-content,black)_60%,transparent)] [--bg-default-color:color-mix(in_oklab,color-mix(in_oklab,var(--color-table-header),var(--color-stone-500)_20%)_40%,transparent)]", onClick: props.onFilterPopupToggle, children: props.filterType === "search" ? _jsx(SearchIcon, {}) : _jsx(FilterIcon, {}) })), props.onDisplayToggle !== undefined && (_jsx(DynamicTable.Button, { className: "[--text-color:color-mix(in_oklab,var(--color-base-content,black)_60%,transparent)] [--bg-default-color:color-mix(in_oklab,color-mix(in_oklab,var(--color-table-header),var(--color-stone-500)_20%)_40%,transparent)]", onClick: props.onDisplayToggle, children: _jsx(HideIcon, {}) }))] }));
|
|
80
|
+
}
|
|
81
|
+
export function HeaderStatus(props) {
|
|
82
|
+
if (props.filterType === undefined && props.sortState === undefined) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
return (_jsxs("span", { className: "h-full pb-1 flex flex-col items-end justify-between", children: [_jsxs("span", { className: "text-sm", children: [props.sortState !== undefined && props.sortState.totalSortedColumns > 1 && props.sortState.order, props.sortState !== undefined &&
|
|
86
|
+
(props.sortState.direction === "asc" ? _jsx(AscSortIcon, {}) : _jsx(DescSortIcon, {}))] }), _jsx("span", { className: "shrink text-xs", children: props.filterType !== undefined && (props.filterType === "search" ? _jsx(SearchIcon, {}) : _jsx(FilterIcon, {})) })] }));
|
|
87
|
+
}
|
|
88
|
+
export function SearchFilterPopup(props) {
|
|
89
|
+
return (_jsx(DynamicTable.Popup, { className: "right-2", onDismiss: props.onDismiss, children: _jsx("main", { children: _jsx("input", { type: "search", name: "search", autoFocus: true, value: props.searchText, onChange: (event) => {
|
|
90
|
+
props.onSearchTextChange(event.currentTarget.value);
|
|
91
|
+
} }) }) }));
|
|
92
|
+
}
|
|
93
|
+
export function SelectionFilterPopup(props) {
|
|
94
|
+
return (_jsx(DynamicTable.Popup, { className: "right-2", onDismiss: props.onDismiss, children: _jsx("main", { children: props.dictionary === null ? (_jsx(DynamicTable.Loader, {})) : (_jsx("ul", { className: "min-w-40", children: Array.from(props.dictionary, ([value, entry]) => {
|
|
95
|
+
const hidden = props.hiddenValues.includes(value);
|
|
96
|
+
return (_jsxs("li", { className: "flex items-center gap-1 px-2 py-0.5 cursor-pointer hover:bg-stone-500/40", onClick: () => {
|
|
97
|
+
props.onHiddenValuesChange(hidden
|
|
98
|
+
? props.hiddenValues.filter((val) => val !== value)
|
|
99
|
+
: [...props.hiddenValues, value]);
|
|
100
|
+
}, children: [hidden ? _jsx(UncheckedIcon, {}) : _jsx(CheckedIcon, {}), entry.prepend, entry.title] }, `${value}`));
|
|
101
|
+
}) })) }) }));
|
|
102
|
+
}
|
|
103
|
+
export function BodyContainer(props) {
|
|
104
|
+
return _jsx("tbody", { children: props.children });
|
|
105
|
+
}
|
|
106
|
+
export function Line(props) {
|
|
107
|
+
return (_jsx("tr", { className: "even:[&>*]:bg-table-line-even odd:[&>*]:bg-table-line-odd hover:bg-table-line-hover", children: props.children }));
|
|
108
|
+
}
|
|
109
|
+
export function Cell(props) {
|
|
110
|
+
return _jsx("td", { children: props.children });
|
|
111
|
+
}
|
|
112
|
+
export function ClickableCell(props) {
|
|
113
|
+
return (_jsx("td", { className: "!p-0", children: _jsx("a", { href: props.target, className: "block size-full px-4 py-2", children: props.children }) }));
|
|
114
|
+
}
|
|
115
|
+
export function FooterContainer(props) {
|
|
116
|
+
return (_jsx("tfoot", { children: _jsx("tr", { children: _jsxs("td", { className: "relative py-0! px-16 h-9 text-center", colSpan: props.totalColums, children: [props.pageSelector, props.itemsPerPageSelector] }) }) }));
|
|
117
|
+
}
|
|
118
|
+
const ITEM_PER_PAGE_CHOICES = [12, 24, 50, 100, 250, 500];
|
|
119
|
+
export function ItemsPerPageSelector(props) {
|
|
120
|
+
return (_jsx("select", { className: "absolute top-0 right-0 h-full w-14 bg-white outline-none", name: "items-per-page", value: props.itemsPerPage, onChange: (event) => {
|
|
121
|
+
props.onItemsPerPageChange(Number.parseInt(event.target.value, 10));
|
|
122
|
+
}, children: ITEM_PER_PAGE_CHOICES.map((choice) => (_jsx("option", { value: choice, children: choice }, choice))) }));
|
|
123
|
+
}
|
|
124
|
+
const DISPLAYED_PAGES = 5;
|
|
125
|
+
export function PageSelector(props) {
|
|
126
|
+
const visiblePages = useMemo(() => {
|
|
127
|
+
const interval = Math.floor(DISPLAYED_PAGES / 2);
|
|
128
|
+
const min = Math.max(1, Math.min(props.currentPage - interval, props.totalPages - 2 * interval));
|
|
129
|
+
const max = Math.min(props.totalPages, min + 2 * interval);
|
|
130
|
+
const pages = [];
|
|
131
|
+
for (let page = min; page <= max; ++page) {
|
|
132
|
+
pages.push(page);
|
|
133
|
+
}
|
|
134
|
+
return pages;
|
|
135
|
+
}, [props.totalPages, props.currentPage]);
|
|
136
|
+
if (props.totalPages <= 1) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
return (_jsxs("ul", { className: "inline-flex flex-wrap justify-center", children: [_jsx(DynamicTable.PageButton, { enabled: props.currentPage > 1, onClick: () => {
|
|
140
|
+
props.onCurrentPageChange(1);
|
|
141
|
+
}, children: "1" }), _jsx(DynamicTable.PageButton, { enabled: props.currentPage > 1, onClick: () => {
|
|
142
|
+
props.onCurrentPageChange(Math.max(1, props.currentPage - 1));
|
|
143
|
+
}, children: _jsx(PreviousIcon, {}) }), visiblePages.map((page) => (_jsx(DynamicTable.PageButton, { enabled: page !== props.currentPage, active: page === props.currentPage, onClick: () => {
|
|
144
|
+
props.onCurrentPageChange(page);
|
|
145
|
+
}, children: page }, page))), _jsx(DynamicTable.PageButton, { enabled: props.currentPage < props.totalPages, onClick: () => {
|
|
146
|
+
props.onCurrentPageChange(Math.min(props.totalPages, props.currentPage + 1));
|
|
147
|
+
}, children: _jsx(NextIcon, {}) }), _jsx(DynamicTable.PageButton, { enabled: props.currentPage < props.totalPages, onClick: () => {
|
|
148
|
+
props.onCurrentPageChange(props.totalPages);
|
|
149
|
+
}, children: props.totalPages })] }));
|
|
150
|
+
}
|
|
151
|
+
export function PageButton(props) {
|
|
152
|
+
return (_jsx("li", { className: cx(props.active ? "bg-stone-600 text-white" : "bg-white", (!props.active && props.enabled) || "text-stone-300"), children: _jsx("button", { type: "button", className: cx("flex items-center justify-center h-full min-w-10 p-2 border-none oultine-none", props.active || !props.enabled || "cursor-pointer hover:bg-stone-500/40"), disabled: !props.enabled, onClick: () => {
|
|
153
|
+
if (props.enabled) {
|
|
154
|
+
props.onClick();
|
|
155
|
+
}
|
|
156
|
+
}, children: props.children }) }));
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* A customizable button.
|
|
160
|
+
*
|
|
161
|
+
* The button text color is defined using the `--text-color` css property. It uses by default `--color-base-content`
|
|
162
|
+
* css property or fallback to black if `--color-base-content` is not defined. You can override this value by giving an
|
|
163
|
+
* appropriate className, for example:
|
|
164
|
+
* `<DynamicTable.Button className="[--text-color:var(--color-stone-400)]">...</DynamicTable.Button>`
|
|
165
|
+
*
|
|
166
|
+
* The button background color is defined using the `--bg-color` css property. It uses by default `--color-stone-400`
|
|
167
|
+
* css property. You can override this value by giving an appropriate className, for example:
|
|
168
|
+
* `<DynamicTable.Button className="[--bg-color:var(--color-orange-400)]">...</DynamicTable.Button>`
|
|
169
|
+
* Note that the color defined by `--bg-color` will be used when the button is hovered. A less saturated color will be
|
|
170
|
+
* used for non-hovered button (by default, --bg-color mix with 50% transparent). You can also override the default
|
|
171
|
+
* background color (when button is non-hovered) by defining a custom `--bg-default-color`, for example:
|
|
172
|
+
* `<DynamicTable.Button className="[--bg-default-color:transparent]">...</DynamicTable.Button>`
|
|
173
|
+
*/
|
|
174
|
+
export function Button(props) {
|
|
175
|
+
return (_jsx("button", { type: "button", className: cx(props.className?.includes("[--text-color:") || "[--text-color:var(--color-base-content,black)]", props.className?.includes("[--bg-color:") || "[--bg-color:var(--color-stone-400)]", props.className?.includes("[--bg-default-color:") ||
|
|
176
|
+
"[--bg-default-color:color-mix(in_oklab,var(--bg-color)_50%,transparent)]", "inline-flex items-center justify-center size-6 rounded-full [&_.hover-content]:hidden hover:[&_.default-content]:hidden hover:[&_.hover-content]:inline-flex", props.disabled
|
|
177
|
+
? "text-(--text-color)/50"
|
|
178
|
+
: "text-(--text-color) bg-(--bg-default-color) hover:bg-(--bg-color) cursor-pointer", props.className), disabled: props.disabled, onClick: (event) => {
|
|
179
|
+
event.preventDefault();
|
|
180
|
+
event.stopPropagation();
|
|
181
|
+
props.onClick();
|
|
182
|
+
}, children: props.hoverChildren && !props.disabled ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "inline-flex default-content", children: props.children }), _jsx("span", { className: "inline-flex hover-content", children: props.hoverChildren })] })) : (props.children) }));
|
|
183
|
+
}
|
|
184
|
+
export function Popup(props) {
|
|
185
|
+
useEffect(() => {
|
|
186
|
+
window.addEventListener("click", props.onDismiss);
|
|
187
|
+
return () => {
|
|
188
|
+
window.removeEventListener("click", props.onDismiss);
|
|
189
|
+
};
|
|
190
|
+
}, [props.onDismiss]);
|
|
191
|
+
return (_jsx("div", { className: cx("absolute max-w-xs max-h-[60vh] border border-stone-400 rounded bg-base-100 overflow-auto z-50", props.className), onClick: (event) => {
|
|
192
|
+
event.stopPropagation();
|
|
193
|
+
}, children: props.children }));
|
|
194
|
+
}
|
|
195
|
+
export function Loader() {
|
|
196
|
+
return _jsx("span", { children: "Loading\u2026" });
|
|
197
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { Primitive } from ".";
|
|
3
|
+
export interface DictionaryEntry {
|
|
4
|
+
title: string;
|
|
5
|
+
prepend?: ReactNode;
|
|
6
|
+
}
|
|
7
|
+
export default class Dictionary<Value extends Primitive> extends Map<Value, DictionaryEntry> {
|
|
8
|
+
readonly unknownMessage: string;
|
|
9
|
+
constructor(unknownMessage: string, entries?: readonly (readonly [Value, DictionaryEntry])[] | null);
|
|
10
|
+
}
|
package/lib/Icon.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const DEFAULT_CLASS = "inline grow-0 shrink-0 size-[1em]";
|
|
2
|
+
export declare function CheckedIcon(): import("react/jsx-runtime").JSX.Element;
|
|
3
|
+
export declare function UncheckedIcon(): import("react/jsx-runtime").JSX.Element;
|
|
4
|
+
export declare function CloseIcon(): import("react/jsx-runtime").JSX.Element;
|
|
5
|
+
export declare function ColumnsIcon(): import("react/jsx-runtime").JSX.Element;
|
|
6
|
+
export declare function FilterIcon(): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
export declare function HideIcon(): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
export declare function NextIcon(): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
export declare function PreviousIcon(): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
export declare function SearchIcon(): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
export declare function SortIcon(): import("react/jsx-runtime").JSX.Element;
|
|
12
|
+
export declare function AscSortIcon(): import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
export declare function DescSortIcon(): import("react/jsx-runtime").JSX.Element;
|
package/lib/Icon.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com
|
|
3
|
+
// License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.
|
|
4
|
+
export const DEFAULT_CLASS = "inline grow-0 shrink-0 size-[1em]";
|
|
5
|
+
export function CheckedIcon() {
|
|
6
|
+
return (_jsx("svg", { className: DEFAULT_CLASS, xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 448 512", children: _jsx("path", { d: "M64 32C28.7 32 0 60.7 0 96L0 416c0 35.3 28.7 64 64 64l320 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64L64 32zM337 209L209 337c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L303 175c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z", fill: "currentColor" }) }));
|
|
7
|
+
}
|
|
8
|
+
export function UncheckedIcon() {
|
|
9
|
+
return (_jsx("svg", { className: DEFAULT_CLASS, xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 448 512", children: _jsx("path", { d: "M0 96C0 60.7 28.7 32 64 32H384c35.3 0 64 28.7 64 64V416c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V96z", fill: "currentColor" }) }));
|
|
10
|
+
}
|
|
11
|
+
export function CloseIcon() {
|
|
12
|
+
return (_jsx("svg", { className: DEFAULT_CLASS, xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 384 512", children: _jsx("path", { d: "M342.6 150.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L192 210.7 86.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L146.7 256 41.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 301.3 297.4 406.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L237.3 256 342.6 150.6z", fill: "currentColor" }) }));
|
|
13
|
+
}
|
|
14
|
+
export function ColumnsIcon() {
|
|
15
|
+
return (_jsx("svg", { className: DEFAULT_CLASS, xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 512 512", children: _jsx("path", { d: "M0 96C0 60.7 28.7 32 64 32l384 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zm64 64l0 256 160 0 0-256L64 160zm384 0l-160 0 0 256 160 0 0-256z", fill: "currentColor" }) }));
|
|
16
|
+
}
|
|
17
|
+
export function FilterIcon() {
|
|
18
|
+
return (_jsx("svg", { className: DEFAULT_CLASS, xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 512 512", children: _jsx("path", { d: "M3.9 54.9C10.5 40.9 24.5 32 40 32l432 0c15.5 0 29.5 8.9 36.1 22.9s4.6 30.5-5.2 42.5L320 320.9 320 448c0 12.1-6.8 23.2-17.7 28.6s-23.8 4.3-33.5-3l-64-48c-8.1-6-12.8-15.5-12.8-25.6l0-79.1L9 97.3C-.7 85.4-2.8 68.8 3.9 54.9z", fill: "currentColor" }) }));
|
|
19
|
+
}
|
|
20
|
+
export function HideIcon() {
|
|
21
|
+
return (_jsx("svg", { className: DEFAULT_CLASS, xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 640 512", children: _jsx("path", { d: "M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L525.6 386.7c39.6-40.6 66.4-86.1 79.9-118.4c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C465.5 68.8 400.8 32 320 32c-68.2 0-125 26.3-169.3 60.8L38.8 5.1zM223.1 149.5C248.6 126.2 282.7 112 320 112c79.5 0 144 64.5 144 144c0 24.9-6.3 48.3-17.4 68.7L408 294.5c8.4-19.3 10.6-41.4 4.8-63.3c-11.1-41.5-47.8-69.4-88.6-71.1c-5.8-.2-9.2 6.1-7.4 11.7c2.1 6.4 3.3 13.2 3.3 20.3c0 10.2-2.4 19.8-6.6 28.3l-90.3-70.8zM373 389.9c-16.4 6.5-34.3 10.1-53 10.1c-79.5 0-144-64.5-144-144c0-6.9 .5-13.6 1.4-20.2L83.1 161.5C60.3 191.2 44 220.8 34.5 243.7c-3.3 7.9-3.3 16.7 0 24.6c14.9 35.7 46.2 87.7 93 131.1C174.5 443.2 239.2 480 320 480c47.8 0 89.9-12.9 126.2-32.5L373 389.9z", fill: "currentColor" }) }));
|
|
22
|
+
}
|
|
23
|
+
export function NextIcon() {
|
|
24
|
+
return (_jsx("svg", { className: DEFAULT_CLASS, xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 320 512", children: _jsx("path", { d: "M278.6 233.4c12.5 12.5 12.5 32.8 0 45.3l-160 160c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L210.7 256 73.4 118.6c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l160 160z", fill: "currentColor" }) }));
|
|
25
|
+
}
|
|
26
|
+
export function PreviousIcon() {
|
|
27
|
+
return (_jsx("svg", { className: DEFAULT_CLASS, xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 320 512", children: _jsx("path", { d: "M41.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l160 160c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L109.3 256 246.6 118.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-160 160z", fill: "currentColor" }) }));
|
|
28
|
+
}
|
|
29
|
+
export function SearchIcon() {
|
|
30
|
+
return (_jsx("svg", { className: DEFAULT_CLASS, xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 512 512", children: _jsx("path", { d: "M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z", fill: "currentColor" }) }));
|
|
31
|
+
}
|
|
32
|
+
export function SortIcon() {
|
|
33
|
+
return (_jsx("svg", { className: DEFAULT_CLASS, xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 320 512", children: _jsx("path", { d: "M137.4 41.4c12.5-12.5 32.8-12.5 45.3 0l128 128c9.2 9.2 11.9 22.9 6.9 34.9s-16.6 19.8-29.6 19.8L32 224c-12.9 0-24.6-7.8-29.6-19.8s-2.2-25.7 6.9-34.9l128-128zm0 429.3l-128-128c-9.2-9.2-11.9-22.9-6.9-34.9s16.6-19.8 29.6-19.8l256 0c12.9 0 24.6 7.8 29.6 19.8s2.2 25.7-6.9 34.9l-128 128c-12.5 12.5-32.8 12.5-45.3 0z", fill: "currentColor" }) }));
|
|
34
|
+
}
|
|
35
|
+
export function AscSortIcon() {
|
|
36
|
+
return (_jsx("svg", { className: DEFAULT_CLASS, xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 320 512", children: _jsx("path", { d: "M182.6 41.4c-12.5-12.5-32.8-12.5-45.3 0l-128 128c-9.2 9.2-11.9 22.9-6.9 34.9s16.6 19.8 29.6 19.8l256 0c12.9 0 24.6-7.8 29.6-19.8s2.2-25.7-6.9-34.9l-128-128z", fill: "currentColor" }) }));
|
|
37
|
+
}
|
|
38
|
+
export function DescSortIcon() {
|
|
39
|
+
return (_jsx("svg", { className: DEFAULT_CLASS, xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 320 512", children: _jsx("path", { d: "M182.6 470.6c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-9.2-9.2-11.9-22.9-6.9-34.9s16.6-19.8 29.6-19.8l256 0c12.9 0 24.6 7.8 29.6 19.8s2.2 25.7-6.9 34.9l-128 128z", fill: "currentColor" }) }));
|
|
40
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
type Callback = () => void;
|
|
3
|
+
type PopupState = {
|
|
4
|
+
display: true;
|
|
5
|
+
show: Callback;
|
|
6
|
+
dismiss: Callback;
|
|
7
|
+
} | {
|
|
8
|
+
display: false;
|
|
9
|
+
show: Callback;
|
|
10
|
+
dismiss: null;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* A hook that eases popup display.
|
|
14
|
+
*
|
|
15
|
+
* It provides a display boolean and show & dismiss functions. It unsures that only one popup is displayed at a time.
|
|
16
|
+
*/
|
|
17
|
+
export declare function usePopup(): PopupState;
|
|
18
|
+
interface Props {
|
|
19
|
+
children?: ReactNode;
|
|
20
|
+
}
|
|
21
|
+
export default function UniquePopupProvider(props: Props): import("react/jsx-runtime").JSX.Element;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, createRef, useCallback, useContext, useRef, useState, } from "react";
|
|
3
|
+
/**
|
|
4
|
+
* A context for saving the currently opened popup's dismiss function.
|
|
5
|
+
*/
|
|
6
|
+
const PopupContext = createContext(createRef());
|
|
7
|
+
/**
|
|
8
|
+
* A hook that eases popup display.
|
|
9
|
+
*
|
|
10
|
+
* It provides a display boolean and show & dismiss functions. It unsures that only one popup is displayed at a time.
|
|
11
|
+
*/
|
|
12
|
+
// eslint-disable-next-line react-refresh/only-export-components
|
|
13
|
+
export function usePopup() {
|
|
14
|
+
const dismiss = useContext(PopupContext);
|
|
15
|
+
const [display, setDisplay] = useState(false);
|
|
16
|
+
return {
|
|
17
|
+
display,
|
|
18
|
+
show: useCallback(() => {
|
|
19
|
+
if (dismiss.current !== null) {
|
|
20
|
+
dismiss.current();
|
|
21
|
+
}
|
|
22
|
+
dismiss.current = () => {
|
|
23
|
+
setDisplay(false);
|
|
24
|
+
dismiss.current = null;
|
|
25
|
+
};
|
|
26
|
+
setDisplay(true);
|
|
27
|
+
}, [dismiss]),
|
|
28
|
+
dismiss: dismiss.current,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export default function UniquePopupProvider(props) {
|
|
32
|
+
return _jsx(PopupContext, { value: useRef(null), children: props.children });
|
|
33
|
+
}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Key as ReactKey } from "react";
|
|
2
|
+
import type { ColumnDefinition } from "./ColumnDefinition";
|
|
3
|
+
import { type FilterState } from "./useFilterState";
|
|
4
|
+
import type { ColumnsMaskState } from "./useMaskableColumns";
|
|
5
|
+
import { type PaginationState } from "./usePagination";
|
|
6
|
+
import { type SortState } from "./useSortState";
|
|
7
|
+
export type Primitive = boolean | Date | number | string;
|
|
8
|
+
export type BaseItem = Record<string, any> & {
|
|
9
|
+
readonly id: ReactKey;
|
|
10
|
+
};
|
|
11
|
+
export type ItemKey<Item extends BaseItem> = Extract<keyof Item, string>;
|
|
12
|
+
export type { ColumnDefinition, ValueResolver } from "./ColumnDefinition";
|
|
13
|
+
export { default as Dictionary } from "./Dictionary";
|
|
14
|
+
export type { FilterState } from "./useFilterState";
|
|
15
|
+
export type { SortDirection, SortState } from "./useSortState";
|
|
16
|
+
export type { ColumnsMaskState } from "./useMaskableColumns";
|
|
17
|
+
export type { PaginationState } from "./usePagination";
|
|
18
|
+
interface Props<Item extends BaseItem> {
|
|
19
|
+
items: Item[];
|
|
20
|
+
columns: ColumnDefinition<Item, Primitive>[];
|
|
21
|
+
itemTarget?: (item: Item) => string;
|
|
22
|
+
initialFilterState?: FilterState;
|
|
23
|
+
initialSortState?: SortState;
|
|
24
|
+
initialColumnsMaskState?: ColumnsMaskState;
|
|
25
|
+
initialPaginationState?: PaginationState;
|
|
26
|
+
}
|
|
27
|
+
declare function DynamicTable<Item extends BaseItem>(props: Props<Item>): import("react/jsx-runtime").JSX.Element;
|
|
28
|
+
declare namespace DynamicTable {
|
|
29
|
+
var TableContainer: typeof import("./DefaultTheme").TableContainer;
|
|
30
|
+
var Controller: typeof import("./DefaultTheme").Controller;
|
|
31
|
+
var ColumnsPopup: typeof import("./DefaultTheme").ColumnsPopup;
|
|
32
|
+
var Table: typeof import("./DefaultTheme").Table;
|
|
33
|
+
var HeaderContainer: typeof import("./DefaultTheme").HeaderContainer;
|
|
34
|
+
var HeaderLine: typeof import("./DefaultTheme").HeaderLine;
|
|
35
|
+
var Header: typeof import("./DefaultTheme").Header;
|
|
36
|
+
var HeaderActions: typeof import("./DefaultTheme").HeaderActions;
|
|
37
|
+
var HeaderStatus: typeof import("./DefaultTheme").HeaderStatus;
|
|
38
|
+
var SearchFilterPopup: typeof import("./DefaultTheme").SearchFilterPopup;
|
|
39
|
+
var SelectionFilterPopup: typeof import("./DefaultTheme").SelectionFilterPopup;
|
|
40
|
+
var BodyContainer: typeof import("./DefaultTheme").BodyContainer;
|
|
41
|
+
var Line: typeof import("./DefaultTheme").Line;
|
|
42
|
+
var Cell: typeof import("./DefaultTheme").Cell;
|
|
43
|
+
var ClickableCell: typeof import("./DefaultTheme").ClickableCell;
|
|
44
|
+
var FooterContainer: typeof import("./DefaultTheme").FooterContainer;
|
|
45
|
+
var ItemsPerPageSelector: typeof import("./DefaultTheme").ItemsPerPageSelector;
|
|
46
|
+
var PageSelector: typeof import("./DefaultTheme").PageSelector;
|
|
47
|
+
var PageButton: typeof import("./DefaultTheme").PageButton;
|
|
48
|
+
var Button: typeof import("./DefaultTheme").Button;
|
|
49
|
+
var Popup: typeof import("./DefaultTheme").Popup;
|
|
50
|
+
var Loader: typeof import("./DefaultTheme").Loader;
|
|
51
|
+
}
|
|
52
|
+
export default DynamicTable;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { BodyContainer, Button, Cell, ClickableCell, ColumnsPopup, Controller, FooterContainer, Header, HeaderActions, HeaderContainer, HeaderLine, HeaderStatus, ItemsPerPageSelector, Line, Loader, PageButton, PageSelector, Popup, SearchFilterPopup, SelectionFilterPopup, Table, TableContainer, } from "./DefaultTheme";
|
|
3
|
+
import UniquePopupProvider from "./UniquePopupProvider";
|
|
4
|
+
import useColumns from "./useColumns";
|
|
5
|
+
import useFilterState from "./useFilterState";
|
|
6
|
+
import useItems from "./useItems";
|
|
7
|
+
import useMaskableColumns from "./useMaskableColumns";
|
|
8
|
+
import usePagination from "./usePagination";
|
|
9
|
+
import useSortState from "./useSortState";
|
|
10
|
+
export { default as Dictionary } from "./Dictionary";
|
|
11
|
+
export default function DynamicTable(props) {
|
|
12
|
+
const columns = useColumns(props.columns);
|
|
13
|
+
const items = useItems(props.items, props.itemTarget, columns);
|
|
14
|
+
const { columns: filteredColumns, items: filteredItems, clearFilterState, } = useFilterState(columns, items, props.initialFilterState);
|
|
15
|
+
const { columns: sortedColumns, items: sortedItems, clearSortState, } = useSortState(filteredColumns, filteredItems, props.initialSortState);
|
|
16
|
+
const { allColumns, columns: displayedColumns, items: displayedItems, } = useMaskableColumns(sortedColumns, sortedItems, props.initialColumnsMaskState);
|
|
17
|
+
const { items: paginatedItems, itemsPerPage, onItemsPerPageChange, totalPages, currentPage, onCurrentPageChange, } = usePagination(displayedItems, props.initialPaginationState);
|
|
18
|
+
return (_jsx(UniquePopupProvider, { children: _jsxs(DynamicTable.TableContainer, { children: [_jsx(DynamicTable.Controller, { columns: allColumns, clearFilterState: clearFilterState, clearSortState: clearSortState }), _jsxs(DynamicTable.Table, { children: [_jsx(DynamicTable.HeaderContainer, { children: _jsx(DynamicTable.HeaderLine, { children: displayedColumns.map((column) => (_jsx(DynamicTable.Header, { column: column, children: column.title }, column.id))) }) }), _jsx(DynamicTable.BodyContainer, { children: paginatedItems.map((item) => (_jsx(DynamicTable.Line, { children: item.target !== null
|
|
19
|
+
? item.values.map((value) => (_jsx(DynamicTable.ClickableCell, { target: item.target, children: value.loading ? _jsx(DynamicTable.Loader, {}) : value.display }, value.column)))
|
|
20
|
+
: item.values.map((value) => (_jsx(DynamicTable.Cell, { children: value.loading ? _jsx(DynamicTable.Loader, {}) : value.display }, value.column))) }, item.key))) }), _jsx(DynamicTable.FooterContainer, { totalColums: displayedColumns.length, pageSelector: _jsx(DynamicTable.PageSelector, { totalPages: totalPages, currentPage: currentPage, onCurrentPageChange: onCurrentPageChange }), itemsPerPageSelector: _jsx(DynamicTable.ItemsPerPageSelector, { itemsPerPage: itemsPerPage, onItemsPerPageChange: onItemsPerPageChange }) })] })] }) }));
|
|
21
|
+
}
|
|
22
|
+
DynamicTable.TableContainer = TableContainer;
|
|
23
|
+
DynamicTable.Controller = Controller;
|
|
24
|
+
DynamicTable.ColumnsPopup = ColumnsPopup;
|
|
25
|
+
DynamicTable.Table = Table;
|
|
26
|
+
DynamicTable.HeaderContainer = HeaderContainer;
|
|
27
|
+
DynamicTable.HeaderLine = HeaderLine;
|
|
28
|
+
DynamicTable.Header = Header;
|
|
29
|
+
DynamicTable.HeaderActions = HeaderActions;
|
|
30
|
+
DynamicTable.HeaderStatus = HeaderStatus;
|
|
31
|
+
DynamicTable.SearchFilterPopup = SearchFilterPopup;
|
|
32
|
+
DynamicTable.SelectionFilterPopup = SelectionFilterPopup;
|
|
33
|
+
DynamicTable.BodyContainer = BodyContainer;
|
|
34
|
+
DynamicTable.Line = Line;
|
|
35
|
+
DynamicTable.Cell = Cell;
|
|
36
|
+
DynamicTable.ClickableCell = ClickableCell;
|
|
37
|
+
DynamicTable.FooterContainer = FooterContainer;
|
|
38
|
+
DynamicTable.ItemsPerPageSelector = ItemsPerPageSelector;
|
|
39
|
+
DynamicTable.PageSelector = PageSelector;
|
|
40
|
+
DynamicTable.PageButton = PageButton;
|
|
41
|
+
DynamicTable.Button = Button;
|
|
42
|
+
DynamicTable.Popup = Popup;
|
|
43
|
+
DynamicTable.Loader = Loader;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { BaseItem, ColumnDefinition, Dictionary, Primitive, ValueResolver } from ".";
|
|
2
|
+
/**
|
|
3
|
+
* A column that is loading a dictionary.
|
|
4
|
+
*/
|
|
5
|
+
export interface LoadingInternalColumn<Item extends BaseItem, Value extends Primitive> {
|
|
6
|
+
readonly id: string;
|
|
7
|
+
readonly title: string;
|
|
8
|
+
readonly loadingDictionary: true;
|
|
9
|
+
readonly dictionary: Promise<Dictionary<Value>>;
|
|
10
|
+
readonly resolveValue: ValueResolver<Item, Value>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* A column fully loaded and usable.
|
|
14
|
+
*/
|
|
15
|
+
export interface LoadedInternalColumn<Item extends BaseItem, Value extends Primitive> {
|
|
16
|
+
readonly id: string;
|
|
17
|
+
readonly title: string;
|
|
18
|
+
readonly loadingDictionary: false;
|
|
19
|
+
readonly dictionary?: Dictionary<Value>;
|
|
20
|
+
readonly resolveValue: ValueResolver<Item, Value>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A column, either in loading state or in loaded state.
|
|
24
|
+
*/
|
|
25
|
+
export type InternalColumn<Item extends BaseItem, Value extends Primitive> = LoadingInternalColumn<Item, Value> | LoadedInternalColumn<Item, Value>;
|
|
26
|
+
/**
|
|
27
|
+
* A list of columns.
|
|
28
|
+
*/
|
|
29
|
+
export type InternalColumns<Item extends BaseItem> = readonly InternalColumn<Item, Primitive>[];
|
|
30
|
+
/**
|
|
31
|
+
* Transform a list of column definitions into a list of internal columns.
|
|
32
|
+
*
|
|
33
|
+
* Handles the optional loading state of some columns that may be an async dictionary and update the list of internal
|
|
34
|
+
* columns when each dictionary is resolved.
|
|
35
|
+
*/
|
|
36
|
+
export default function useColumns<Item extends BaseItem>(definitions: ColumnDefinition<Item, Primitive>[]): InternalColumn<Item, Primitive>[];
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { replaceElement } from "@vinorcola/utils/list";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { isAccessorColumnDefinition } from "./ColumnDefinition";
|
|
4
|
+
/**
|
|
5
|
+
* Transform a list of column definitions into a list of internal columns.
|
|
6
|
+
*
|
|
7
|
+
* Handles the optional loading state of some columns that may be an async dictionary and update the list of internal
|
|
8
|
+
* columns when each dictionary is resolved.
|
|
9
|
+
*/
|
|
10
|
+
export default function useColumns(definitions) {
|
|
11
|
+
// This is used for canceling promises (resolved promises' callback won't be executed if the version changed).
|
|
12
|
+
const definitionVersion = useRef(0);
|
|
13
|
+
const [columns, setColumns] = useState(() => resolveInitialColumnState(definitions));
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
definitionVersion.current++;
|
|
16
|
+
const currentVersion = definitionVersion.current;
|
|
17
|
+
setColumns(resolveInitialColumnState(definitions));
|
|
18
|
+
definitions.forEach((definition, index) => {
|
|
19
|
+
if (definition.dictionary instanceof Promise) {
|
|
20
|
+
// Once dictionary is loaded (and the columns definitions hasn't changed), update the internal column's
|
|
21
|
+
// data.
|
|
22
|
+
definition.dictionary.then((dictionary) => {
|
|
23
|
+
if (definitionVersion.current === currentVersion) {
|
|
24
|
+
setColumns((columns) => replaceElement(columns, index, {
|
|
25
|
+
id: definition.id,
|
|
26
|
+
title: definition.title,
|
|
27
|
+
loadingDictionary: false,
|
|
28
|
+
dictionary,
|
|
29
|
+
resolveValue: isAccessorColumnDefinition(definition)
|
|
30
|
+
? (item) => item[definition.id]
|
|
31
|
+
: definition.resolveValue,
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}, [definitions]);
|
|
38
|
+
return columns;
|
|
39
|
+
}
|
|
40
|
+
function resolveInitialColumnState(definitions) {
|
|
41
|
+
return definitions.map((definition) => definition.dictionary instanceof Promise
|
|
42
|
+
? {
|
|
43
|
+
id: definition.id,
|
|
44
|
+
title: definition.title,
|
|
45
|
+
loadingDictionary: true,
|
|
46
|
+
dictionary: definition.dictionary,
|
|
47
|
+
resolveValue: isAccessorColumnDefinition(definition)
|
|
48
|
+
? (item) => item[definition.id]
|
|
49
|
+
: definition.resolveValue,
|
|
50
|
+
}
|
|
51
|
+
: {
|
|
52
|
+
id: definition.id,
|
|
53
|
+
title: definition.title,
|
|
54
|
+
loadingDictionary: false,
|
|
55
|
+
dictionary: definition.dictionary,
|
|
56
|
+
resolveValue: isAccessorColumnDefinition(definition)
|
|
57
|
+
? (item) => item[definition.id]
|
|
58
|
+
: definition.resolveValue,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { InternalColumn, InternalColumns } from "./useColumns";
|
|
2
|
+
import type { InternalItems } from "./useItems";
|
|
3
|
+
import type { BaseItem, Primitive } from ".";
|
|
4
|
+
/**
|
|
5
|
+
* A filter type.
|
|
6
|
+
*/
|
|
7
|
+
export type FilterType = "search" | "selection";
|
|
8
|
+
/**
|
|
9
|
+
* A filter state based on text search.
|
|
10
|
+
*/
|
|
11
|
+
export interface SearchState {
|
|
12
|
+
readonly searchText: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A filter state based on value selection.
|
|
16
|
+
*/
|
|
17
|
+
export interface SelectionState<Value extends Primitive> {
|
|
18
|
+
readonly hiddenValues: Value[];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A filter state for a column, either based on text search or value selection.
|
|
22
|
+
*/
|
|
23
|
+
export type ColumnFilterState<Value extends Primitive> = SearchState | SelectionState<Value>;
|
|
24
|
+
/**
|
|
25
|
+
* A complete filter state.
|
|
26
|
+
*/
|
|
27
|
+
export interface FilterState {
|
|
28
|
+
[columnId: string]: ColumnFilterState<Primitive>;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Mark a column as filterable by text search.
|
|
32
|
+
*/
|
|
33
|
+
export type InternalSearchableColumn<Item extends BaseItem, Value extends Primitive> = InternalColumn<Item, Value> & {
|
|
34
|
+
readonly searchText: string | null;
|
|
35
|
+
readonly onSearchTextChange: (searchText: string | null) => void;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Mark a column as filterable by value selection.
|
|
39
|
+
*/
|
|
40
|
+
export type InternalSelectableColumn<Item extends BaseItem, Value extends Primitive> = InternalColumn<Item, Value> & {
|
|
41
|
+
readonly hiddenValues: Value[];
|
|
42
|
+
readonly onSelectionChange: (hiddenValues: Value[]) => void;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Mark a column as filterable, either by text search or value selection.
|
|
46
|
+
*/
|
|
47
|
+
export type InternalFilterableColumn<Item extends BaseItem, Value extends Primitive> = InternalSearchableColumn<Item, Value> | InternalSelectableColumn<Item, Value>;
|
|
48
|
+
export declare function isFilterable<Item extends BaseItem, Value extends Primitive>(column: InternalColumn<Item, Value>): column is InternalFilterableColumn<Item, Value>;
|
|
49
|
+
export declare function isSearchable<Item extends BaseItem, Value extends Primitive>(column: InternalColumn<Item, Value>): column is InternalSearchableColumn<Item, Value>;
|
|
50
|
+
export declare function isSelectable<Item extends BaseItem, Value extends Primitive>(column: InternalColumn<Item, Value>): column is InternalSelectableColumn<Item, Value>;
|
|
51
|
+
/**
|
|
52
|
+
* Filters items.
|
|
53
|
+
*
|
|
54
|
+
* This hook will add filter state & filter control on each given columns, returning decorated columns. It will also
|
|
55
|
+
* apply those filters on the items list, returning a filtered list.
|
|
56
|
+
*/
|
|
57
|
+
export default function useFilterState<Item extends BaseItem>(columns: InternalColumns<Item>, items: InternalItems<Item>, initialFilterState?: FilterState): {
|
|
58
|
+
columns: InternalFilterableColumn<Item, Primitive>[];
|
|
59
|
+
items: import("./useItems").InternalItem<Item>[];
|
|
60
|
+
clearFilterState: () => void;
|
|
61
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { drop } from "@vinorcola/utils/object";
|
|
2
|
+
import { extractSearchableText } from "@vinorcola/utils/text";
|
|
3
|
+
import { useCallback, useMemo, useState } from "react";
|
|
4
|
+
export function isFilterable(column) {
|
|
5
|
+
return ((column.searchText !== undefined && column.onSearchTextChange !== undefined) ||
|
|
6
|
+
(column.hiddenValues !== undefined && column.onSelectionChange !== undefined));
|
|
7
|
+
}
|
|
8
|
+
export function isSearchable(column) {
|
|
9
|
+
return column.searchText !== undefined && column.onSearchTextChange !== undefined;
|
|
10
|
+
}
|
|
11
|
+
export function isSelectable(column) {
|
|
12
|
+
return column.hiddenValues !== undefined && column.onSelectionChange !== undefined;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Filters items.
|
|
16
|
+
*
|
|
17
|
+
* This hook will add filter state & filter control on each given columns, returning decorated columns. It will also
|
|
18
|
+
* apply those filters on the items list, returning a filtered list.
|
|
19
|
+
*/
|
|
20
|
+
export default function useFilterState(columns, items, initialFilterState = {}) {
|
|
21
|
+
const [filterState, setFilterState] = useState(initialFilterState);
|
|
22
|
+
return {
|
|
23
|
+
columns: useMemo(() => columns.map((column) => {
|
|
24
|
+
if (column.dictionary === undefined) {
|
|
25
|
+
return {
|
|
26
|
+
...column,
|
|
27
|
+
searchText: filterState[column.id]?.searchText ?? null,
|
|
28
|
+
onSearchTextChange: (searchText) => {
|
|
29
|
+
setFilterState((filterState) => searchText === null || extractSearchableText(searchText) === ""
|
|
30
|
+
? drop(filterState, column.id)
|
|
31
|
+
: {
|
|
32
|
+
...filterState,
|
|
33
|
+
[column.id]: { searchText },
|
|
34
|
+
});
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
return {
|
|
40
|
+
...column,
|
|
41
|
+
hiddenValues: filterState[column.id]?.hiddenValues ?? [],
|
|
42
|
+
onSelectionChange: (hiddenValues) => {
|
|
43
|
+
setFilterState((filterState) => hiddenValues.length === 0
|
|
44
|
+
? drop(filterState, column.id)
|
|
45
|
+
: { ...filterState, [column.id]: { hiddenValues } });
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}), [columns, filterState]),
|
|
50
|
+
items: useMemo(() => items.filter((items) => items.values.every((value) => {
|
|
51
|
+
if (value.loading) {
|
|
52
|
+
// Keep loading values.
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
const columnFilterState = filterState[value.column] ?? null;
|
|
56
|
+
if (columnFilterState === null) {
|
|
57
|
+
// There is no filter on this column.
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
return isSearchedState(columnFilterState)
|
|
61
|
+
? matchSearch(value, columnFilterState)
|
|
62
|
+
: matchSelection(value, columnFilterState);
|
|
63
|
+
})), [items, filterState]),
|
|
64
|
+
clearFilterState: useCallback(() => {
|
|
65
|
+
setFilterState({});
|
|
66
|
+
}, []),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function isSearchedState(state) {
|
|
70
|
+
return state.searchText !== undefined;
|
|
71
|
+
}
|
|
72
|
+
function matchSearch(value, filterState) {
|
|
73
|
+
if (value.search === null) {
|
|
74
|
+
// Filter out empty values.
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
return value.search.includes(filterState.searchText);
|
|
78
|
+
}
|
|
79
|
+
function matchSelection(value, filterState) {
|
|
80
|
+
if (value.raw === null) {
|
|
81
|
+
// Filter out empty values.
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
return !filterState.hiddenValues.includes(value.raw);
|
|
85
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type Key, type ReactNode } from "react";
|
|
2
|
+
import type { InternalColumns } from "./useColumns";
|
|
3
|
+
import type { SortableValue } from "./useSortState";
|
|
4
|
+
import type { BaseItem, Primitive } from ".";
|
|
5
|
+
/**
|
|
6
|
+
* A value that is loading (waiting for an async column dictionary to be available).
|
|
7
|
+
*/
|
|
8
|
+
export interface LoadingInternalValue {
|
|
9
|
+
readonly column: string;
|
|
10
|
+
readonly loading: true;
|
|
11
|
+
readonly raw: Primitive | null;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A loaded value, ready for filter, sort and display.
|
|
15
|
+
*/
|
|
16
|
+
export interface LoadedInternalValue {
|
|
17
|
+
readonly column: string;
|
|
18
|
+
readonly loading: false;
|
|
19
|
+
readonly raw: Primitive | null;
|
|
20
|
+
readonly search: string | null;
|
|
21
|
+
readonly sort: SortableValue;
|
|
22
|
+
readonly display: ReactNode;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* A value, either in loading state ou in loaded state.
|
|
26
|
+
*/
|
|
27
|
+
export type InternalValue = LoadingInternalValue | LoadedInternalValue;
|
|
28
|
+
/**
|
|
29
|
+
* An internal item.
|
|
30
|
+
*/
|
|
31
|
+
export interface InternalItem<Item extends BaseItem> {
|
|
32
|
+
readonly key: Key;
|
|
33
|
+
readonly item: Item;
|
|
34
|
+
readonly target: string | null;
|
|
35
|
+
readonly values: InternalValue[];
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* A list of items.
|
|
39
|
+
*/
|
|
40
|
+
export type InternalItems<Item extends BaseItem> = readonly InternalItem<Item>[];
|
|
41
|
+
export default function useItems<Item extends BaseItem>(items: Item[], itemTarget: ((item: Item) => string) | undefined, columns: InternalColumns<Item>): InternalItems<Item>;
|
package/lib/useItems.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { displayInteger } from "@vinorcola/utils/number";
|
|
3
|
+
import { extractSearchableText } from "@vinorcola/utils/text";
|
|
4
|
+
import { useMemo } from "react";
|
|
5
|
+
export default function useItems(items, itemTarget, columns) {
|
|
6
|
+
return useMemo(() => items.map((item) => ({
|
|
7
|
+
key: item.id,
|
|
8
|
+
item,
|
|
9
|
+
target: itemTarget === undefined ? null : itemTarget(item),
|
|
10
|
+
values: columns.map((column) => resolveInternalValue(item, column)),
|
|
11
|
+
})), [items, itemTarget, columns]);
|
|
12
|
+
}
|
|
13
|
+
function resolveInternalValue(item, column) {
|
|
14
|
+
const raw = column.resolveValue(item);
|
|
15
|
+
return column.loadingDictionary
|
|
16
|
+
? {
|
|
17
|
+
column: column.id,
|
|
18
|
+
loading: true,
|
|
19
|
+
raw,
|
|
20
|
+
}
|
|
21
|
+
: {
|
|
22
|
+
column: column.id,
|
|
23
|
+
loading: false,
|
|
24
|
+
raw,
|
|
25
|
+
search: resolveSearchableValue(raw, column.dictionary),
|
|
26
|
+
sort: resolveSortableValue(raw, column.dictionary),
|
|
27
|
+
display: resolveDisplayableValue(raw, column.dictionary),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function resolveSearchableValue(value, dictionary) {
|
|
31
|
+
if (dictionary !== undefined) {
|
|
32
|
+
// Columns with dictionary are selectable, not searchable.
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
if (value === null) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
if (typeof value === "boolean") {
|
|
39
|
+
return value ? "true" : "false";
|
|
40
|
+
}
|
|
41
|
+
if (value instanceof Date) {
|
|
42
|
+
return value.toLocaleString();
|
|
43
|
+
}
|
|
44
|
+
if (typeof value === "number") {
|
|
45
|
+
return `${value}`;
|
|
46
|
+
}
|
|
47
|
+
return extractSearchableText(value);
|
|
48
|
+
}
|
|
49
|
+
function resolveSortableValue(value, dictionary) {
|
|
50
|
+
if (value === null) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
if (dictionary) {
|
|
54
|
+
return (dictionary.get(value)?.title ?? dictionary.unknownMessage).toLocaleLowerCase();
|
|
55
|
+
}
|
|
56
|
+
if (typeof value === "boolean") {
|
|
57
|
+
return value ? 1 : 0;
|
|
58
|
+
}
|
|
59
|
+
if (value instanceof Date) {
|
|
60
|
+
return value.getTime();
|
|
61
|
+
}
|
|
62
|
+
if (typeof value === "number") {
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
return value.toLocaleLowerCase();
|
|
66
|
+
}
|
|
67
|
+
function resolveDisplayableValue(value, dictionary) {
|
|
68
|
+
if (value === null) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
if (dictionary) {
|
|
72
|
+
const dictionaryEntry = dictionary.get(value);
|
|
73
|
+
if (dictionaryEntry === undefined) {
|
|
74
|
+
return dictionary.unknownMessage;
|
|
75
|
+
}
|
|
76
|
+
return dictionaryEntry.prepend !== undefined ? (_jsxs(_Fragment, { children: [dictionaryEntry.prepend, dictionaryEntry.title] })) : (dictionaryEntry.title);
|
|
77
|
+
}
|
|
78
|
+
if (typeof value === "boolean") {
|
|
79
|
+
return value ? "true" : "false";
|
|
80
|
+
}
|
|
81
|
+
if (value instanceof Date) {
|
|
82
|
+
return value.toLocaleDateString();
|
|
83
|
+
}
|
|
84
|
+
if (typeof value === "number") {
|
|
85
|
+
return displayInteger(value);
|
|
86
|
+
}
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { InternalColumn } from "./useColumns";
|
|
2
|
+
import type { InternalItems } from "./useItems";
|
|
3
|
+
import type { BaseItem, Primitive } from ".";
|
|
4
|
+
/**
|
|
5
|
+
* A complete columns mask state.
|
|
6
|
+
*/
|
|
7
|
+
export type ColumnsMaskState = string[];
|
|
8
|
+
/**
|
|
9
|
+
* Mark a column as maskable.
|
|
10
|
+
*/
|
|
11
|
+
export type InternalMaskableColumn<Item extends BaseItem, Value extends Primitive> = InternalColumn<Item, Value> & {
|
|
12
|
+
readonly displayed: boolean;
|
|
13
|
+
readonly onDisplayToggle: () => void;
|
|
14
|
+
};
|
|
15
|
+
export declare function isMaskable<Item extends BaseItem, Value extends Primitive>(column: InternalColumn<Item, Value>): column is InternalMaskableColumn<Item, Value>;
|
|
16
|
+
/**
|
|
17
|
+
* Masks columns.
|
|
18
|
+
*
|
|
19
|
+
* This hook will add mask state & mask control on each given columns, returning decorated columns. It will also alter
|
|
20
|
+
* the items list to remove masked columns' values.
|
|
21
|
+
*
|
|
22
|
+
* Note that the returned `columns` contains only the displayed columns, while `allColumns` contains all the columns
|
|
23
|
+
* (for control purpose).
|
|
24
|
+
*/
|
|
25
|
+
export default function useMaskableColumns<Item extends BaseItem>(columns: InternalColumn<Item, Primitive>[], items: InternalItems<Item>, initialHiddenColumns?: ColumnsMaskState): {
|
|
26
|
+
allColumns: InternalMaskableColumn<Item, Primitive>[];
|
|
27
|
+
columns: InternalMaskableColumn<Item, Primitive>[];
|
|
28
|
+
items: {
|
|
29
|
+
values: import("./useItems").InternalValue[];
|
|
30
|
+
key: import("react").Key;
|
|
31
|
+
item: Item;
|
|
32
|
+
target: string | null;
|
|
33
|
+
}[];
|
|
34
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useMemo, useState } from "react";
|
|
2
|
+
export function isMaskable(column) {
|
|
3
|
+
return column.displayed !== undefined && column.onDisplayToggle !== undefined;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Masks columns.
|
|
7
|
+
*
|
|
8
|
+
* This hook will add mask state & mask control on each given columns, returning decorated columns. It will also alter
|
|
9
|
+
* the items list to remove masked columns' values.
|
|
10
|
+
*
|
|
11
|
+
* Note that the returned `columns` contains only the displayed columns, while `allColumns` contains all the columns
|
|
12
|
+
* (for control purpose).
|
|
13
|
+
*/
|
|
14
|
+
export default function useMaskableColumns(columns, items, initialHiddenColumns = []) {
|
|
15
|
+
const [hidden, setHidden] = useState(initialHiddenColumns);
|
|
16
|
+
const allColumns = useMemo(() => columns.map((column) => {
|
|
17
|
+
const displayed = !hidden.includes(column.id);
|
|
18
|
+
return {
|
|
19
|
+
...column,
|
|
20
|
+
displayed,
|
|
21
|
+
onDisplayToggle: () => {
|
|
22
|
+
setHidden((hidden) => displayed ? [...hidden, column.id] : hidden.filter((id) => id !== column.id));
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}), [columns, hidden]);
|
|
26
|
+
return {
|
|
27
|
+
allColumns,
|
|
28
|
+
columns: useMemo(() => allColumns.filter((column) => column.displayed), [allColumns]),
|
|
29
|
+
items: useMemo(() => items.map((item) => ({
|
|
30
|
+
...item,
|
|
31
|
+
values: item.values.filter((value) => allColumns.find((column) => column.id === value.column).displayed),
|
|
32
|
+
})), [items, allColumns]),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { InternalItems } from "./useItems";
|
|
2
|
+
import type { BaseItem } from ".";
|
|
3
|
+
/**
|
|
4
|
+
* A complete pagination state.
|
|
5
|
+
*/
|
|
6
|
+
export interface PaginationState {
|
|
7
|
+
currentPage: number;
|
|
8
|
+
itemsPerPage: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Apply pagination.
|
|
12
|
+
*
|
|
13
|
+
* Return pagination state & pagination control as well as items to display on the current page.
|
|
14
|
+
*/
|
|
15
|
+
export default function usePagination<Item extends BaseItem>(items: InternalItems<Item>, initialPaginationState?: PaginationState): {
|
|
16
|
+
items: import("./useItems").InternalItem<Item>[];
|
|
17
|
+
itemsPerPage: number;
|
|
18
|
+
onItemsPerPageChange: (itemsPerPage: number) => void;
|
|
19
|
+
totalPages: number;
|
|
20
|
+
currentPage: number;
|
|
21
|
+
onCurrentPageChange: (page: number) => void;
|
|
22
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { useCallback, useMemo, useState } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Apply pagination.
|
|
4
|
+
*
|
|
5
|
+
* Return pagination state & pagination control as well as items to display on the current page.
|
|
6
|
+
*/
|
|
7
|
+
export default function usePagination(items, initialPaginationState = { currentPage: 1, itemsPerPage: 12 }) {
|
|
8
|
+
const [pagination, setPagination] = useState(initialPaginationState);
|
|
9
|
+
const totalPages = useMemo(() => Math.ceil(items.length / pagination.itemsPerPage), [items, pagination]);
|
|
10
|
+
const currentPage = useMemo(() => Math.max(1, Math.min(totalPages, pagination.currentPage)), [totalPages, pagination]);
|
|
11
|
+
return {
|
|
12
|
+
items: useMemo(() => items.slice((currentPage - 1) * pagination.itemsPerPage, currentPage * pagination.itemsPerPage), [items, pagination, currentPage]),
|
|
13
|
+
itemsPerPage: pagination.itemsPerPage,
|
|
14
|
+
onItemsPerPageChange: useCallback((itemsPerPage) => {
|
|
15
|
+
setPagination((pagination) => ({
|
|
16
|
+
...pagination,
|
|
17
|
+
itemsPerPage,
|
|
18
|
+
}));
|
|
19
|
+
}, []),
|
|
20
|
+
totalPages,
|
|
21
|
+
currentPage,
|
|
22
|
+
onCurrentPageChange: useCallback((page) => {
|
|
23
|
+
setPagination((pagination) => ({
|
|
24
|
+
...pagination,
|
|
25
|
+
currentPage: page,
|
|
26
|
+
}));
|
|
27
|
+
}, []),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { InternalColumn, InternalColumns } from "./useColumns";
|
|
2
|
+
import type { InternalItems, InternalItem } from "./useItems";
|
|
3
|
+
import type { BaseItem, Primitive } from ".";
|
|
4
|
+
/**
|
|
5
|
+
* A sort direction.
|
|
6
|
+
*/
|
|
7
|
+
export type SortDirection = "asc" | "desc";
|
|
8
|
+
/**
|
|
9
|
+
* A sortable value.
|
|
10
|
+
*/
|
|
11
|
+
export type SortableValue = string | number | null;
|
|
12
|
+
/**
|
|
13
|
+
* A complete sort state.
|
|
14
|
+
*/
|
|
15
|
+
export type SortState = {
|
|
16
|
+
columnId: string;
|
|
17
|
+
direction: SortDirection;
|
|
18
|
+
}[];
|
|
19
|
+
/**
|
|
20
|
+
* Mark a column as sortable.
|
|
21
|
+
*/
|
|
22
|
+
export type InternalSortableColumn<Item extends BaseItem, Value extends Primitive> = InternalColumn<Item, Value> & {
|
|
23
|
+
readonly sorted: {
|
|
24
|
+
readonly order: number;
|
|
25
|
+
readonly totalSortedColumns: number;
|
|
26
|
+
readonly direction: SortDirection;
|
|
27
|
+
} | null;
|
|
28
|
+
readonly onSortToggle: () => void;
|
|
29
|
+
};
|
|
30
|
+
export declare function isSortable<Item extends BaseItem, Value extends Primitive>(column: InternalColumn<Item, Value>): column is InternalSortableColumn<Item, Value>;
|
|
31
|
+
/**
|
|
32
|
+
* Sorts items.
|
|
33
|
+
*
|
|
34
|
+
* This hook will add sort state & sort control on each given columns, returning decorated columns. It will also apply
|
|
35
|
+
* those sorts on the items list, returning a sorted list.
|
|
36
|
+
*/
|
|
37
|
+
export default function useSortState<Item extends BaseItem>(columns: InternalColumns<Item>, items: InternalItems<Item>, initialSortState?: SortState): {
|
|
38
|
+
columns: InternalSortableColumn<Item, Primitive>[];
|
|
39
|
+
items: InternalItem<Item>[];
|
|
40
|
+
clearSortState: () => void;
|
|
41
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { dropElement, replaceElement } from "@vinorcola/utils/list";
|
|
2
|
+
import { useCallback, useMemo, useState } from "react";
|
|
3
|
+
export function isSortable(column) {
|
|
4
|
+
return column.sorted !== undefined && column.onSortToggle !== undefined;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Sorts items.
|
|
8
|
+
*
|
|
9
|
+
* This hook will add sort state & sort control on each given columns, returning decorated columns. It will also apply
|
|
10
|
+
* those sorts on the items list, returning a sorted list.
|
|
11
|
+
*/
|
|
12
|
+
export default function useSortState(columns, items, initialSortState = []) {
|
|
13
|
+
const [sortState, setSortState] = useState(initialSortState);
|
|
14
|
+
return {
|
|
15
|
+
columns: useMemo(() => columns.map((column) => {
|
|
16
|
+
const columnSortStateIndex = sortState.findIndex((columnSortState) => columnSortState.columnId === column.id);
|
|
17
|
+
return columnSortStateIndex === -1
|
|
18
|
+
? {
|
|
19
|
+
...column,
|
|
20
|
+
sorted: null,
|
|
21
|
+
onSortToggle: () => {
|
|
22
|
+
setSortState([...sortState, { columnId: column.id, direction: "asc" }]);
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
: {
|
|
26
|
+
...column,
|
|
27
|
+
sorted: {
|
|
28
|
+
order: columnSortStateIndex + 1,
|
|
29
|
+
totalSortedColumns: sortState.length,
|
|
30
|
+
direction: sortState[columnSortStateIndex].direction,
|
|
31
|
+
},
|
|
32
|
+
onSortToggle: () => {
|
|
33
|
+
setSortState(sortState[columnSortStateIndex].direction === "asc"
|
|
34
|
+
? replaceElement(sortState, columnSortStateIndex, {
|
|
35
|
+
columnId: column.id,
|
|
36
|
+
direction: "desc",
|
|
37
|
+
})
|
|
38
|
+
: dropElement(sortState, columnSortStateIndex));
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}), [columns, sortState]),
|
|
42
|
+
items: useMemo(() => [...items].sort((a, b) => {
|
|
43
|
+
for (const columnSortState of sortState) {
|
|
44
|
+
const aValue = extractSortValue(a, columnSortState.columnId);
|
|
45
|
+
const bValue = extractSortValue(b, columnSortState.columnId);
|
|
46
|
+
if (aValue === bValue) {
|
|
47
|
+
// If a & b are equal, continue the loop to sort according to the next column.
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
// Always display null values (or loading values) last, no mater the sort order.
|
|
51
|
+
if (aValue === null) {
|
|
52
|
+
return 1;
|
|
53
|
+
}
|
|
54
|
+
if (bValue === null) {
|
|
55
|
+
return -1;
|
|
56
|
+
}
|
|
57
|
+
if (typeof aValue === "string") {
|
|
58
|
+
return ((columnSortState.direction === "asc" ? 1 : -1) * aValue.localeCompare(bValue));
|
|
59
|
+
}
|
|
60
|
+
return columnSortState.direction === "asc"
|
|
61
|
+
? aValue - bValue
|
|
62
|
+
: bValue - aValue;
|
|
63
|
+
}
|
|
64
|
+
// Lines are equals.
|
|
65
|
+
return 0;
|
|
66
|
+
}), [items, sortState]),
|
|
67
|
+
clearSortState: useCallback(() => {
|
|
68
|
+
setSortState([]);
|
|
69
|
+
}, []),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function extractSortValue(line, column) {
|
|
73
|
+
const value = line.values.find((value) => value.column === column) ?? null;
|
|
74
|
+
return value === null ? null : value.loading ? null : value.sort;
|
|
75
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vinorcola/dynamic-table",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A table to ease the display of a filterable, sortable and paginable dataset.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"table",
|
|
7
|
+
"list",
|
|
8
|
+
"filter",
|
|
9
|
+
"sort",
|
|
10
|
+
"paginate",
|
|
11
|
+
"pagination"
|
|
12
|
+
],
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/Vinorcola/dynamic-table.git"
|
|
16
|
+
},
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/Vinorcola/dynamic-table/issues"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/Vinorcola/dynamic-table#readme",
|
|
21
|
+
"type": "module",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": "./lib/index.js",
|
|
24
|
+
"./DefaultTheme": "./lib/DefaultTheme.js",
|
|
25
|
+
"./DefaultTheme.css": "./assets/DefaultTheme.css"
|
|
26
|
+
},
|
|
27
|
+
"main": "lib/index.js",
|
|
28
|
+
"author": "Vinorcola",
|
|
29
|
+
"private": false,
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"files": [
|
|
32
|
+
"assets/**/*",
|
|
33
|
+
"lib/**/*"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsc",
|
|
37
|
+
"lint": "eslint --fix",
|
|
38
|
+
"prepare": "npm run build",
|
|
39
|
+
"prepublishOnly": "npm run lint",
|
|
40
|
+
"preversion": "npm run lint",
|
|
41
|
+
"version": "git add -A .",
|
|
42
|
+
"postversion": "git push && git push --tags"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/react": "^19.2.2",
|
|
46
|
+
"@vinorcola/lint": "^1.0.5",
|
|
47
|
+
"typescript": "^5.9.3"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"@vinorcola/utils": "^1.0.1",
|
|
51
|
+
"react": "^19.0.0"
|
|
52
|
+
}
|
|
53
|
+
}
|