@ackplus/react-tanstack-data-table 1.1.20 → 1.1.21
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/dist/lib/hooks/use-data-table-engine.d.ts.map +1 -1
- package/dist/lib/hooks/use-data-table-engine.js +4 -0
- package/package.json +3 -4
- package/src/index.ts +0 -75
- package/src/lib/components/data-table-view.tsx +0 -386
- package/src/lib/components/droupdown/menu-dropdown.tsx +0 -103
- package/src/lib/components/filters/filter-value-input.tsx +0 -225
- package/src/lib/components/filters/index.ts +0 -126
- package/src/lib/components/headers/draggable-header.tsx +0 -326
- package/src/lib/components/headers/index.ts +0 -6
- package/src/lib/components/headers/table-header.tsx +0 -175
- package/src/lib/components/index.ts +0 -21
- package/src/lib/components/pagination/data-table-pagination.tsx +0 -111
- package/src/lib/components/pagination/index.ts +0 -5
- package/src/lib/components/rows/data-table-row.tsx +0 -218
- package/src/lib/components/rows/empty-data-row.tsx +0 -69
- package/src/lib/components/rows/index.ts +0 -7
- package/src/lib/components/rows/loading-rows.tsx +0 -164
- package/src/lib/components/toolbar/bulk-actions-toolbar.tsx +0 -125
- package/src/lib/components/toolbar/column-filter-control.tsx +0 -432
- package/src/lib/components/toolbar/column-pinning-control.tsx +0 -275
- package/src/lib/components/toolbar/column-reset-control.tsx +0 -74
- package/src/lib/components/toolbar/column-visibility-control.tsx +0 -105
- package/src/lib/components/toolbar/data-table-toolbar.tsx +0 -257
- package/src/lib/components/toolbar/index.ts +0 -17
- package/src/lib/components/toolbar/table-export-control.tsx +0 -233
- package/src/lib/components/toolbar/table-refresh-control.tsx +0 -62
- package/src/lib/components/toolbar/table-search-control.tsx +0 -155
- package/src/lib/components/toolbar/table-size-control.tsx +0 -102
- package/src/lib/contexts/data-table-context.tsx +0 -126
- package/src/lib/data-table.tsx +0 -29
- package/src/lib/features/README.md +0 -161
- package/src/lib/features/column-filter.feature.ts +0 -493
- package/src/lib/features/index.ts +0 -23
- package/src/lib/features/selection.feature.ts +0 -322
- package/src/lib/hooks/index.ts +0 -2
- package/src/lib/hooks/use-data-table-engine.ts +0 -1552
- package/src/lib/icons/add-icon.tsx +0 -23
- package/src/lib/icons/csv-icon.tsx +0 -15
- package/src/lib/icons/delete-icon.tsx +0 -30
- package/src/lib/icons/excel-icon.tsx +0 -15
- package/src/lib/icons/index.ts +0 -7
- package/src/lib/icons/unpin-icon.tsx +0 -18
- package/src/lib/icons/view-comfortable-icon.tsx +0 -45
- package/src/lib/icons/view-compact-icon.tsx +0 -55
- package/src/lib/types/column.types.ts +0 -63
- package/src/lib/types/data-table-api.ts +0 -191
- package/src/lib/types/data-table.types.ts +0 -193
- package/src/lib/types/export.types.ts +0 -223
- package/src/lib/types/index.ts +0 -24
- package/src/lib/types/slots.types.ts +0 -342
- package/src/lib/types/table.types.ts +0 -88
- package/src/lib/utils/column-helpers.ts +0 -72
- package/src/lib/utils/debounced-fetch.utils.ts +0 -131
- package/src/lib/utils/export-utils.ts +0 -712
- package/src/lib/utils/index.ts +0 -27
- package/src/lib/utils/logger.ts +0 -203
- package/src/lib/utils/slot-helpers.tsx +0 -194
- package/src/lib/utils/special-columns.utils.ts +0 -101
- package/src/lib/utils/styling-helpers.ts +0 -126
- package/src/lib/utils/table-helpers.ts +0 -106
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
import React, { ReactElement } from 'react';
|
|
2
|
-
import { FormControl, InputLabel, Select, MenuItem, TextField, Checkbox, ListItemText, Box, FormControlProps, TextFieldProps, SelectProps, SxProps } from '@mui/material';
|
|
3
|
-
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
|
4
|
-
import { Column } from '@tanstack/react-table';
|
|
5
|
-
import moment from 'moment';
|
|
6
|
-
|
|
7
|
-
import { getColumnOptions, getColumnType, getCustomFilterComponent } from '../../utils/column-helpers';
|
|
8
|
-
import { AdapterMoment } from '@mui/x-date-pickers/AdapterMoment';
|
|
9
|
-
import { LocalizationProvider } from '@mui/x-date-pickers';
|
|
10
|
-
import { ColumnFilterRule } from '../../features';
|
|
11
|
-
|
|
12
|
-
interface FilterValueInputProps<T> {
|
|
13
|
-
filter: ColumnFilterRule;
|
|
14
|
-
column: Column<T, any>;
|
|
15
|
-
onValueChange: (value: any) => void;
|
|
16
|
-
// Enhanced customization props
|
|
17
|
-
formControlProps?: FormControlProps;
|
|
18
|
-
textFieldProps?: TextFieldProps;
|
|
19
|
-
selectProps?: SelectProps;
|
|
20
|
-
datePickerProps?: any;
|
|
21
|
-
containerSx?: SxProps;
|
|
22
|
-
[key: string]: any;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function FilterValueInput<T>(props: FilterValueInputProps<T>): ReactElement {
|
|
26
|
-
const {
|
|
27
|
-
filter,
|
|
28
|
-
column,
|
|
29
|
-
onValueChange,
|
|
30
|
-
formControlProps,
|
|
31
|
-
textFieldProps,
|
|
32
|
-
selectProps,
|
|
33
|
-
datePickerProps,
|
|
34
|
-
containerSx,
|
|
35
|
-
...otherProps
|
|
36
|
-
} = props;
|
|
37
|
-
|
|
38
|
-
const columnType = getColumnType(column);
|
|
39
|
-
const customComponent = getCustomFilterComponent(column);
|
|
40
|
-
const options = getColumnOptions(column);
|
|
41
|
-
const operator = filter.operator;
|
|
42
|
-
|
|
43
|
-
// If custom component is provided, use it
|
|
44
|
-
if (customComponent) {
|
|
45
|
-
const CustomComponent = customComponent;
|
|
46
|
-
return (
|
|
47
|
-
<Box sx={containerSx}>
|
|
48
|
-
<CustomComponent
|
|
49
|
-
value={filter.value}
|
|
50
|
-
onChange={onValueChange}
|
|
51
|
-
filter={filter}
|
|
52
|
-
column={column}
|
|
53
|
-
{...otherProps}
|
|
54
|
-
/>
|
|
55
|
-
</Box>
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Boolean type - Yes/No select
|
|
60
|
-
if (columnType === 'boolean') {
|
|
61
|
-
return (
|
|
62
|
-
<FormControl
|
|
63
|
-
size="small"
|
|
64
|
-
sx={{
|
|
65
|
-
flex: 1,
|
|
66
|
-
minWidth: 120,
|
|
67
|
-
...containerSx,
|
|
68
|
-
}}
|
|
69
|
-
{...formControlProps}
|
|
70
|
-
>
|
|
71
|
-
<InputLabel>Value</InputLabel>
|
|
72
|
-
<Select
|
|
73
|
-
value={filter.value || 'any'}
|
|
74
|
-
label="Value"
|
|
75
|
-
onChange={(e) => onValueChange(e.target.value)}
|
|
76
|
-
{...selectProps}
|
|
77
|
-
>
|
|
78
|
-
<MenuItem
|
|
79
|
-
key={'any'}
|
|
80
|
-
value={'any'}
|
|
81
|
-
>
|
|
82
|
-
Any
|
|
83
|
-
</MenuItem>
|
|
84
|
-
<MenuItem
|
|
85
|
-
key={'true'}
|
|
86
|
-
value={'true'}
|
|
87
|
-
>
|
|
88
|
-
True
|
|
89
|
-
</MenuItem>
|
|
90
|
-
<MenuItem
|
|
91
|
-
key={'false'}
|
|
92
|
-
value={'false'}
|
|
93
|
-
>
|
|
94
|
-
False
|
|
95
|
-
</MenuItem>
|
|
96
|
-
</Select>
|
|
97
|
-
</FormControl>
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Select type with options
|
|
102
|
-
if (options && options.length > 0) {
|
|
103
|
-
// Multi-select for set operators
|
|
104
|
-
if (operator === 'in' || operator === 'notIn') {
|
|
105
|
-
const currentValue = Array.isArray(filter.value) ? filter.value : [];
|
|
106
|
-
|
|
107
|
-
return (
|
|
108
|
-
<FormControl
|
|
109
|
-
size="small"
|
|
110
|
-
sx={{
|
|
111
|
-
flex: 1,
|
|
112
|
-
minWidth: 120,
|
|
113
|
-
...containerSx,
|
|
114
|
-
}}
|
|
115
|
-
{...formControlProps}
|
|
116
|
-
>
|
|
117
|
-
<InputLabel>Values</InputLabel>
|
|
118
|
-
<Select
|
|
119
|
-
multiple
|
|
120
|
-
value={currentValue}
|
|
121
|
-
label="Values"
|
|
122
|
-
onChange={(e) => onValueChange(e.target.value)}
|
|
123
|
-
renderValue={(selected) => (selected as string[]).join(', ')}
|
|
124
|
-
{...selectProps}
|
|
125
|
-
>
|
|
126
|
-
{options.map(option => (
|
|
127
|
-
<MenuItem key={String(option.value)} value={option.value}>
|
|
128
|
-
<Checkbox checked={currentValue.includes(option.value)} />
|
|
129
|
-
<ListItemText primary={option.label} />
|
|
130
|
-
</MenuItem>
|
|
131
|
-
))}
|
|
132
|
-
</Select>
|
|
133
|
-
</FormControl>
|
|
134
|
-
);
|
|
135
|
-
}
|
|
136
|
-
// Single select for other operators
|
|
137
|
-
return (
|
|
138
|
-
<FormControl
|
|
139
|
-
size="small"
|
|
140
|
-
sx={{
|
|
141
|
-
flex: 1,
|
|
142
|
-
minWidth: 120,
|
|
143
|
-
...containerSx,
|
|
144
|
-
}}
|
|
145
|
-
{...formControlProps}
|
|
146
|
-
>
|
|
147
|
-
<InputLabel>Value</InputLabel>
|
|
148
|
-
<Select
|
|
149
|
-
value={filter.value}
|
|
150
|
-
label="Value"
|
|
151
|
-
onChange={(e) => onValueChange(e.target.value)}
|
|
152
|
-
{...selectProps}
|
|
153
|
-
>
|
|
154
|
-
{options.map(option => (
|
|
155
|
-
<MenuItem key={String(option.value)} value={option.value}>
|
|
156
|
-
{option.label}
|
|
157
|
-
</MenuItem>
|
|
158
|
-
))}
|
|
159
|
-
</Select>
|
|
160
|
-
</FormControl>
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Date type
|
|
165
|
-
if (columnType === 'date') {
|
|
166
|
-
// Only single date picker, no 'between' support
|
|
167
|
-
return (
|
|
168
|
-
<LocalizationProvider dateAdapter={AdapterMoment}>
|
|
169
|
-
<DatePicker
|
|
170
|
-
value={filter.value ? moment(filter.value) : null}
|
|
171
|
-
onChange={(e) => onValueChange(e?.toDate())}
|
|
172
|
-
slotProps={{
|
|
173
|
-
textField: {
|
|
174
|
-
size: 'small',
|
|
175
|
-
label: 'Value',
|
|
176
|
-
sx: {
|
|
177
|
-
flex: 1,
|
|
178
|
-
minWidth: 120,
|
|
179
|
-
...containerSx,
|
|
180
|
-
},
|
|
181
|
-
...textFieldProps,
|
|
182
|
-
},
|
|
183
|
-
}}
|
|
184
|
-
{...datePickerProps}
|
|
185
|
-
/>
|
|
186
|
-
</LocalizationProvider>
|
|
187
|
-
);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Number type
|
|
191
|
-
if (columnType === 'number') {
|
|
192
|
-
// Only single number input, no 'between' support
|
|
193
|
-
return (
|
|
194
|
-
<TextField
|
|
195
|
-
size="small"
|
|
196
|
-
label="Value"
|
|
197
|
-
value={filter.value}
|
|
198
|
-
onChange={(e) => onValueChange(e.target.value)}
|
|
199
|
-
type="number"
|
|
200
|
-
sx={{
|
|
201
|
-
flex: 1,
|
|
202
|
-
minWidth: 120,
|
|
203
|
-
...containerSx,
|
|
204
|
-
}}
|
|
205
|
-
{...textFieldProps}
|
|
206
|
-
/>
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// Default: text input
|
|
211
|
-
return (
|
|
212
|
-
<TextField
|
|
213
|
-
size="small"
|
|
214
|
-
label="Value"
|
|
215
|
-
value={filter.value}
|
|
216
|
-
onChange={(e) => onValueChange(e.target.value)}
|
|
217
|
-
sx={{
|
|
218
|
-
flex: 1,
|
|
219
|
-
minWidth: 120,
|
|
220
|
-
...containerSx,
|
|
221
|
-
}}
|
|
222
|
-
{...textFieldProps}
|
|
223
|
-
/>
|
|
224
|
-
);
|
|
225
|
-
}
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
export const FILTER_OPERATORS = {
|
|
2
|
-
text: [
|
|
3
|
-
{
|
|
4
|
-
value: 'contains',
|
|
5
|
-
label: 'Contains',
|
|
6
|
-
},
|
|
7
|
-
{
|
|
8
|
-
value: 'startsWith',
|
|
9
|
-
label: 'Starts with',
|
|
10
|
-
},
|
|
11
|
-
{
|
|
12
|
-
value: 'endsWith',
|
|
13
|
-
label: 'Ends with',
|
|
14
|
-
},
|
|
15
|
-
{
|
|
16
|
-
value: 'equals',
|
|
17
|
-
label: 'Equals',
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
value: 'notEquals',
|
|
21
|
-
label: 'Not equals',
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
value: 'isEmpty',
|
|
25
|
-
label: 'Is empty',
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
value: 'isNotEmpty',
|
|
29
|
-
label: 'Is not empty',
|
|
30
|
-
},
|
|
31
|
-
],
|
|
32
|
-
boolean: [
|
|
33
|
-
{
|
|
34
|
-
value: 'is',
|
|
35
|
-
label: 'Is',
|
|
36
|
-
},
|
|
37
|
-
],
|
|
38
|
-
number: [
|
|
39
|
-
{
|
|
40
|
-
value: 'equals',
|
|
41
|
-
label: 'Equals',
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
value: 'notEquals',
|
|
45
|
-
label: 'Not equals',
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
value: 'greaterThan',
|
|
49
|
-
label: 'Greater than',
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
value: 'lessThan',
|
|
53
|
-
label: 'Less than',
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
value: 'greaterThanOrEqual',
|
|
57
|
-
label: 'Greater than or equal',
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
value: 'lessThanOrEqual',
|
|
61
|
-
label: 'Less than or equal',
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
value: 'isEmpty',
|
|
65
|
-
label: 'Is empty',
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
value: 'isNotEmpty',
|
|
69
|
-
label: 'Is not empty',
|
|
70
|
-
},
|
|
71
|
-
],
|
|
72
|
-
date: [
|
|
73
|
-
{
|
|
74
|
-
value: 'equals',
|
|
75
|
-
label: 'Equals',
|
|
76
|
-
},
|
|
77
|
-
{
|
|
78
|
-
value: 'notEquals',
|
|
79
|
-
label: 'Not equals',
|
|
80
|
-
},
|
|
81
|
-
{
|
|
82
|
-
value: 'after',
|
|
83
|
-
label: 'After',
|
|
84
|
-
},
|
|
85
|
-
{
|
|
86
|
-
value: 'before',
|
|
87
|
-
label: 'Before',
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
value: 'isEmpty',
|
|
91
|
-
label: 'Is empty',
|
|
92
|
-
},
|
|
93
|
-
{
|
|
94
|
-
value: 'isNotEmpty',
|
|
95
|
-
label: 'Is not empty',
|
|
96
|
-
},
|
|
97
|
-
],
|
|
98
|
-
select: [
|
|
99
|
-
{
|
|
100
|
-
value: 'equals',
|
|
101
|
-
label: 'Equals',
|
|
102
|
-
},
|
|
103
|
-
{
|
|
104
|
-
value: 'notEquals',
|
|
105
|
-
label: 'Not equals',
|
|
106
|
-
},
|
|
107
|
-
{
|
|
108
|
-
value: 'in',
|
|
109
|
-
label: 'In',
|
|
110
|
-
},
|
|
111
|
-
{
|
|
112
|
-
value: 'notIn',
|
|
113
|
-
label: 'Not in',
|
|
114
|
-
},
|
|
115
|
-
{
|
|
116
|
-
value: 'isEmpty',
|
|
117
|
-
label: 'Is empty',
|
|
118
|
-
},
|
|
119
|
-
{
|
|
120
|
-
value: 'isNotEmpty',
|
|
121
|
-
label: 'Is not empty',
|
|
122
|
-
},
|
|
123
|
-
],
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
export * from './filter-value-input';
|
|
@@ -1,326 +0,0 @@
|
|
|
1
|
-
import { ArrowUpwardOutlined, ArrowDownwardOutlined } from '@mui/icons-material';
|
|
2
|
-
import { Box, SxProps } from '@mui/material';
|
|
3
|
-
import { Header, flexRender } from '@tanstack/react-table';
|
|
4
|
-
import React, { useState, useRef, useCallback, useMemo } from 'react';
|
|
5
|
-
|
|
6
|
-
import { getSlotComponent, mergeSlotProps, extractSlotProps } from '../../utils/slot-helpers';
|
|
7
|
-
|
|
8
|
-
interface DraggableHeaderProps<T> {
|
|
9
|
-
header: Header<T, unknown>;
|
|
10
|
-
enableSorting?: boolean;
|
|
11
|
-
draggable?: boolean;
|
|
12
|
-
onColumnReorder?: (draggedColumnId: string, targetColumnId: string) => void;
|
|
13
|
-
// Enhanced customization props
|
|
14
|
-
containerSx?: SxProps;
|
|
15
|
-
sortIconProps?: any;
|
|
16
|
-
alignment?: 'left' | 'center' | 'right';
|
|
17
|
-
slots?: Record<string, any>;
|
|
18
|
-
slotProps?: Record<string, any>;
|
|
19
|
-
[key: string]: any;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function DraggableHeader<T>(props: DraggableHeaderProps<T>) {
|
|
23
|
-
const {
|
|
24
|
-
header,
|
|
25
|
-
enableSorting = true,
|
|
26
|
-
draggable = false,
|
|
27
|
-
onColumnReorder,
|
|
28
|
-
containerSx,
|
|
29
|
-
sortIconProps,
|
|
30
|
-
alignment,
|
|
31
|
-
slots,
|
|
32
|
-
slotProps,
|
|
33
|
-
...otherProps
|
|
34
|
-
} = props;
|
|
35
|
-
|
|
36
|
-
const [isDragging, setIsDragging] = useState(false);
|
|
37
|
-
const [dragOver, setDragOver] = useState(false);
|
|
38
|
-
const autoScrollIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
|
39
|
-
const dragStartPositionRef = useRef<{ x: number; y: number } | null>(null);
|
|
40
|
-
const headerElementRef = useRef<HTMLDivElement>(null);
|
|
41
|
-
|
|
42
|
-
// Extract slot-specific props with enhanced merging
|
|
43
|
-
const sortIconAscSlotProps = extractSlotProps(slotProps, 'sortIconAsc');
|
|
44
|
-
const sortIconDescSlotProps = extractSlotProps(slotProps, 'sortIconDesc');
|
|
45
|
-
|
|
46
|
-
const SortIconAscSlot = getSlotComponent(slots, 'sortIconAsc', ArrowUpwardOutlined);
|
|
47
|
-
const SortIconDescSlot = getSlotComponent(slots, 'sortIconDesc', ArrowDownwardOutlined);
|
|
48
|
-
|
|
49
|
-
// Auto-scroll configuration
|
|
50
|
-
const AUTO_SCROLL_THRESHOLD = 50; // Distance from edge to trigger scroll
|
|
51
|
-
const AUTO_SCROLL_SPEED = 10; // Pixels per scroll interval
|
|
52
|
-
const AUTO_SCROLL_INTERVAL = 16; // ~60fps
|
|
53
|
-
|
|
54
|
-
const justifyContent = useMemo(() => {
|
|
55
|
-
return alignment === 'left' ? 'flex-start' : alignment === 'right' ? 'flex-end' : 'center';
|
|
56
|
-
}, [alignment]);
|
|
57
|
-
|
|
58
|
-
const findScrollableContainer = useCallback((element?: HTMLElement): HTMLElement | null => {
|
|
59
|
-
// Start from the provided element or try to find the current table
|
|
60
|
-
let searchElement = element;
|
|
61
|
-
|
|
62
|
-
if (!searchElement) {
|
|
63
|
-
// Start from the header element if available
|
|
64
|
-
if (headerElementRef.current) {
|
|
65
|
-
searchElement = headerElementRef.current.closest('table') as HTMLElement;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (!searchElement) {
|
|
70
|
-
// Find the table that contains our header
|
|
71
|
-
const tables = Array.from(document.querySelectorAll('table'));
|
|
72
|
-
for (const table of tables) {
|
|
73
|
-
// Check if this table contains a header with our ID
|
|
74
|
-
const headerExists = table.querySelector('th'); // fallback
|
|
75
|
-
if (headerExists) {
|
|
76
|
-
searchElement = table;
|
|
77
|
-
break;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (!searchElement) {
|
|
83
|
-
// Last resort: use the first table found
|
|
84
|
-
searchElement = document.querySelector('table') as HTMLElement;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (!searchElement) return null;
|
|
88
|
-
|
|
89
|
-
// Walk up the DOM tree to find the scrollable container
|
|
90
|
-
let container: HTMLElement | null = searchElement;
|
|
91
|
-
while (container && container !== document.body) {
|
|
92
|
-
const styles = window.getComputedStyle(container);
|
|
93
|
-
if (styles.overflowX === 'auto' || styles.overflowX === 'scroll') {
|
|
94
|
-
return container;
|
|
95
|
-
}
|
|
96
|
-
container = container.parentElement;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return null;
|
|
100
|
-
}, []);
|
|
101
|
-
|
|
102
|
-
const startAutoScroll = useCallback((direction: 'left' | 'right') => {
|
|
103
|
-
if (autoScrollIntervalRef.current) {
|
|
104
|
-
clearInterval(autoScrollIntervalRef.current);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const container = findScrollableContainer();
|
|
108
|
-
if (!container) return;
|
|
109
|
-
|
|
110
|
-
autoScrollIntervalRef.current = setInterval(() => {
|
|
111
|
-
const scrollAmount = direction === 'left' ? -AUTO_SCROLL_SPEED : AUTO_SCROLL_SPEED;
|
|
112
|
-
const newScrollLeft = container.scrollLeft + scrollAmount;
|
|
113
|
-
|
|
114
|
-
// Check bounds
|
|
115
|
-
if (direction === 'left' && newScrollLeft <= 0) {
|
|
116
|
-
container.scrollLeft = 0;
|
|
117
|
-
clearInterval(autoScrollIntervalRef.current!);
|
|
118
|
-
autoScrollIntervalRef.current = null;
|
|
119
|
-
} else if (direction === 'right' && newScrollLeft >= container.scrollWidth - container.clientWidth) {
|
|
120
|
-
container.scrollLeft = container.scrollWidth - container.clientWidth;
|
|
121
|
-
clearInterval(autoScrollIntervalRef.current!);
|
|
122
|
-
autoScrollIntervalRef.current = null;
|
|
123
|
-
} else {
|
|
124
|
-
container.scrollLeft = newScrollLeft;
|
|
125
|
-
}
|
|
126
|
-
}, AUTO_SCROLL_INTERVAL);
|
|
127
|
-
}, [findScrollableContainer]);
|
|
128
|
-
|
|
129
|
-
const stopAutoScroll = useCallback(() => {
|
|
130
|
-
if (autoScrollIntervalRef.current) {
|
|
131
|
-
clearInterval(autoScrollIntervalRef.current);
|
|
132
|
-
autoScrollIntervalRef.current = null;
|
|
133
|
-
}
|
|
134
|
-
}, []);
|
|
135
|
-
|
|
136
|
-
const checkAutoScroll = useCallback((clientX: number) => {
|
|
137
|
-
const container = findScrollableContainer();
|
|
138
|
-
if (!container) return;
|
|
139
|
-
|
|
140
|
-
const containerRect = container.getBoundingClientRect();
|
|
141
|
-
const leftEdge = containerRect.left + AUTO_SCROLL_THRESHOLD;
|
|
142
|
-
const rightEdge = containerRect.right - AUTO_SCROLL_THRESHOLD;
|
|
143
|
-
|
|
144
|
-
if (clientX < leftEdge) {
|
|
145
|
-
startAutoScroll('left');
|
|
146
|
-
} else if (clientX > rightEdge) {
|
|
147
|
-
startAutoScroll('right');
|
|
148
|
-
} else {
|
|
149
|
-
stopAutoScroll();
|
|
150
|
-
}
|
|
151
|
-
}, [findScrollableContainer, startAutoScroll, stopAutoScroll]);
|
|
152
|
-
|
|
153
|
-
const handleDragStart = (e: React.DragEvent) => {
|
|
154
|
-
if (!draggable) return;
|
|
155
|
-
|
|
156
|
-
e.dataTransfer.setData('text/plain', header.id);
|
|
157
|
-
e.dataTransfer.effectAllowed = 'move';
|
|
158
|
-
setIsDragging(true);
|
|
159
|
-
dragStartPositionRef.current = { x: e.clientX, y: e.clientY };
|
|
160
|
-
};
|
|
161
|
-
|
|
162
|
-
const handleDrag = (e: React.DragEvent) => {
|
|
163
|
-
if (!draggable || !dragStartPositionRef.current) return;
|
|
164
|
-
checkAutoScroll(e.clientX);
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
const getSortIcon = () => {
|
|
168
|
-
if (!enableSorting) return null;
|
|
169
|
-
const sortDirection = header.column.getIsSorted();
|
|
170
|
-
|
|
171
|
-
const mergedSortIconProps = mergeSlotProps(
|
|
172
|
-
{
|
|
173
|
-
fontSize: 'small',
|
|
174
|
-
},
|
|
175
|
-
sortIconProps || {}
|
|
176
|
-
);
|
|
177
|
-
|
|
178
|
-
// Only show icons when column is actually sorted
|
|
179
|
-
if (sortDirection === 'asc') {
|
|
180
|
-
return (
|
|
181
|
-
<SortIconAscSlot
|
|
182
|
-
{...mergeSlotProps(
|
|
183
|
-
mergedSortIconProps,
|
|
184
|
-
sortIconAscSlotProps
|
|
185
|
-
)}
|
|
186
|
-
/>
|
|
187
|
-
);
|
|
188
|
-
} if (sortDirection === 'desc') {
|
|
189
|
-
return (
|
|
190
|
-
<SortIconDescSlot
|
|
191
|
-
{...mergeSlotProps(
|
|
192
|
-
mergedSortIconProps,
|
|
193
|
-
sortIconDescSlotProps
|
|
194
|
-
)}
|
|
195
|
-
/>
|
|
196
|
-
);
|
|
197
|
-
}
|
|
198
|
-
// Don't show any icon when not sorted
|
|
199
|
-
return null;
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
const handleDragEnd = () => {
|
|
203
|
-
setIsDragging(false);
|
|
204
|
-
setDragOver(false);
|
|
205
|
-
stopAutoScroll();
|
|
206
|
-
dragStartPositionRef.current = null;
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
const handleDragOver = (e: React.DragEvent) => {
|
|
210
|
-
if (!draggable) return;
|
|
211
|
-
|
|
212
|
-
e.preventDefault();
|
|
213
|
-
e.dataTransfer.dropEffect = 'move';
|
|
214
|
-
setDragOver(true);
|
|
215
|
-
|
|
216
|
-
// Check for auto-scroll during drag over
|
|
217
|
-
checkAutoScroll(e.clientX);
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
const handleDragLeave = () => {
|
|
221
|
-
setDragOver(false);
|
|
222
|
-
// Don't stop auto-scroll on drag leave as the drag might continue outside this element
|
|
223
|
-
};
|
|
224
|
-
|
|
225
|
-
const handleDrop = (e: React.DragEvent) => {
|
|
226
|
-
if (!draggable) return;
|
|
227
|
-
|
|
228
|
-
e.preventDefault();
|
|
229
|
-
const draggedColumnId = e.dataTransfer.getData('text/plain');
|
|
230
|
-
if (draggedColumnId !== header.id && onColumnReorder) {
|
|
231
|
-
onColumnReorder(draggedColumnId, header.id);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
setDragOver(false);
|
|
235
|
-
stopAutoScroll();
|
|
236
|
-
};
|
|
237
|
-
|
|
238
|
-
const handleSort = () => {
|
|
239
|
-
if (!enableSorting || !header.column.getCanSort()) return;
|
|
240
|
-
|
|
241
|
-
const currentSort = header.column.getIsSorted();
|
|
242
|
-
if (currentSort === false) {
|
|
243
|
-
header.column.toggleSorting(false); // asc
|
|
244
|
-
} else if (currentSort === 'asc') {
|
|
245
|
-
header.column.toggleSorting(true); // desc
|
|
246
|
-
} else {
|
|
247
|
-
header.column.clearSorting(); // none
|
|
248
|
-
}
|
|
249
|
-
};
|
|
250
|
-
|
|
251
|
-
const getCursor = () => {
|
|
252
|
-
if (draggable) return 'grab';
|
|
253
|
-
if (enableSorting && header.column.getCanSort()) return 'pointer';
|
|
254
|
-
return 'default';
|
|
255
|
-
};
|
|
256
|
-
|
|
257
|
-
// Merge all props for maximum flexibility
|
|
258
|
-
const mergedContainerProps = mergeSlotProps(
|
|
259
|
-
{
|
|
260
|
-
ref: headerElementRef,
|
|
261
|
-
sx: {
|
|
262
|
-
display: 'flex',
|
|
263
|
-
alignItems: 'center',
|
|
264
|
-
justifyContent: justifyContent,
|
|
265
|
-
gap: 1,
|
|
266
|
-
cursor: getCursor(),
|
|
267
|
-
userSelect: 'none',
|
|
268
|
-
opacity: isDragging ? 0.5 : 1,
|
|
269
|
-
backgroundColor: dragOver ? 'rgba(25, 118, 210, 0.08)' : 'transparent',
|
|
270
|
-
borderLeft: dragOver ? '2px solid #1976d2' : 'none',
|
|
271
|
-
padding: dragOver ? '0 0 0 -2px' : '0',
|
|
272
|
-
width: '100%',
|
|
273
|
-
height: '100%',
|
|
274
|
-
minWidth: '0',
|
|
275
|
-
'&:active': {
|
|
276
|
-
cursor: draggable ? 'grabbing' : 'pointer',
|
|
277
|
-
},
|
|
278
|
-
'.header-content': {
|
|
279
|
-
display: 'block',
|
|
280
|
-
flex: 1,
|
|
281
|
-
minWidth: 0,
|
|
282
|
-
overflow: 'hidden',
|
|
283
|
-
whiteSpace: 'nowrap',
|
|
284
|
-
textOverflow: 'ellipsis',
|
|
285
|
-
},
|
|
286
|
-
...containerSx,
|
|
287
|
-
},
|
|
288
|
-
draggable: draggable,
|
|
289
|
-
onDragStart: handleDragStart,
|
|
290
|
-
onDrag: handleDrag,
|
|
291
|
-
onDragEnd: handleDragEnd,
|
|
292
|
-
onDragOver: handleDragOver,
|
|
293
|
-
onDragLeave: handleDragLeave,
|
|
294
|
-
onDrop: handleDrop,
|
|
295
|
-
onClick: enableSorting ? handleSort : undefined,
|
|
296
|
-
},
|
|
297
|
-
otherProps
|
|
298
|
-
);
|
|
299
|
-
|
|
300
|
-
if (!draggable && !enableSorting) {
|
|
301
|
-
return flexRender(header.column.columnDef.header, header.getContext());
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
return (
|
|
305
|
-
<Box
|
|
306
|
-
{...mergedContainerProps}
|
|
307
|
-
>
|
|
308
|
-
<Box
|
|
309
|
-
component="span"
|
|
310
|
-
className='header-wrapper'
|
|
311
|
-
sx={{
|
|
312
|
-
display: 'inline-flex',
|
|
313
|
-
gap: 1,
|
|
314
|
-
}}
|
|
315
|
-
>
|
|
316
|
-
<Box component="span" className='header-content'>
|
|
317
|
-
{flexRender(header.column.columnDef.header, header.getContext())}
|
|
318
|
-
</Box>
|
|
319
|
-
{getSortIcon()}
|
|
320
|
-
</Box>
|
|
321
|
-
</Box>
|
|
322
|
-
);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// Memoize component to prevent unnecessary re-renders
|
|
326
|
-
export const DraggableHeaderMemo = React.memo(DraggableHeader) as typeof DraggableHeader;
|