jattac.libs.web.responsive-table 0.1.5 → 0.2.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/README.md +579 -274
- package/dist/Data/IResponsiveTableColumnDefinition.d.ts +1 -0
- package/dist/Plugins/FilterPlugin.d.ts +12 -0
- package/dist/Plugins/IResponsiveTablePlugin.d.ts +26 -0
- package/dist/Plugins/InfiniteScrollPlugin.d.ts +13 -0
- package/dist/UI/ResponsiveTable.d.ts +28 -5
- package/dist/index.d.ts +4 -1
- package/dist/index.js +1063 -31
- package/dist/index.js.map +1 -1
- package/gemini/project_analysis.md +41 -41
- package/gemini/release_git_steps.md +64 -0
- package/package.json +3 -1
- package/src/Data/IFooterColumnDefinition.ts +21 -21
- package/src/Data/IResponsiveTableColumnDefinition.tsx +12 -11
- package/src/Plugins/FilterPlugin.tsx +71 -0
- package/src/Plugins/IResponsiveTablePlugin.ts +48 -0
- package/src/Plugins/InfiniteScrollPlugin.tsx +73 -0
- package/src/Styles/ResponsiveTable.module.css +27 -2
- package/src/UI/ResponsiveTable.tsx +192 -40
- package/src/index.tsx +10 -7
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import { ReactNode } from 'react';
|
|
2
|
-
|
|
3
|
-
export default interface IFooterColumnDefinition {
|
|
4
|
-
colSpan: number;
|
|
5
|
-
cellRenderer: () => ReactNode;
|
|
6
|
-
/**
|
|
7
|
-
* An optional, explicit label for the footer cell, especially for mobile view.
|
|
8
|
-
* If not provided, the table could try to infer it from the corresponding column.
|
|
9
|
-
*/
|
|
10
|
-
displayLabel?: ReactNode;
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* An optional click handler for the footer cell.
|
|
14
|
-
*/
|
|
15
|
-
onCellClick?: () => void;
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Optional class name for custom styling of the footer cell.
|
|
19
|
-
*/
|
|
20
|
-
className?: string;
|
|
21
|
-
}
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export default interface IFooterColumnDefinition {
|
|
4
|
+
colSpan: number;
|
|
5
|
+
cellRenderer: () => ReactNode;
|
|
6
|
+
/**
|
|
7
|
+
* An optional, explicit label for the footer cell, especially for mobile view.
|
|
8
|
+
* If not provided, the table could try to infer it from the corresponding column.
|
|
9
|
+
*/
|
|
10
|
+
displayLabel?: ReactNode;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* An optional click handler for the footer cell.
|
|
14
|
+
*/
|
|
15
|
+
onCellClick?: () => void;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Optional class name for custom styling of the footer cell.
|
|
19
|
+
*/
|
|
20
|
+
className?: string;
|
|
21
|
+
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { ReactNode } from 'react';
|
|
2
|
-
|
|
3
|
-
export default interface IResponsiveTableColumnDefinition<TData> {
|
|
4
|
-
displayLabel: ReactNode;
|
|
5
|
-
cellRenderer: (data: TData) => ReactNode;
|
|
6
|
-
interactivity?: {
|
|
7
|
-
id: string;
|
|
8
|
-
onHeaderClick?: (id: string) => void;
|
|
9
|
-
className?: string;
|
|
10
|
-
};
|
|
11
|
-
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export default interface IResponsiveTableColumnDefinition<TData> {
|
|
4
|
+
displayLabel: ReactNode;
|
|
5
|
+
cellRenderer: (data: TData) => ReactNode;
|
|
6
|
+
interactivity?: {
|
|
7
|
+
id: string;
|
|
8
|
+
onHeaderClick?: (id: string) => void;
|
|
9
|
+
className?: string;
|
|
10
|
+
};
|
|
11
|
+
getFilterableValue?: (data: TData) => string | number;
|
|
12
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { IResponsiveTablePlugin, IPluginAPI } from './IResponsiveTablePlugin';
|
|
3
|
+
import IResponsiveTableColumnDefinition from '../Data/IResponsiveTableColumnDefinition';
|
|
4
|
+
|
|
5
|
+
export class FilterPlugin<TData> implements IResponsiveTablePlugin<TData> {
|
|
6
|
+
public id = 'filter';
|
|
7
|
+
private filterText = '';
|
|
8
|
+
private api!: IPluginAPI<TData>;
|
|
9
|
+
|
|
10
|
+
constructor() {
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
public onPluginInit = (api: IPluginAPI<TData>) => {
|
|
14
|
+
this.api = api;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
public renderHeader = () => {
|
|
18
|
+
if (!this.api.filterProps?.showFilter) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
return (
|
|
22
|
+
<div style={{ float: 'right', marginBottom: '1rem' }}>
|
|
23
|
+
<input
|
|
24
|
+
type="text"
|
|
25
|
+
placeholder={this.api.filterProps.filterPlaceholder || "Search..."}
|
|
26
|
+
onChange={this.handleFilterChange}
|
|
27
|
+
style={{
|
|
28
|
+
padding: '0.5rem',
|
|
29
|
+
border: '1px solid #ccc',
|
|
30
|
+
borderRadius: '4px',
|
|
31
|
+
}}
|
|
32
|
+
/>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
public processData = (data: TData[]): TData[] => {
|
|
38
|
+
if (!this.filterText || !this.api.columnDefinitions) {
|
|
39
|
+
return data;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const lowercasedFilter = this.filterText.toLowerCase();
|
|
43
|
+
|
|
44
|
+
return data.filter((row) => {
|
|
45
|
+
return this.api.columnDefinitions!.some((colDef) => {
|
|
46
|
+
// If colDef is a function, it won't have getFilterableValue, so skip it.
|
|
47
|
+
if (typeof colDef === 'function') {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Now we know colDef is an object (IResponsiveTableColumnDefinition<TData>)
|
|
52
|
+
const typedColDef = colDef as IResponsiveTableColumnDefinition<TData>;
|
|
53
|
+
|
|
54
|
+
// Check if getFilterableValue exists and is a function
|
|
55
|
+
if (typedColDef.getFilterableValue && typeof typedColDef.getFilterableValue === 'function') {
|
|
56
|
+
const value = typedColDef.getFilterableValue(row);
|
|
57
|
+
return value?.toString().toLowerCase().includes(lowercasedFilter);
|
|
58
|
+
}
|
|
59
|
+
return false; // If getFilterableValue is not present or not a function
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
private handleFilterChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
65
|
+
// Debounce the filter change
|
|
66
|
+
setTimeout(() => {
|
|
67
|
+
this.filterText = e.target.value;
|
|
68
|
+
this.api.forceUpdate();
|
|
69
|
+
}, 300);
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { ColumnDefinition } from '../UI/ResponsiveTable';
|
|
3
|
+
|
|
4
|
+
export interface IResponsiveTablePlugin<TData> {
|
|
5
|
+
// A unique identifier for the plugin
|
|
6
|
+
id: string;
|
|
7
|
+
|
|
8
|
+
// Optional: Renders a UI component above the table
|
|
9
|
+
renderHeader?: () => ReactNode;
|
|
10
|
+
|
|
11
|
+
// Optional: Renders a UI component below the table
|
|
12
|
+
renderFooter?: () => ReactNode;
|
|
13
|
+
|
|
14
|
+
// Optional: Processes the data before it's rendered
|
|
15
|
+
processData?: (data: TData[]) => TData[];
|
|
16
|
+
|
|
17
|
+
// Optional: A callback that the table can use to provide the plugin with its own API
|
|
18
|
+
onPluginInit?: (api: IPluginAPI<TData>) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface IPluginAPI<TData> {
|
|
22
|
+
// Function to get the current data from the table
|
|
23
|
+
getData: () => TData[];
|
|
24
|
+
|
|
25
|
+
// Function to force the table to re-render
|
|
26
|
+
forceUpdate: () => void;
|
|
27
|
+
|
|
28
|
+
// Function to get the column definitions from the table
|
|
29
|
+
columnDefinitions: ColumnDefinition<TData>[];
|
|
30
|
+
|
|
31
|
+
// Function to get the scrollable element of the table
|
|
32
|
+
getScrollableElement?: () => HTMLElement | null;
|
|
33
|
+
|
|
34
|
+
// Optional: Infinite scroll props from the ResponsiveTable component
|
|
35
|
+
infiniteScrollProps?: {
|
|
36
|
+
enableInfiniteScroll?: boolean;
|
|
37
|
+
onLoadMore?: (currentData: TData[]) => Promise<TData[] | null>;
|
|
38
|
+
hasMore?: boolean;
|
|
39
|
+
loadingMoreComponent?: ReactNode;
|
|
40
|
+
noMoreDataComponent?: ReactNode;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Optional: Filter props from the ResponsiveTable component
|
|
44
|
+
filterProps?: {
|
|
45
|
+
showFilter?: boolean;
|
|
46
|
+
filterPlaceholder?: string;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { IResponsiveTablePlugin, IPluginAPI } from './IResponsiveTablePlugin';
|
|
3
|
+
|
|
4
|
+
export class InfiniteScrollPlugin<TData> implements IResponsiveTablePlugin<TData> {
|
|
5
|
+
public id = 'infinite-scroll';
|
|
6
|
+
private api!: IPluginAPI<TData>;
|
|
7
|
+
private isLoadingMore = false;
|
|
8
|
+
|
|
9
|
+
constructor() {
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
public onPluginInit = (api: IPluginAPI<TData>) => {
|
|
13
|
+
this.api = api;
|
|
14
|
+
this.attachScrollListener();
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
private attachScrollListener = () => {
|
|
18
|
+
const scrollableElement = this.api.getScrollableElement?.();
|
|
19
|
+
if (scrollableElement) {
|
|
20
|
+
scrollableElement.addEventListener('scroll', this.handleScroll);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
private handleScroll = async () => {
|
|
25
|
+
const scrollableElement = this.api.getScrollableElement?.();
|
|
26
|
+
if (!scrollableElement || !this.api.infiniteScrollProps?.enableInfiniteScroll) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const { scrollTop, scrollHeight, clientHeight } = scrollableElement;
|
|
31
|
+
|
|
32
|
+
const scrollThreshold = 200; // Load more data when 200px from the bottom
|
|
33
|
+
|
|
34
|
+
if (
|
|
35
|
+
scrollHeight - scrollTop - clientHeight < scrollThreshold &&
|
|
36
|
+
this.api.infiniteScrollProps.hasMore &&
|
|
37
|
+
!this.isLoadingMore
|
|
38
|
+
) {
|
|
39
|
+
this.isLoadingMore = true;
|
|
40
|
+
this.api.forceUpdate(); // Trigger re-render to show loading component
|
|
41
|
+
|
|
42
|
+
const newData = await this.api.infiniteScrollProps.onLoadMore?.(this.api.getData());
|
|
43
|
+
|
|
44
|
+
if (newData) {
|
|
45
|
+
// The main component will handle appending data via processData
|
|
46
|
+
} else {
|
|
47
|
+
// No more data, update hasMore in parent if necessary
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
this.isLoadingMore = false;
|
|
51
|
+
this.api.forceUpdate(); // Trigger re-render to hide loading component
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
public processData = (data: TData[]): TData[] => {
|
|
56
|
+
// This plugin doesn't modify the data directly, but rather triggers loading more.
|
|
57
|
+
// The main component's data prop should be updated by the consumer of the table.
|
|
58
|
+
return data;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
public renderFooter = () => {
|
|
62
|
+
if (!this.api.infiniteScrollProps) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (this.isLoadingMore) {
|
|
67
|
+
return this.api.infiniteScrollProps.loadingMoreComponent || <div>Loading more...</div>;
|
|
68
|
+
} else if (!this.api.infiniteScrollProps.hasMore) {
|
|
69
|
+
return this.api.infiniteScrollProps.noMoreDataComponent || <div>No more data.</div>;
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
:root {
|
|
3
3
|
--table-border-color: #e0e0e0;
|
|
4
4
|
--table-header-bg: #f8f9fa;
|
|
5
|
-
--table-row-hover-bg: #
|
|
6
|
-
--table-row-stripe-bg: #
|
|
5
|
+
--table-row-hover-bg: #e9ecef;
|
|
6
|
+
--table-row-stripe-bg: #f2f2f2;
|
|
7
7
|
--card-bg: #ffffff;
|
|
8
8
|
--card-border-color: #e0e0e0;
|
|
9
9
|
--card-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
|
@@ -226,3 +226,28 @@
|
|
|
226
226
|
font-weight: 500; /* Less aggressive than bold */
|
|
227
227
|
font-size: 1rem;
|
|
228
228
|
}
|
|
229
|
+
|
|
230
|
+
.row-exit {
|
|
231
|
+
opacity: 0;
|
|
232
|
+
transform: scaleY(0);
|
|
233
|
+
transition: transform 0.3s ease-out, opacity 0.3s ease-out;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.row-enter {
|
|
237
|
+
opacity: 0;
|
|
238
|
+
transform: translateY(20px);
|
|
239
|
+
transition: transform 0.5s ease-out, opacity 0.5s ease-out;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.row-flash {
|
|
243
|
+
animation: flash 0.5s ease-out;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
@keyframes flash {
|
|
247
|
+
0% {
|
|
248
|
+
background-color: var(--table-row-hover-bg);
|
|
249
|
+
}
|
|
250
|
+
100% {
|
|
251
|
+
background-color: transparent;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
@@ -1,35 +1,58 @@
|
|
|
1
|
-
import React, { CSSProperties, Component, ReactNode } from 'react';
|
|
1
|
+
import React, { CSSProperties, Component, ReactNode, createRef } from 'react';
|
|
2
2
|
import styles from '../Styles/ResponsiveTable.module.css';
|
|
3
3
|
import IResponsiveTableColumnDefinition from '../Data/IResponsiveTableColumnDefinition';
|
|
4
4
|
import IFooterRowDefinition from '../Data/IFooterRowDefinition';
|
|
5
|
+
import { IResponsiveTablePlugin } from '../Plugins/IResponsiveTablePlugin';
|
|
6
|
+
import { FilterPlugin } from '../Plugins/FilterPlugin';
|
|
7
|
+
import { InfiniteScrollPlugin } from '../Plugins/InfiniteScrollPlugin';
|
|
8
|
+
import { FixedSizeList as List } from 'react-window';
|
|
5
9
|
|
|
6
10
|
export type ColumnDefinition<TData> =
|
|
7
11
|
| IResponsiveTableColumnDefinition<TData>
|
|
8
12
|
| ((data: TData, rowIndex?: number) => IResponsiveTableColumnDefinition<TData>);
|
|
9
|
-
interface IProps<TData> {
|
|
10
|
-
columnDefinitions: ColumnDefinition<TData>[];
|
|
11
|
-
data: TData[];
|
|
12
|
-
noDataComponent?: ReactNode;
|
|
13
|
-
maxHeight?: string;
|
|
14
|
-
onRowClick?: (item: TData) => void;
|
|
15
|
-
footerRows?: IFooterRowDefinition[];
|
|
16
|
-
mobileBreakpoint?: number;
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
interface IProps<TData> {
|
|
14
|
+
columnDefinitions: ColumnDefinition<TData>[];
|
|
15
|
+
data: TData[];
|
|
16
|
+
noDataComponent?: ReactNode;
|
|
17
|
+
maxHeight?: string;
|
|
18
|
+
onRowClick?: (item: TData) => void;
|
|
19
|
+
footerRows?: IFooterRowDefinition[];
|
|
20
|
+
mobileBreakpoint?: number;
|
|
21
|
+
plugins?: IResponsiveTablePlugin<TData>[];
|
|
22
|
+
infiniteScrollProps?: {
|
|
23
|
+
enableInfiniteScroll?: boolean;
|
|
24
|
+
onLoadMore?: (currentData: TData[]) => Promise<TData[] | null>;
|
|
25
|
+
hasMore?: boolean;
|
|
26
|
+
loadingMoreComponent?: ReactNode;
|
|
27
|
+
noMoreDataComponent?: ReactNode;
|
|
28
|
+
};
|
|
29
|
+
filterProps?: {
|
|
30
|
+
showFilter?: boolean;
|
|
31
|
+
filterPlaceholder?: string;
|
|
32
|
+
};
|
|
33
|
+
animationProps?: {
|
|
34
|
+
isLoading?: boolean;
|
|
35
|
+
animateOnLoad?: boolean;
|
|
36
|
+
};
|
|
19
37
|
}
|
|
20
38
|
|
|
21
|
-
interface IState {
|
|
39
|
+
interface IState<TData> {
|
|
22
40
|
isMobile: boolean;
|
|
41
|
+
processedData: TData[];
|
|
42
|
+
isLoadingMore: boolean;
|
|
23
43
|
}
|
|
24
44
|
|
|
25
45
|
// Class component
|
|
26
|
-
class ResponsiveTable<TData> extends Component<IProps<TData>, IState
|
|
46
|
+
class ResponsiveTable<TData> extends Component<IProps<TData>, IState<TData>> {
|
|
27
47
|
private debouncedResize: () => void;
|
|
48
|
+
private tableContainerRef = createRef<HTMLDivElement>();
|
|
28
49
|
|
|
29
50
|
constructor(props: IProps<TData>) {
|
|
30
51
|
super(props);
|
|
31
52
|
this.state = {
|
|
32
53
|
isMobile: false,
|
|
54
|
+
processedData: props.data,
|
|
55
|
+
isLoadingMore: false,
|
|
33
56
|
};
|
|
34
57
|
|
|
35
58
|
this.debouncedResize = this.debounce(this.handleResize, 200);
|
|
@@ -48,8 +71,8 @@ class ResponsiveTable<TData> extends Component<IProps<TData>, IState> {
|
|
|
48
71
|
}
|
|
49
72
|
|
|
50
73
|
private get data(): TData[] {
|
|
51
|
-
if (Array.isArray(this.
|
|
52
|
-
return this.
|
|
74
|
+
if (Array.isArray(this.state.processedData) && this.state.processedData.length > 0) {
|
|
75
|
+
return this.state.processedData;
|
|
53
76
|
} else {
|
|
54
77
|
return [];
|
|
55
78
|
}
|
|
@@ -81,12 +104,81 @@ class ResponsiveTable<TData> extends Component<IProps<TData>, IState> {
|
|
|
81
104
|
componentDidMount(): void {
|
|
82
105
|
this.handleResize(); // Initial check
|
|
83
106
|
window.addEventListener('resize', this.debouncedResize);
|
|
107
|
+
this.initializePlugins();
|
|
84
108
|
}
|
|
85
109
|
|
|
86
110
|
componentWillUnmount(): void {
|
|
87
111
|
window.removeEventListener('resize', this.debouncedResize);
|
|
88
112
|
}
|
|
89
113
|
|
|
114
|
+
componentDidUpdate(prevProps: IProps<TData>) {
|
|
115
|
+
if (prevProps.data !== this.props.data) {
|
|
116
|
+
this.processData();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Handle infinite scroll loading
|
|
120
|
+
if (this.props.infiniteScrollProps?.enableInfiniteScroll && this.props.infiniteScrollProps?.hasMore && !this.state.isLoadingMore && this.props.infiniteScrollProps?.onLoadMore) {
|
|
121
|
+
// This condition will be met when the parent component updates `data` and `hasMore`
|
|
122
|
+
// after a successful load, or when `hasMore` becomes true.
|
|
123
|
+
// The actual load trigger is within the InfiniteScrollPlugin via `onItemsRendered`
|
|
124
|
+
// or `handleScroll`.
|
|
125
|
+
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private initializePlugins() {
|
|
130
|
+
const activePlugins: IResponsiveTablePlugin<TData>[] = [];
|
|
131
|
+
|
|
132
|
+
// Add explicitly provided plugins first
|
|
133
|
+
if (this.props.plugins) {
|
|
134
|
+
activePlugins.push(...this.props.plugins);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Automatically add FilterPlugin if filterProps are provided and not already present
|
|
138
|
+
if (this.props.filterProps?.showFilter && !activePlugins.some(p => p.id === 'filter')) {
|
|
139
|
+
activePlugins.push(new FilterPlugin());
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Automatically add InfiniteScrollPlugin if infiniteScrollProps are provided and not already present
|
|
143
|
+
if (this.props.infiniteScrollProps?.enableInfiniteScroll && !activePlugins.some(p => p.id === 'infinite-scroll')) {
|
|
144
|
+
activePlugins.push(new InfiniteScrollPlugin());
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
activePlugins.forEach((plugin) => {
|
|
148
|
+
if (plugin.onPluginInit) {
|
|
149
|
+
plugin.onPluginInit({
|
|
150
|
+
getData: () => this.props.data,
|
|
151
|
+
forceUpdate: () => this.processData(),
|
|
152
|
+
getScrollableElement: () => this.tableContainerRef.current,
|
|
153
|
+
infiniteScrollProps: this.props.infiniteScrollProps,
|
|
154
|
+
filterProps: this.props.filterProps,
|
|
155
|
+
columnDefinitions: this.props.columnDefinitions,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Process data with all active plugins
|
|
161
|
+
let processedData = [...this.props.data];
|
|
162
|
+
activePlugins.forEach((plugin) => {
|
|
163
|
+
if (plugin.processData) {
|
|
164
|
+
processedData = plugin.processData(processedData);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
this.setState({ processedData });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private processData() {
|
|
171
|
+
let processedData = [...this.props.data];
|
|
172
|
+
if (this.props.plugins) {
|
|
173
|
+
this.props.plugins.forEach((plugin) => {
|
|
174
|
+
if (plugin.processData) {
|
|
175
|
+
processedData = plugin.processData(processedData);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
this.setState({ processedData });
|
|
180
|
+
}
|
|
181
|
+
|
|
90
182
|
handleResize = (): void => {
|
|
91
183
|
this.setState({
|
|
92
184
|
isMobile: window.innerWidth <= this.mobileBreakpoint,
|
|
@@ -266,7 +358,7 @@ class ResponsiveTable<TData> extends Component<IProps<TData>, IState> {
|
|
|
266
358
|
{this.data.map((row, rowIndex) => (
|
|
267
359
|
<div
|
|
268
360
|
key={rowIndex}
|
|
269
|
-
|
|
361
|
+
className={`${styles['card']} ${this.props.animationProps?.animateOnLoad ? styles.animatedRow : ''}`}
|
|
270
362
|
style={{ animationDelay: `${rowIndex * 0.05}s` }}
|
|
271
363
|
onClick={(e) => {
|
|
272
364
|
this.rowClickFunction(row);
|
|
@@ -311,8 +403,29 @@ class ResponsiveTable<TData> extends Component<IProps<TData>, IState> {
|
|
|
311
403
|
? ({ maxHeight: this.props.maxHeight, overflowY: 'auto' } as CSSProperties)
|
|
312
404
|
: {};
|
|
313
405
|
|
|
406
|
+
const Row = ({ index, style }: { index: number; style: CSSProperties }) => {
|
|
407
|
+
const row = this.data[index];
|
|
408
|
+
if (!row) return null; // Should not happen with correct item count
|
|
409
|
+
|
|
410
|
+
return (
|
|
411
|
+
<tr
|
|
412
|
+
key={index}
|
|
413
|
+
className={this.props.animationProps?.animateOnLoad ? styles.animatedRow : ''}
|
|
414
|
+
style={{ ...style, animationDelay: `${index * 0.05}s` }}
|
|
415
|
+
>
|
|
416
|
+
{this.props.columnDefinitions.map((columnDefinition, colIndex) => (
|
|
417
|
+
<td onClick={() => this.rowClickFunction(row)} key={colIndex}>
|
|
418
|
+
<span style={{ ...this.rowClickStyle }}>
|
|
419
|
+
{this.getColumnDefinition(columnDefinition, index).cellRenderer(row)}
|
|
420
|
+
</span>
|
|
421
|
+
</td>
|
|
422
|
+
))}
|
|
423
|
+
</tr>
|
|
424
|
+
);
|
|
425
|
+
};
|
|
426
|
+
|
|
314
427
|
return (
|
|
315
|
-
<div style={fixedHeadersStyle}>
|
|
428
|
+
<div style={fixedHeadersStyle} ref={this.tableContainerRef}>
|
|
316
429
|
<table className={styles['responsiveTable']} style={{ zIndex: -1 }}>
|
|
317
430
|
<thead>
|
|
318
431
|
<tr>
|
|
@@ -340,42 +453,81 @@ class ResponsiveTable<TData> extends Component<IProps<TData>, IState> {
|
|
|
340
453
|
</tr>
|
|
341
454
|
</thead>
|
|
342
455
|
<tbody>
|
|
343
|
-
{this.
|
|
344
|
-
<
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
456
|
+
{this.props.infiniteScrollProps?.enableInfiniteScroll ? (
|
|
457
|
+
<List
|
|
458
|
+
height={fixedHeadersStyle.maxHeight ? (typeof fixedHeadersStyle.maxHeight === 'string' ? parseFloat(fixedHeadersStyle.maxHeight) : fixedHeadersStyle.maxHeight) : 500} // Default height if not provided
|
|
459
|
+
itemCount={this.data.length}
|
|
460
|
+
itemSize={50} // Average row height, can be made configurable
|
|
461
|
+
width={'100%'}
|
|
462
|
+
outerRef={this.tableContainerRef} // Pass ref to outer element for scroll events
|
|
348
463
|
>
|
|
349
|
-
{
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
464
|
+
{Row}
|
|
465
|
+
</List>
|
|
466
|
+
) : (
|
|
467
|
+
this.data.map((row, rowIndex) => (
|
|
468
|
+
<tr
|
|
469
|
+
key={rowIndex}
|
|
470
|
+
className={this.props.animationProps?.animateOnLoad ? styles.animatedRow : ''}
|
|
471
|
+
style={{ animationDelay: `${rowIndex * 0.05}s` }}
|
|
472
|
+
>
|
|
473
|
+
{this.props.columnDefinitions.map((columnDefinition, colIndex) => (
|
|
474
|
+
<td onClick={() => this.rowClickFunction(row)} key={colIndex}>
|
|
475
|
+
<span style={{ ...this.rowClickStyle }}>
|
|
476
|
+
{this.getColumnDefinition(columnDefinition, rowIndex).cellRenderer(row)}
|
|
477
|
+
</span>
|
|
478
|
+
</td>
|
|
479
|
+
))}
|
|
480
|
+
</tr>
|
|
481
|
+
))
|
|
482
|
+
)}
|
|
358
483
|
</tbody>
|
|
359
484
|
{this.tableFooter}
|
|
360
485
|
</table>
|
|
486
|
+
{this.renderPluginFooters()}
|
|
361
487
|
</div>
|
|
362
488
|
);
|
|
363
489
|
}
|
|
364
490
|
|
|
365
|
-
|
|
366
|
-
if (this.props.
|
|
367
|
-
return
|
|
491
|
+
private renderPluginHeaders() {
|
|
492
|
+
if (!this.props.plugins) {
|
|
493
|
+
return null;
|
|
368
494
|
}
|
|
369
495
|
|
|
370
|
-
|
|
371
|
-
|
|
496
|
+
return this.props.plugins.map((plugin) => {
|
|
497
|
+
if (plugin.renderHeader) {
|
|
498
|
+
return <div key={plugin.id}>{plugin.renderHeader()}</div>;
|
|
499
|
+
}
|
|
500
|
+
return null;
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
private renderPluginFooters() {
|
|
505
|
+
if (!this.props.plugins) {
|
|
506
|
+
return null;
|
|
372
507
|
}
|
|
373
|
-
|
|
374
|
-
|
|
508
|
+
|
|
509
|
+
return this.props.plugins.map((plugin) => {
|
|
510
|
+
if (plugin.renderFooter) {
|
|
511
|
+
return <div key={plugin.id + '-footer'}>{plugin.renderFooter()}</div>;
|
|
512
|
+
}
|
|
513
|
+
return null;
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
render() {
|
|
518
|
+
if (this.props.animationProps?.isLoading) {
|
|
519
|
+
return this.skeletonView;
|
|
375
520
|
}
|
|
376
521
|
|
|
377
|
-
return
|
|
522
|
+
return (
|
|
523
|
+
<div>
|
|
524
|
+
{this.renderPluginHeaders()}
|
|
525
|
+
{!this.hasData && this.noDataComponent}
|
|
526
|
+
{this.hasData && this.state.isMobile && this.mobileView}
|
|
527
|
+
{this.hasData && !this.state.isMobile && this.largeScreenView}
|
|
528
|
+
</div>
|
|
529
|
+
);
|
|
378
530
|
}
|
|
379
531
|
}
|
|
380
532
|
|
|
381
|
-
export default ResponsiveTable;
|
|
533
|
+
export default ResponsiveTable;
|
package/src/index.tsx
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import IFooterColumnDefinition from './Data/IFooterColumnDefinition';
|
|
2
|
-
import IFooterRowDefinition from './Data/IFooterRowDefinition';
|
|
3
|
-
import IResponsiveTableColumnDefinition from './Data/IResponsiveTableColumnDefinition';
|
|
4
|
-
import ResponsiveTable, { ColumnDefinition } from './UI/ResponsiveTable';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
import IFooterColumnDefinition from './Data/IFooterColumnDefinition';
|
|
2
|
+
import IFooterRowDefinition from './Data/IFooterRowDefinition';
|
|
3
|
+
import IResponsiveTableColumnDefinition from './Data/IResponsiveTableColumnDefinition';
|
|
4
|
+
import ResponsiveTable, { ColumnDefinition } from './UI/ResponsiveTable';
|
|
5
|
+
import { FilterPlugin } from './Plugins/FilterPlugin';
|
|
6
|
+
import { InfiniteScrollPlugin } from './Plugins/InfiniteScrollPlugin';
|
|
7
|
+
import { IResponsiveTablePlugin } from './Plugins/IResponsiveTablePlugin';
|
|
8
|
+
|
|
9
|
+
export { IResponsiveTableColumnDefinition, ColumnDefinition, IFooterColumnDefinition, IFooterRowDefinition, FilterPlugin, InfiniteScrollPlugin, IResponsiveTablePlugin };
|
|
10
|
+
export default ResponsiveTable;
|