@react-fabric/data 1.0.0-beta.6 → 1.0.0-beta.60
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/{src/histogram/Histogram.module.css → css/_histogram.css} +16 -13
- package/css/styles.css +24 -0
- package/dist/cjs/checklist/CheckList.js +23 -0
- package/dist/cjs/histogram/Histogram.js +37 -7
- package/dist/cjs/hooks/useFilteredList.js +88 -10
- package/dist/cjs/hooks/usePagination.js +27 -4
- package/dist/cjs/hooks/useSelectableList.js +29 -0
- package/dist/cjs/i18n/ar.json.js +8 -1
- package/dist/cjs/i18n/en.json.js +8 -1
- package/dist/cjs/json/JsonViewer.js +67 -27
- package/dist/cjs/pagination/Pagination.js +10 -0
- package/dist/cjs/table/AddColumn.js +1 -1
- package/dist/cjs/table/BodyCell.js +5 -4
- package/dist/cjs/table/BodyRow.js +2 -2
- package/dist/cjs/table/CheckboxCell.js +1 -1
- package/dist/cjs/table/ColumnFilters.js +97 -0
- package/dist/cjs/table/HeaderCell.js +71 -43
- package/dist/cjs/table/Table.js +58 -12
- package/dist/cjs/table/types.js +29 -0
- package/dist/cjs/table/useResizer.js +4 -4
- package/dist/cjs/table/useVirtualTable.js +127 -19
- package/dist/cjs/tree/TreeNode.js +48 -30
- package/dist/cjs/tree/TreePanel.js +45 -11
- package/dist/cjs/tree/useTree.js +199 -38
- package/dist/cjs/tree/utils.js +39 -12
- package/dist/cjs/virtual/Gallery.js +97 -45
- package/dist/cjs/virtual/List.js +93 -39
- package/dist/cjs/virtual/Scroller.js +16 -8
- package/dist/esm/checklist/CheckList.js +23 -0
- package/dist/esm/histogram/Histogram.js +38 -8
- package/dist/esm/hooks/useFilteredList.js +89 -11
- package/dist/esm/hooks/usePagination.js +27 -4
- package/dist/esm/hooks/useSelectableList.js +29 -0
- package/dist/esm/i18n/ar.json.js +8 -2
- package/dist/esm/i18n/en.json.js +8 -2
- package/dist/esm/json/JsonViewer.js +68 -28
- package/dist/esm/pagination/Pagination.js +10 -0
- package/dist/esm/table/AddColumn.js +1 -1
- package/dist/esm/table/BodyCell.js +5 -4
- package/dist/esm/table/BodyRow.js +2 -2
- package/dist/esm/table/CheckboxCell.js +1 -1
- package/dist/esm/table/ColumnFilters.js +97 -0
- package/dist/esm/table/HeaderCell.js +72 -44
- package/dist/esm/table/Table.js +59 -13
- package/dist/esm/table/types.js +29 -0
- package/dist/esm/table/useResizer.js +4 -4
- package/dist/esm/table/useVirtualTable.js +129 -21
- package/dist/esm/tree/TreeNode.js +51 -33
- package/dist/esm/tree/TreePanel.js +46 -12
- package/dist/esm/tree/useTree.js +202 -41
- package/dist/esm/tree/utils.js +40 -13
- package/dist/esm/virtual/Gallery.js +97 -45
- package/dist/esm/virtual/List.js +93 -39
- package/dist/esm/virtual/Scroller.js +16 -8
- package/dist/types/checklist/CheckList.d.ts +22 -0
- package/dist/types/histogram/Histogram.d.ts +28 -1
- package/dist/types/hooks/useFilteredList.d.ts +66 -5
- package/dist/types/hooks/usePagination.d.ts +19 -0
- package/dist/types/hooks/useSelectableList.d.ts +29 -0
- package/dist/types/json/JsonViewer.d.ts +27 -2
- package/dist/types/pagination/Pagination.d.ts +10 -1
- package/dist/types/table/ColumnFilters.d.ts +9 -0
- package/dist/types/table/HeaderCell.d.ts +1 -1
- package/dist/types/table/Table.d.ts +14 -1
- package/dist/types/table/types.d.ts +44 -5
- package/dist/types/table/useVirtualTable.d.ts +23 -9
- package/dist/types/tree/TreeNode.d.ts +1 -1
- package/dist/types/tree/TreePanel.d.ts +42 -1
- package/dist/types/tree/types.d.ts +65 -19
- package/dist/types/tree/useTree.d.ts +9 -10
- package/dist/types/tree/utils.d.ts +10 -3
- package/dist/types/virtual/Gallery.d.ts +42 -3
- package/dist/types/virtual/List.d.ts +42 -3
- package/package.json +16 -14
- package/src/checklist/CheckList.tsx +22 -0
- package/src/histogram/Histogram.tsx +40 -10
- package/src/hooks/useFilteredList.tsx +109 -9
- package/src/hooks/usePagination.tsx +32 -7
- package/src/hooks/useSelectableList.ts +29 -0
- package/src/i18n/ar.json +5 -0
- package/src/i18n/en.json +5 -0
- package/src/json/JsonViewer.tsx +71 -35
- package/src/pagination/Pagination.tsx +10 -0
- package/src/table/AddColumn.tsx +1 -1
- package/src/table/BodyCell.tsx +6 -4
- package/src/table/BodyRow.tsx +3 -2
- package/src/table/CheckboxCell.tsx +1 -1
- package/src/table/ColumnFilters.tsx +105 -0
- package/src/table/HeaderCell.tsx +72 -18
- package/src/table/Table.tsx +99 -11
- package/src/table/types.ts +46 -8
- package/src/table/useResizer.ts +5 -3
- package/src/table/useVirtualTable.ts +158 -27
- package/src/tree/TreeNode.tsx +98 -58
- package/src/tree/TreePanel.tsx +77 -6
- package/src/tree/types.ts +73 -18
- package/src/tree/useTree.ts +220 -44
- package/src/tree/utils.ts +58 -20
- package/src/virtual/Gallery.tsx +118 -54
- package/src/virtual/List.tsx +99 -39
- package/src/virtual/Scroller.tsx +16 -8
- package/dist/cjs/histogram/Histogram.module.css +0 -1
- package/dist/esm/histogram/Histogram.module.css +0 -1
package/src/table/Table.tsx
CHANGED
|
@@ -21,8 +21,15 @@
|
|
|
21
21
|
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
CoreIcons,
|
|
26
|
+
EmptyContent,
|
|
27
|
+
Icon,
|
|
28
|
+
Loading,
|
|
29
|
+
useDebounce,
|
|
30
|
+
} from "@react-fabric/core";
|
|
25
31
|
import { debounce } from "@react-fabric/utilities";
|
|
32
|
+
import classNames from "classnames";
|
|
26
33
|
import {
|
|
27
34
|
useEffect,
|
|
28
35
|
useImperativeHandle,
|
|
@@ -41,23 +48,42 @@ import { type TableProps } from "./types";
|
|
|
41
48
|
import { useTableColumns } from "./useTableColumns";
|
|
42
49
|
import { useVirtualTable } from "./useVirtualTable";
|
|
43
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Table component for displaying data in a tabular format with features like sorting, filtering, and virtual scrolling.
|
|
53
|
+
* It supports expandable rows, checkable rows, and customizable columns.
|
|
54
|
+
* The component is designed to handle large datasets efficiently by rendering only the visible rows.
|
|
55
|
+
* It also provides a way to manage column visibility and allows for custom rendering of group headers.
|
|
56
|
+
* Uses TanStack's `useVirtual` for virtualization and React's `useMemo` for performance optimization.
|
|
57
|
+
*
|
|
58
|
+
* @param {TableProps<T>} props - The properties for the Table component.
|
|
59
|
+
* @returns {JSX.Element} The rendered Table component.
|
|
60
|
+
*
|
|
61
|
+
* @see {@link https://adarshpastakia.github.io/react-fabric/?path=/story/data-table--table}
|
|
62
|
+
* @see {@link https://tanstack.com/table/v8/docs/api/virtual}
|
|
63
|
+
*/
|
|
44
64
|
export const Table = <T extends KeyValue = KeyValue>({
|
|
45
65
|
ref,
|
|
46
66
|
data,
|
|
47
67
|
columns,
|
|
48
|
-
keyProperty,
|
|
68
|
+
keyProperty = "id",
|
|
69
|
+
groupProperty,
|
|
49
70
|
checkableRows,
|
|
50
71
|
initialScroll,
|
|
51
72
|
hideableColumns,
|
|
52
73
|
sort,
|
|
53
74
|
loading,
|
|
75
|
+
checked,
|
|
54
76
|
emptyDisplay,
|
|
77
|
+
groupRenderer,
|
|
55
78
|
canExpand,
|
|
56
79
|
children,
|
|
57
80
|
onSort,
|
|
81
|
+
onFilter,
|
|
58
82
|
onScroll,
|
|
59
83
|
onRowClick,
|
|
60
84
|
onCheckedChanged,
|
|
85
|
+
total = 0,
|
|
86
|
+
onLoadMore,
|
|
61
87
|
}: TableProps<T>) => {
|
|
62
88
|
const { t } = useTranslation("data");
|
|
63
89
|
const fireCheckChanged = useDebounce(
|
|
@@ -72,13 +98,24 @@ export const Table = <T extends KeyValue = KeyValue>({
|
|
|
72
98
|
top,
|
|
73
99
|
checkState,
|
|
74
100
|
virtualizer,
|
|
101
|
+
hasActiveSticky,
|
|
75
102
|
toggleExpand,
|
|
76
103
|
toggleChecked,
|
|
77
104
|
toggleAllChecked,
|
|
105
|
+
toggleGroupExpand,
|
|
78
106
|
getData,
|
|
79
107
|
totalSize,
|
|
108
|
+
isActiveSticky,
|
|
80
109
|
measureElement,
|
|
81
|
-
} = useVirtualTable(data,
|
|
110
|
+
} = useVirtualTable(data, {
|
|
111
|
+
checked,
|
|
112
|
+
keyProperty,
|
|
113
|
+
groupProperty,
|
|
114
|
+
total,
|
|
115
|
+
loading,
|
|
116
|
+
onLoadMore,
|
|
117
|
+
onCheckChanged: fireCheckChanged,
|
|
118
|
+
});
|
|
82
119
|
const refBody = useRef<HTMLDivElement>(null);
|
|
83
120
|
|
|
84
121
|
const wrapperStart = useMemo(
|
|
@@ -142,9 +179,13 @@ export const Table = <T extends KeyValue = KeyValue>({
|
|
|
142
179
|
style={{
|
|
143
180
|
gridTemplate: `"head" auto "loader" auto "content" 1fr "foot" auto/1fr`,
|
|
144
181
|
}}
|
|
182
|
+
data-ref="tableScroller"
|
|
145
183
|
ref={scrollerRef}
|
|
146
184
|
>
|
|
147
|
-
<div
|
|
185
|
+
<div
|
|
186
|
+
data-ref="tableHeader"
|
|
187
|
+
className="bg-base font-medium sticky top-0 h-8 area-head border-b select-none flex flex-nowrap z-3"
|
|
188
|
+
>
|
|
148
189
|
<div className={wrapperStart}>
|
|
149
190
|
{canExpand && <div className="w-6" />}
|
|
150
191
|
{checkableRows && (
|
|
@@ -154,15 +195,33 @@ export const Table = <T extends KeyValue = KeyValue>({
|
|
|
154
195
|
/>
|
|
155
196
|
)}
|
|
156
197
|
{state.start?.map((col, idx) => (
|
|
157
|
-
<HeaderCell
|
|
198
|
+
<HeaderCell
|
|
199
|
+
key={idx}
|
|
200
|
+
{...col}
|
|
201
|
+
sort={sort}
|
|
202
|
+
onSort={onSort}
|
|
203
|
+
onFilter={onFilter}
|
|
204
|
+
/>
|
|
158
205
|
))}
|
|
159
206
|
</div>
|
|
160
207
|
{state.cols?.map((col, idx) => (
|
|
161
|
-
<HeaderCell
|
|
208
|
+
<HeaderCell
|
|
209
|
+
key={idx}
|
|
210
|
+
{...col}
|
|
211
|
+
sort={sort}
|
|
212
|
+
onSort={onSort}
|
|
213
|
+
onFilter={onFilter}
|
|
214
|
+
/>
|
|
162
215
|
))}
|
|
163
216
|
<div className={wrapperEnd}>
|
|
164
217
|
{state.end?.map((col, idx) => (
|
|
165
|
-
<HeaderCell
|
|
218
|
+
<HeaderCell
|
|
219
|
+
key={idx}
|
|
220
|
+
{...col}
|
|
221
|
+
sort={sort}
|
|
222
|
+
onSort={onSort}
|
|
223
|
+
onFilter={onFilter}
|
|
224
|
+
/>
|
|
166
225
|
))}
|
|
167
226
|
|
|
168
227
|
{hideableColumns && (
|
|
@@ -176,16 +235,45 @@ export const Table = <T extends KeyValue = KeyValue>({
|
|
|
176
235
|
{loading && <Loading />}
|
|
177
236
|
{data.length > 0 && (
|
|
178
237
|
<div
|
|
179
|
-
className="area-content flex flex-col flex-nowrap"
|
|
238
|
+
className="area-content flex flex-col flex-nowrap min-h-full"
|
|
180
239
|
style={{ height: totalSize() }}
|
|
181
240
|
ref={refBody}
|
|
182
241
|
>
|
|
183
|
-
<div
|
|
242
|
+
<div
|
|
243
|
+
style={{
|
|
244
|
+
height: items[hasActiveSticky ? 1 : 0]?.start,
|
|
245
|
+
}}
|
|
246
|
+
/>
|
|
184
247
|
{items.map(({ index, key }) => {
|
|
185
248
|
const data = getData(index);
|
|
186
|
-
return (
|
|
249
|
+
return "__GROUP__" in data ? (
|
|
250
|
+
<div
|
|
251
|
+
key={key}
|
|
252
|
+
ref={measureElement}
|
|
253
|
+
data-index={index}
|
|
254
|
+
className={classNames(
|
|
255
|
+
"px-2 py-1 text-md bg-base border-y border-tint-100 cursor-pointer flex items-center gap-1 z-2",
|
|
256
|
+
isActiveSticky(index) && "sticky top-8",
|
|
257
|
+
)}
|
|
258
|
+
role="none"
|
|
259
|
+
onClick={() => toggleGroupExpand(data.key)}
|
|
260
|
+
>
|
|
261
|
+
<Icon
|
|
262
|
+
rtlFlip
|
|
263
|
+
icon={
|
|
264
|
+
data.open ? CoreIcons.chevronDown : CoreIcons.chevronRight
|
|
265
|
+
}
|
|
266
|
+
/>
|
|
267
|
+
{groupRenderer?.(data) ?? (
|
|
268
|
+
<div className="flex gap-1 items-center">
|
|
269
|
+
<span>{data.key}</span>
|
|
270
|
+
<span className="text-sm">({data.itemCount})</span>
|
|
271
|
+
</div>
|
|
272
|
+
)}
|
|
273
|
+
</div>
|
|
274
|
+
) : (
|
|
187
275
|
<BodyRow
|
|
188
|
-
rowKey={key}
|
|
276
|
+
rowKey={key.toString()}
|
|
189
277
|
index={index}
|
|
190
278
|
checkableRows={checkableRows}
|
|
191
279
|
checked={
|
package/src/table/types.ts
CHANGED
|
@@ -21,10 +21,10 @@
|
|
|
21
21
|
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
+
import { type IconProps } from "@react-fabric/core/dist/types/components/icon/Icon";
|
|
24
25
|
import {
|
|
25
26
|
type CssProp,
|
|
26
27
|
type Elements,
|
|
27
|
-
type IconProps,
|
|
28
28
|
type RefProp,
|
|
29
29
|
type TestProps,
|
|
30
30
|
} from "@react-fabric/core/dist/types/types";
|
|
@@ -34,22 +34,51 @@ export const COL_MIN_WIDTH = "3rem";
|
|
|
34
34
|
export const COL_MAX_WIDTH = "64rem";
|
|
35
35
|
export const COL_DEFAULT_WIDTH = "12rem";
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
/**
|
|
38
|
+
* TableColumn interface defines the structure of a column in a table.
|
|
39
|
+
* It includes properties for column identification, display options, alignment,
|
|
40
|
+
* visibility, resizing, sorting, filtering, and rendering.
|
|
41
|
+
*
|
|
42
|
+
* @template T - The type of data the column will represent, defaulting to KeyValue.
|
|
43
|
+
* @property {keyof T | string} id - Unique identifier for the column.
|
|
44
|
+
* @property {"string" | "number" | "boolean" | "datetime" | "date" | "time" | "unknown"} [dataType] - Data type of the column values.
|
|
45
|
+
* @property {string} [label] - Display label for the column header.
|
|
46
|
+
* @property {string} [tooltip] - Tooltip text for the column header.
|
|
47
|
+
* @property {string} [format] - Format string for displaying the column values.
|
|
48
|
+
* @property {"start" | "center" | "end"} [align] - Text alignment within the column.
|
|
49
|
+
* @property {"start" | "end"} [locked] - Indicates if the column is locked at the start or end.
|
|
50
|
+
* @property {boolean} [hidden] - Whether the column is hidden.
|
|
51
|
+
* @property {boolean} [truncate] - Whether to truncate the text in the column.
|
|
52
|
+
* @property {boolean} [hideable] - Whether the column can be hidden by the user.
|
|
53
|
+
* @property {boolean} [resizable] - Whether the column can be resized by the user.
|
|
54
|
+
* @property {boolean} [sortable] - Whether the column can be sorted.
|
|
55
|
+
* @property {boolean} [filterable] - Whether the column can be filtered.
|
|
56
|
+
* @property {AnyObject} [filter] - Filter criteria for the column.
|
|
57
|
+
* @property {ReactElement[]} [actions] - Array of action elements to be displayed in the column.
|
|
58
|
+
* @property {AnyObject[]} [filterOptions] - Options for filtering the column.
|
|
59
|
+
* @property {string | number} [width] - Width of the column.
|
|
60
|
+
* @property {string | number} [minWidth] - Minimum width of the column.
|
|
61
|
+
* @property {string | number} [maxWidth] - Maximum width of the column.
|
|
62
|
+
* @property {KeyValue} [valueMap] - Mapping of values for the column.
|
|
63
|
+
* @property {(value: AnyObject, data: T, index: number) => Elements<AnyObject>} [renderer] - Custom renderer function for the column values.
|
|
64
|
+
*/
|
|
65
|
+
export interface TableColumn<T = KeyValue> extends CssProp, TestProps {
|
|
41
66
|
id: keyof T | string;
|
|
67
|
+
field?: string;
|
|
42
68
|
label?: string;
|
|
43
69
|
tooltip?: string;
|
|
44
|
-
|
|
70
|
+
icon?: IconProps;
|
|
71
|
+
actions?: ReactElement[];
|
|
45
72
|
align?: "start" | "center" | "end";
|
|
46
73
|
locked?: "start" | "end";
|
|
47
74
|
hidden?: boolean;
|
|
75
|
+
truncate?: boolean;
|
|
48
76
|
hideable?: boolean;
|
|
49
77
|
resizable?: boolean;
|
|
50
78
|
sortable?: boolean;
|
|
51
79
|
filterable?: boolean;
|
|
52
|
-
|
|
80
|
+
filter?: AnyObject;
|
|
81
|
+
filterOptions?: AnyObject[];
|
|
53
82
|
width?: string | number;
|
|
54
83
|
minWidth?: string | number;
|
|
55
84
|
maxWidth?: string | number;
|
|
@@ -76,12 +105,16 @@ export interface TableProps<T = KeyValue> extends RefProp<TableRef> {
|
|
|
76
105
|
columns: Array<TableColumn<T>>;
|
|
77
106
|
data: T[];
|
|
78
107
|
keyProperty?: keyof T;
|
|
108
|
+
groupProperty?: keyof T;
|
|
109
|
+
groupRenderer?: (key: AnyObject) => ReactElement;
|
|
79
110
|
emptyDisplay?: ReactElement;
|
|
111
|
+
|
|
80
112
|
/**
|
|
81
113
|
* checkable rows
|
|
82
114
|
*/
|
|
115
|
+
checked?: AnyObject[];
|
|
83
116
|
checkableRows?: boolean;
|
|
84
|
-
onCheckedChanged?: (rows:
|
|
117
|
+
onCheckedChanged?: (rows: AnyObject[]) => void;
|
|
85
118
|
/**
|
|
86
119
|
* sorting
|
|
87
120
|
*/
|
|
@@ -98,4 +131,9 @@ export interface TableProps<T = KeyValue> extends RefProp<TableRef> {
|
|
|
98
131
|
|
|
99
132
|
canExpand?: (record: T) => boolean;
|
|
100
133
|
children?: (record: T) => Elements<AnyObject>;
|
|
134
|
+
|
|
135
|
+
onFilter?: (id: keyof T | string, value?: AnyObject) => void;
|
|
136
|
+
|
|
137
|
+
total?: number;
|
|
138
|
+
onLoadMore?: (lastIndex?: number) => void;
|
|
101
139
|
}
|
package/src/table/useResizer.ts
CHANGED
|
@@ -47,13 +47,15 @@ export const useResizer = (
|
|
|
47
47
|
/** ***************** attach handlers *******************/
|
|
48
48
|
const box = colEl.getBoundingClientRect();
|
|
49
49
|
if (colEl.parentElement != null) {
|
|
50
|
-
const parentBox = colEl
|
|
50
|
+
const parentBox = colEl
|
|
51
|
+
.closest("[data-ref='tableScroller']")
|
|
52
|
+
?.getBoundingClientRect() as unknown as DOMRect;
|
|
51
53
|
if (isRtl) {
|
|
52
54
|
placeholder.style.left = "unset";
|
|
53
|
-
placeholder.style.right = `${
|
|
55
|
+
placeholder.style.right = `${parentBox.width - box.right}px`;
|
|
54
56
|
} else {
|
|
55
57
|
placeholder.style.right = "unset";
|
|
56
|
-
placeholder.style.left = `${box.left
|
|
58
|
+
placeholder.style.left = `${box.left}px`;
|
|
57
59
|
}
|
|
58
60
|
}
|
|
59
61
|
placeholder.style.width = `${colEl.offsetWidth}px`;
|
|
@@ -21,37 +21,59 @@
|
|
|
21
21
|
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
24
|
+
import { compareValues, debounce, groupBy } from "@react-fabric/utilities";
|
|
25
|
+
import {
|
|
26
|
+
defaultRangeExtractor,
|
|
27
|
+
type Range,
|
|
28
|
+
useVirtualizer,
|
|
29
|
+
} from "@tanstack/react-virtual";
|
|
30
|
+
import {
|
|
31
|
+
useCallback,
|
|
32
|
+
useEffect,
|
|
33
|
+
useMemo,
|
|
34
|
+
useReducer,
|
|
35
|
+
useRef,
|
|
36
|
+
useState,
|
|
37
|
+
} from "react";
|
|
26
38
|
|
|
27
|
-
interface DataState {
|
|
28
|
-
|
|
29
|
-
|
|
39
|
+
interface DataState<T = KeyValue> {
|
|
40
|
+
data: T[];
|
|
41
|
+
items: T[];
|
|
42
|
+
grouped: KeyValue<T[]>;
|
|
43
|
+
groups: AnyObject[];
|
|
44
|
+
expanded: AnyObject[];
|
|
45
|
+
checked: AnyObject[];
|
|
30
46
|
allChecked: 0 | 1 | 2;
|
|
31
47
|
}
|
|
32
48
|
|
|
33
49
|
type DataAction =
|
|
34
|
-
| { type: "
|
|
35
|
-
| { type: "
|
|
50
|
+
| { type: "updateState"; checked: AnyObject[] }
|
|
51
|
+
| { type: "groupExpand"; key: AnyObject }
|
|
52
|
+
| { type: "reset"; key: AnyObject; data: AnyObject[] }
|
|
53
|
+
| { type: "expand"; key: AnyObject }
|
|
54
|
+
| { type: "check"; key: AnyObject }
|
|
36
55
|
| { type: "checkall" };
|
|
37
56
|
|
|
38
|
-
export const useVirtualTable = (
|
|
39
|
-
data:
|
|
40
|
-
|
|
41
|
-
|
|
57
|
+
export const useVirtualTable = <T extends KeyValue = KeyValue>(
|
|
58
|
+
data: T[],
|
|
59
|
+
options: {
|
|
60
|
+
checked?: AnyObject[];
|
|
61
|
+
keyProperty: keyof T;
|
|
62
|
+
groupProperty?: keyof T;
|
|
63
|
+
loading?: boolean;
|
|
64
|
+
total: number;
|
|
65
|
+
onLoadMore?: (startIndex: number) => void;
|
|
66
|
+
onCheckChanged?: (checked: AnyObject[]) => void;
|
|
67
|
+
},
|
|
42
68
|
) => {
|
|
43
69
|
// TODO: refactor data into item list, apply grouping if groupColumn provided
|
|
44
70
|
// TODO: when grouping maintain map of group with children, {___GROUP___:true, label, items, open}
|
|
45
71
|
|
|
46
|
-
const scrollerRef = useRef<HTMLDivElement>(null);
|
|
47
|
-
const virtualizer = useVirtualizer({
|
|
48
|
-
count: data.length,
|
|
49
|
-
getScrollElement: () => scrollerRef.current,
|
|
50
|
-
estimateSize: () => 24,
|
|
51
|
-
});
|
|
52
|
-
|
|
53
72
|
const [state, dispatch] = useReducer(
|
|
54
|
-
(state: DataState
|
|
73
|
+
(state: DataState<T>, action: DataAction) => {
|
|
74
|
+
if (action.type === "updateState") {
|
|
75
|
+
state.checked = action.checked;
|
|
76
|
+
}
|
|
55
77
|
if (action.type === "expand") {
|
|
56
78
|
if (state.expanded.includes(action.key))
|
|
57
79
|
state.expanded.splice(state.expanded.indexOf(action.key), 1);
|
|
@@ -61,51 +83,160 @@ export const useVirtualTable = (
|
|
|
61
83
|
if (state.checked.includes(action.key))
|
|
62
84
|
state.checked.splice(state.checked.indexOf(action.key), 1);
|
|
63
85
|
else state.checked.push(action.key);
|
|
64
|
-
onCheckChanged?.(state.checked);
|
|
86
|
+
options.onCheckChanged?.(state.checked);
|
|
65
87
|
}
|
|
66
88
|
if (action.type === "checkall") {
|
|
67
89
|
if (state.allChecked !== 0) {
|
|
68
90
|
state.checked = [];
|
|
69
|
-
} else if (keyProperty) {
|
|
70
|
-
state.checked = data.map((item) => item[keyProperty]);
|
|
91
|
+
} else if (options.keyProperty) {
|
|
92
|
+
state.checked = state.data.map((item) => item[options.keyProperty]);
|
|
93
|
+
}
|
|
94
|
+
options.onCheckChanged?.(state.checked);
|
|
95
|
+
}
|
|
96
|
+
const checkList = state.data.filter((hit) =>
|
|
97
|
+
state.checked.includes(hit[options.keyProperty]),
|
|
98
|
+
);
|
|
99
|
+
if (action.type === "groupExpand") {
|
|
100
|
+
const group = state.groups.find((group) => group.key === action.key);
|
|
101
|
+
if (group) {
|
|
102
|
+
group.open = !group.open;
|
|
103
|
+
state.groups = state.groups.slice();
|
|
104
|
+
state.items = state.groups
|
|
105
|
+
.map((group) => [
|
|
106
|
+
group,
|
|
107
|
+
group.open ? state.grouped[group.key] : undefined,
|
|
108
|
+
])
|
|
109
|
+
.flat(2)
|
|
110
|
+
.filter(Boolean) as any;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (action.type === "reset") {
|
|
114
|
+
state.data = action.data;
|
|
115
|
+
if (action.key) {
|
|
116
|
+
state.grouped = groupBy<T>(state.data, action.key);
|
|
117
|
+
state.groups = Object.keys(state.grouped)
|
|
118
|
+
.sort(compareValues("asc"))
|
|
119
|
+
.map((group) => ({
|
|
120
|
+
__GROUP__: true,
|
|
121
|
+
open: true,
|
|
122
|
+
key: group,
|
|
123
|
+
items: state.grouped[group],
|
|
124
|
+
itemCount: state.grouped[group].length,
|
|
125
|
+
}));
|
|
126
|
+
state.items = state.groups
|
|
127
|
+
.map((group) => [group, ...state.grouped[group.key]])
|
|
128
|
+
.flat() as any;
|
|
129
|
+
} else {
|
|
130
|
+
state.items = state.data;
|
|
71
131
|
}
|
|
72
|
-
onCheckChanged?.(state.checked);
|
|
73
132
|
}
|
|
74
133
|
return {
|
|
75
134
|
...state,
|
|
76
135
|
allChecked:
|
|
77
|
-
|
|
136
|
+
checkList.length === state.data.length
|
|
78
137
|
? 1
|
|
79
138
|
: state.checked.length
|
|
80
139
|
? 2
|
|
81
140
|
: 0,
|
|
82
|
-
} as DataState
|
|
141
|
+
} as DataState<T>;
|
|
83
142
|
},
|
|
84
143
|
{
|
|
144
|
+
data,
|
|
145
|
+
items: [],
|
|
146
|
+
grouped: {},
|
|
147
|
+
groups: [],
|
|
85
148
|
checked: [],
|
|
86
149
|
expanded: [],
|
|
87
150
|
allChecked: 0,
|
|
88
151
|
},
|
|
89
152
|
);
|
|
90
153
|
|
|
154
|
+
const activeStickyIndexRef = useRef(0);
|
|
155
|
+
const stickyIndexes = useMemo<number[]>(
|
|
156
|
+
() =>
|
|
157
|
+
state.items
|
|
158
|
+
.map((hit, idx) => ("__GROUP__" in hit ? idx : undefined))
|
|
159
|
+
.filter(Boolean) as number[],
|
|
160
|
+
[state.items],
|
|
161
|
+
);
|
|
162
|
+
const isActiveSticky = (index: number) =>
|
|
163
|
+
activeStickyIndexRef.current === index;
|
|
164
|
+
|
|
165
|
+
const [hasActiveSticky, setHasActiveSticky] = useState(false);
|
|
166
|
+
const scrollerRef = useRef<HTMLDivElement>(null);
|
|
167
|
+
const virtualizer = useVirtualizer({
|
|
168
|
+
count: state.items.length,
|
|
169
|
+
getScrollElement: () => scrollerRef.current,
|
|
170
|
+
estimateSize: () => 32,
|
|
171
|
+
rangeExtractor: useCallback(
|
|
172
|
+
(range: Range) => {
|
|
173
|
+
activeStickyIndexRef.current =
|
|
174
|
+
[...stickyIndexes]
|
|
175
|
+
.reverse()
|
|
176
|
+
.find((index) => range.startIndex > index) ?? 0;
|
|
177
|
+
|
|
178
|
+
setHasActiveSticky(range.startIndex !== activeStickyIndexRef.current);
|
|
179
|
+
|
|
180
|
+
const next = new Set([
|
|
181
|
+
activeStickyIndexRef.current,
|
|
182
|
+
...defaultRangeExtractor(range),
|
|
183
|
+
]);
|
|
184
|
+
|
|
185
|
+
return [...next];
|
|
186
|
+
},
|
|
187
|
+
[stickyIndexes],
|
|
188
|
+
),
|
|
189
|
+
});
|
|
190
|
+
|
|
91
191
|
const virtualItems = virtualizer.getVirtualItems();
|
|
92
192
|
|
|
93
193
|
const getData = useCallback(
|
|
94
194
|
(index: number) => {
|
|
95
|
-
return
|
|
195
|
+
return state.items[index];
|
|
96
196
|
},
|
|
97
|
-
[
|
|
197
|
+
[state.items],
|
|
98
198
|
);
|
|
99
199
|
|
|
200
|
+
useEffect(() => {
|
|
201
|
+
dispatch({ type: "reset", key: options.groupProperty, data });
|
|
202
|
+
}, [data, options.groupProperty]);
|
|
203
|
+
|
|
204
|
+
useEffect(() => {
|
|
205
|
+
dispatch({ type: "updateState", checked: options.checked ?? [] });
|
|
206
|
+
}, [options.checked]);
|
|
207
|
+
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
if (options.onLoadMore) {
|
|
210
|
+
const firstItem = virtualItems[0]?.index;
|
|
211
|
+
const lastItem = virtualItems?.slice(-1).pop()?.index;
|
|
212
|
+
const canLoadPrevPage =
|
|
213
|
+
firstItem !== undefined && state.items[firstItem] === null;
|
|
214
|
+
const canLoadNextPage =
|
|
215
|
+
lastItem !== undefined &&
|
|
216
|
+
lastItem === state.items.length - 1 &&
|
|
217
|
+
state.items.length < options.total;
|
|
218
|
+
const db = debounce(options.onLoadMore);
|
|
219
|
+
if (canLoadPrevPage) {
|
|
220
|
+
db(firstItem);
|
|
221
|
+
} else if (!options.loading && canLoadNextPage) {
|
|
222
|
+
db(lastItem + 1);
|
|
223
|
+
}
|
|
224
|
+
return () => db.cancel();
|
|
225
|
+
}
|
|
226
|
+
}, [options.onLoadMore, virtualItems]);
|
|
227
|
+
|
|
100
228
|
return {
|
|
101
229
|
scrollerRef,
|
|
102
230
|
items: virtualItems,
|
|
103
231
|
checkState: state,
|
|
104
232
|
getData,
|
|
105
233
|
virtualizer,
|
|
234
|
+
hasActiveSticky,
|
|
235
|
+
isActiveSticky,
|
|
106
236
|
top: virtualizer.range?.startIndex,
|
|
107
237
|
totalSize: virtualizer.getTotalSize,
|
|
108
238
|
measureElement: virtualizer.measureElement,
|
|
239
|
+
toggleGroupExpand: (key: Key) => dispatch({ type: "groupExpand", key }),
|
|
109
240
|
toggleExpand: (key: Key) => dispatch({ type: "expand", key }),
|
|
110
241
|
toggleChecked: (key: Key) => dispatch({ type: "check", key }),
|
|
111
242
|
toggleAllChecked: () => dispatch({ type: "checkall" }),
|