jattac.libs.web.responsive-table 0.2.2 → 0.2.3

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.
@@ -1,532 +1,552 @@
1
- import React, { CSSProperties, Component, ReactNode, createRef } from 'react';
2
- import styles from '../Styles/ResponsiveTable.module.css';
3
- import IResponsiveTableColumnDefinition from '../Data/IResponsiveTableColumnDefinition';
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';
9
-
10
- export type ColumnDefinition<TData> =
11
- | IResponsiveTableColumnDefinition<TData>
12
- | ((data: TData, rowIndex?: number) => IResponsiveTableColumnDefinition<TData>);
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
- };
37
- }
38
-
39
- interface IState<TData> {
40
- isMobile: boolean;
41
- processedData: TData[];
42
- isLoadingMore: boolean;
43
- }
44
-
45
- // Class component
46
- class ResponsiveTable<TData> extends Component<IProps<TData>, IState<TData>> {
47
- private debouncedResize: () => void;
48
- private tableContainerRef = createRef<HTMLDivElement>();
49
-
50
- constructor(props: IProps<TData>) {
51
- super(props);
52
- this.state = {
53
- isMobile: false,
54
- processedData: props.data,
55
- isLoadingMore: false,
56
- };
57
-
58
- this.debouncedResize = this.debounce(this.handleResize, 200);
59
- }
60
-
61
- private get mobileBreakpoint(): number {
62
- return this.props.mobileBreakpoint || 600;
63
- }
64
-
65
- private debounce(func: () => void, delay: number): () => void {
66
- let timeout: NodeJS.Timeout;
67
- return () => {
68
- clearTimeout(timeout);
69
- timeout = setTimeout(() => func(), delay);
70
- };
71
- }
72
-
73
- private get data(): TData[] {
74
- if (Array.isArray(this.state.processedData) && this.state.processedData.length > 0) {
75
- return this.state.processedData;
76
- } else {
77
- return [];
78
- }
79
- }
80
-
81
- private get noDataSvg(): ReactNode {
82
- return (
83
- <svg xmlns="http://www.w3.org/2000/svg" fill="#ccc" height="40" width="40" viewBox="0 0 24 24">
84
- <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-14h2v6h-2zm0 8h2v2h-2z" />
85
- </svg>
86
- );
87
- }
88
-
89
- private get hasData(): boolean {
90
- return this.data.length > 0;
91
- }
92
-
93
- private get noDataComponent(): ReactNode {
94
- return (
95
- this.props.noDataComponent || (
96
- <div className={styles.noDataWrapper}>
97
- {this.noDataSvg}
98
- <div className={styles.noData}>No data</div>
99
- </div>
100
- )
101
- );
102
- }
103
-
104
- componentDidMount(): void {
105
- this.handleResize(); // Initial check
106
- window.addEventListener('resize', this.debouncedResize);
107
- this.initializePlugins();
108
- }
109
-
110
- componentWillUnmount(): void {
111
- window.removeEventListener('resize', this.debouncedResize);
112
- }
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
-
182
- handleResize = (): void => {
183
- this.setState({
184
- isMobile: window.innerWidth <= this.mobileBreakpoint,
185
- });
186
- };
187
-
188
- private getColumnDefinition(
189
- columnDefinition: ColumnDefinition<TData>,
190
- rowIndex: number,
191
- ): IResponsiveTableColumnDefinition<TData> {
192
- if (!this.hasData) {
193
- return { displayLabel: '', cellRenderer: () => '' };
194
- }
195
- return columnDefinition instanceof Function ? columnDefinition(this.data[0], rowIndex) : columnDefinition;
196
- }
197
-
198
- private getRawColumnDefinition(columnDefinition: ColumnDefinition<TData>): IResponsiveTableColumnDefinition<TData> {
199
- let rawColumnDefinition: IResponsiveTableColumnDefinition<TData> = {} as IResponsiveTableColumnDefinition<TData>;
200
- if (columnDefinition instanceof Function) {
201
- rawColumnDefinition = columnDefinition(this.data[0], 0);
202
- } else {
203
- rawColumnDefinition = columnDefinition as IResponsiveTableColumnDefinition<TData>;
204
- }
205
- return rawColumnDefinition;
206
- }
207
-
208
- private onHeaderClickCallback(columnDefinition: ColumnDefinition<TData>): ((id: string) => void) | undefined {
209
- const rawColumnDefinition = this.getRawColumnDefinition(columnDefinition);
210
- if (rawColumnDefinition.interactivity && rawColumnDefinition.interactivity.onHeaderClick) {
211
- return rawColumnDefinition.interactivity.onHeaderClick;
212
- } else {
213
- return undefined;
214
- }
215
- }
216
-
217
- private getClickableHeaderClassName(
218
- onHeaderClickCallback: ((id: string) => void) | undefined,
219
- colDef: ColumnDefinition<TData>,
220
- ): string {
221
- const rawColumnDefinition = this.getRawColumnDefinition(colDef);
222
- const clickableHeaderClassName = onHeaderClickCallback
223
- ? rawColumnDefinition.interactivity!.className || styles.clickableHeader
224
- : '';
225
- return clickableHeaderClassName;
226
- }
227
-
228
- private get rowClickFunction(): (item: TData) => void {
229
- if (this.props.onRowClick) {
230
- return this.props.onRowClick;
231
- } else {
232
- return () => {};
233
- }
234
- }
235
-
236
- private get rowClickStyle(): CSSProperties {
237
- if (this.props.onRowClick) {
238
- return { cursor: 'pointer' } as CSSProperties;
239
- } else {
240
- return {};
241
- }
242
- }
243
-
244
- private get tableFooter(): ReactNode {
245
- if (!this.props.footerRows || this.props.footerRows.length === 0) {
246
- return null;
247
- }
248
-
249
- return (
250
- <tfoot>
251
- {this.props.footerRows.map((row, rowIndex) => (
252
- <tr key={rowIndex}>
253
- {row.columns.map((col, colIndex) => (
254
- <td
255
- key={colIndex}
256
- colSpan={col.colSpan}
257
- className={`${styles.footerCell} ${col.className || ''} ${col.onCellClick ? styles.clickableFooterCell : ''}`}
258
- onClick={col.onCellClick}
259
- >
260
- {col.cellRenderer()}
261
- </td>
262
- ))}
263
- </tr>
264
- ))}
265
- </tfoot>
266
- );
267
- }
268
-
269
- private get mobileFooter(): ReactNode {
270
- if (!this.props.footerRows || this.props.footerRows.length === 0) {
271
- return null;
272
- }
273
-
274
- return (
275
- <div className={styles.footerCard}>
276
- <div className={styles['footer-card-body']}>
277
- {this.props.footerRows.map((row, rowIndex) => {
278
- let currentColumnIndex = 0;
279
- return (
280
- <div key={rowIndex}>
281
- {row.columns.map((col, colIndex) => {
282
- let label = col.displayLabel;
283
- if (!label && col.colSpan === 1) {
284
- const header = this.props.columnDefinitions[currentColumnIndex];
285
- if (header) {
286
- label = this.getRawColumnDefinition(header).displayLabel;
287
- }
288
- }
289
- currentColumnIndex += col.colSpan;
290
- return (
291
- <p
292
- key={colIndex}
293
- className={`${styles['footer-card-row']} ${col.className || ''} ${
294
- col.onCellClick ? styles.clickableFooterCell : ''
295
- }`}
296
- onClick={col.onCellClick}
297
- >
298
- {label && <span className={styles['card-label']}>{label}</span>}
299
- <span className={styles['card-value']}>{col.cellRenderer()}</span>
300
- </p>
301
- );
302
- })}
303
- </div>
304
- );
305
- })}
306
- </div>
307
- </div>
308
- );
309
- }
310
-
311
- private get skeletonView(): ReactNode {
312
- const skeletonRowCount = 5; // Or make this configurable
313
- const columnCount = this.props.columnDefinitions.length;
314
-
315
- if (this.state.isMobile) {
316
- return (
317
- <div>
318
- {[...Array(skeletonRowCount)].map((_, i) => (
319
- <div key={i} className={styles.skeletonCard}>
320
- {[...Array(columnCount)].map((_, j) => (
321
- <div key={j} className={`${styles.skeleton} ${styles.skeletonText}`} style={{ marginBottom: '0.5rem' }} />
322
- ))}
323
- </div>
324
- ))}
325
- </div>
326
- );
327
- }
328
-
329
- return (
330
- <table className={styles.responsiveTable}>
331
- <thead>
332
- <tr>
333
- {[...Array(columnCount)].map((_, i) => (
334
- <th key={i}>
335
- <div className={`${styles.skeleton} ${styles.skeletonText}`} />
336
- </th>
337
- ))}
338
- </tr>
339
- </thead>
340
- <tbody>
341
- {[...Array(skeletonRowCount)].map((_, i) => (
342
- <tr key={i}>
343
- {[...Array(columnCount)].map((_, j) => (
344
- <td key={j}>
345
- <div className={`${styles.skeleton} ${styles.skeletonText}`} />
346
- </td>
347
- ))}
348
- </tr>
349
- ))}
350
- </tbody>
351
- </table>
352
- );
353
- }
354
-
355
- private get mobileView(): ReactNode {
356
- return (
357
- <div>
358
- {this.data.map((row, rowIndex) => (
359
- <div
360
- key={rowIndex}
361
- className={`${styles['card']} ${this.props.animationProps?.animateOnLoad ? styles.animatedRow : ''}`}
362
- style={{ animationDelay: `${rowIndex * 0.05}s` }}
363
- onClick={(e) => {
364
- this.rowClickFunction(row);
365
- e.stopPropagation();
366
- e.preventDefault();
367
- }}
368
- >
369
- <div className={styles['card-header']}> </div>
370
- <div className={styles['card-body']}>
371
- {this.props.columnDefinitions.map((columnDefinition, colIndex) => {
372
- const colDef = this.getColumnDefinition(columnDefinition, rowIndex);
373
- const onHeaderClickCallback = this.onHeaderClickCallback(colDef);
374
- const clickableHeaderClassName = this.getClickableHeaderClassName(onHeaderClickCallback, colDef);
375
- return (
376
- <div key={colIndex} className={styles['card-row']}>
377
- <p>
378
- <span
379
- className={`${styles['card-label']} ${clickableHeaderClassName}`}
380
- onClick={
381
- onHeaderClickCallback ? () => onHeaderClickCallback(colDef.interactivity!.id) : undefined
382
- }
383
- >
384
- {colDef.displayLabel}
385
- </span>
386
- <span className={styles['card-value']}>{colDef.cellRenderer(row)}</span>
387
- </p>
388
- </div>
389
- );
390
- })}
391
- </div>
392
- </div>
393
- ))}
394
- {this.mobileFooter}
395
- </div>
396
- );
397
- }
398
-
399
- private get largeScreenView(): ReactNode {
400
- const useFixedHeaders = this.props.maxHeight ? true : false;
401
-
402
- const fixedHeadersStyle = useFixedHeaders
403
- ? ({ maxHeight: this.props.maxHeight, overflowY: 'auto' } as CSSProperties)
404
- : {};
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
-
427
- return (
428
- <div style={fixedHeadersStyle} ref={this.tableContainerRef}>
429
- <table className={styles['responsiveTable']}>
430
- <thead>
431
- <tr>
432
- {this.props.columnDefinitions.map((columnDefinition, colIndex) => {
433
- const onHeaderClickCallback = this.onHeaderClickCallback(columnDefinition);
434
- const clickableHeaderClassName = this.getClickableHeaderClassName(
435
- onHeaderClickCallback,
436
- columnDefinition,
437
- );
438
- return (
439
- <th
440
- key={colIndex}
441
- className={`${clickableHeaderClassName}`}
442
- onClick={
443
- onHeaderClickCallback
444
- ? () => onHeaderClickCallback(this.getColumnDefinition(columnDefinition, 0).interactivity!.id)
445
- : undefined
446
- }
447
- >
448
- {this.getColumnDefinition(columnDefinition, 0).displayLabel}
449
- </th>
450
- );
451
- })}
452
- </tr>
453
- </thead>
454
- <tbody>
455
- {this.props.infiniteScrollProps?.enableInfiniteScroll ? (
456
- <List
457
- height={fixedHeadersStyle.maxHeight ? (typeof fixedHeadersStyle.maxHeight === 'string' ? parseFloat(fixedHeadersStyle.maxHeight) : fixedHeadersStyle.maxHeight) : 500} // Default height if not provided
458
- itemCount={this.data.length}
459
- itemSize={50} // Average row height, can be made configurable
460
- width={'100%'}
461
- outerRef={this.tableContainerRef} // Pass ref to outer element for scroll events
462
- >
463
- {Row}
464
- </List>
465
- ) : (
466
- this.data.map((row, rowIndex) => (
467
- <tr
468
- key={rowIndex}
469
- className={this.props.animationProps?.animateOnLoad ? styles.animatedRow : ''}
470
- style={{ animationDelay: `${rowIndex * 0.05}s` }}
471
- >
472
- {this.props.columnDefinitions.map((columnDefinition, colIndex) => (
473
- <td onClick={() => this.rowClickFunction(row)} key={colIndex}>
474
- <span style={{ ...this.rowClickStyle }}>
475
- {this.getColumnDefinition(columnDefinition, rowIndex).cellRenderer(row)}
476
- </span>
477
- </td>
478
- ))}
479
- </tr>
480
- ))
481
- )}
482
- </tbody>
483
- {this.tableFooter}
484
- </table>
485
- {this.renderPluginFooters()}
486
- </div>
487
- );
488
- }
489
-
490
- private renderPluginHeaders() {
491
- if (!this.props.plugins) {
492
- return null;
493
- }
494
-
495
- return this.props.plugins.map((plugin) => {
496
- if (plugin.renderHeader) {
497
- return <div key={plugin.id}>{plugin.renderHeader()}</div>;
498
- }
499
- return null;
500
- });
501
- }
502
-
503
- private renderPluginFooters() {
504
- if (!this.props.plugins) {
505
- return null;
506
- }
507
-
508
- return this.props.plugins.map((plugin) => {
509
- if (plugin.renderFooter) {
510
- return <div key={plugin.id + '-footer'}>{plugin.renderFooter()}</div>;
511
- }
512
- return null;
513
- });
514
- }
515
-
516
- render() {
517
- if (this.props.animationProps?.isLoading) {
518
- return this.skeletonView;
519
- }
520
-
521
- return (
522
- <div>
523
- {this.renderPluginHeaders()}
524
- {!this.hasData && this.noDataComponent}
525
- {this.hasData && this.state.isMobile && this.mobileView}
526
- {this.hasData && !this.state.isMobile && this.largeScreenView}
527
- </div>
528
- );
529
- }
530
- }
531
-
532
- export default ResponsiveTable;
1
+ import React, { CSSProperties, Component, ReactNode, createRef } from 'react';
2
+ import styles from '../Styles/ResponsiveTable.module.css';
3
+ import IResponsiveTableColumnDefinition from '../Data/IResponsiveTableColumnDefinition';
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';
9
+
10
+ export type ColumnDefinition<TData> =
11
+ | IResponsiveTableColumnDefinition<TData>
12
+ | ((data: TData, rowIndex?: number) => IResponsiveTableColumnDefinition<TData>);
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
+ enablePageLevelStickyHeader?: boolean;
23
+ infiniteScrollProps?: {
24
+ enableInfiniteScroll?: boolean;
25
+ onLoadMore?: (currentData: TData[]) => Promise<TData[] | null>;
26
+ hasMore?: boolean;
27
+ loadingMoreComponent?: ReactNode;
28
+ noMoreDataComponent?: ReactNode;
29
+ };
30
+ filterProps?: {
31
+ showFilter?: boolean;
32
+ filterPlaceholder?: string;
33
+ };
34
+ animationProps?: {
35
+ isLoading?: boolean;
36
+ animateOnLoad?: boolean;
37
+ };
38
+ }
39
+
40
+ interface IState<TData> {
41
+ isMobile: boolean;
42
+ processedData: TData[];
43
+ isLoadingMore: boolean;
44
+ isHeaderSticky: boolean;
45
+ }
46
+
47
+ // Class component
48
+ class ResponsiveTable<TData> extends Component<IProps<TData>, IState<TData>> {
49
+ private debouncedResize: () => void;
50
+ private tableContainerRef = createRef<HTMLDivElement>();
51
+ private headerRef = createRef<HTMLTableSectionElement>();
52
+
53
+ constructor(props: IProps<TData>) {
54
+ super(props);
55
+ this.state = {
56
+ isMobile: false,
57
+ processedData: props.data,
58
+ isLoadingMore: false,
59
+ isHeaderSticky: false,
60
+ };
61
+
62
+ this.debouncedResize = this.debounce(this.handleResize, 200);
63
+ }
64
+
65
+ private get mobileBreakpoint(): number {
66
+ return this.props.mobileBreakpoint || 600;
67
+ }
68
+
69
+ private debounce(func: () => void, delay: number): () => void {
70
+ let timeout: NodeJS.Timeout;
71
+ return () => {
72
+ clearTimeout(timeout);
73
+ timeout = setTimeout(() => func(), delay);
74
+ };
75
+ }
76
+
77
+ private get data(): TData[] {
78
+ if (Array.isArray(this.state.processedData) && this.state.processedData.length > 0) {
79
+ return this.state.processedData;
80
+ } else {
81
+ return [];
82
+ }
83
+ }
84
+
85
+ private get noDataSvg(): ReactNode {
86
+ return (
87
+ <svg xmlns="http://www.w3.org/2000/svg" fill="#ccc" height="40" width="40" viewBox="0 0 24 24">
88
+ <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-14h2v6h-2zm0 8h2v2h-2z" />
89
+ </svg>
90
+ );
91
+ }
92
+
93
+ private get hasData(): boolean {
94
+ return this.data.length > 0;
95
+ }
96
+
97
+ private get noDataComponent(): ReactNode {
98
+ return (
99
+ this.props.noDataComponent || (
100
+ <div className={styles.noDataWrapper}>
101
+ {this.noDataSvg}
102
+ <div className={styles.noData}>No data</div>
103
+ </div>
104
+ )
105
+ );
106
+ }
107
+
108
+ componentDidMount(): void {
109
+ this.handleResize(); // Initial check
110
+ window.addEventListener('resize', this.debouncedResize);
111
+ if (this.props.enablePageLevelStickyHeader !== false) {
112
+ window.addEventListener('scroll', this.handleScroll);
113
+ }
114
+ this.initializePlugins();
115
+ }
116
+
117
+ componentWillUnmount(): void {
118
+ window.removeEventListener('resize', this.debouncedResize);
119
+ if (this.props.enablePageLevelStickyHeader !== false) {
120
+ window.removeEventListener('scroll', this.handleScroll);
121
+ }
122
+ }
123
+
124
+ componentDidUpdate(prevProps: IProps<TData>) {
125
+ if (prevProps.data !== this.props.data) {
126
+ this.processData();
127
+ }
128
+
129
+ // Handle infinite scroll loading
130
+ if (this.props.infiniteScrollProps?.enableInfiniteScroll && this.props.infiniteScrollProps?.hasMore && !this.state.isLoadingMore && this.props.infiniteScrollProps?.onLoadMore) {
131
+ // This condition will be met when the parent component updates `data` and `hasMore`
132
+ // after a successful load, or when `hasMore` becomes true.
133
+ // The actual load trigger is within the InfiniteScrollPlugin via `onItemsRendered`
134
+ // or `handleScroll`.
135
+
136
+ }
137
+ }
138
+
139
+ private handleScroll = (): void => {
140
+ if (this.headerRef.current && !this.props.maxHeight) {
141
+ const { top } = this.headerRef.current.getBoundingClientRect();
142
+ const isSticky = top <= 0;
143
+ if (isSticky !== this.state.isHeaderSticky) {
144
+ this.setState({ isHeaderSticky: isSticky });
145
+ }
146
+ }
147
+ };
148
+
149
+ private initializePlugins() {
150
+ const activePlugins: IResponsiveTablePlugin<TData>[] = [];
151
+
152
+ // Add explicitly provided plugins first
153
+ if (this.props.plugins) {
154
+ activePlugins.push(...this.props.plugins);
155
+ }
156
+
157
+ // Automatically add FilterPlugin if filterProps are provided and not already present
158
+ if (this.props.filterProps?.showFilter && !activePlugins.some(p => p.id === 'filter')) {
159
+ activePlugins.push(new FilterPlugin());
160
+ }
161
+
162
+ // Automatically add InfiniteScrollPlugin if infiniteScrollProps are provided and not already present
163
+ if (this.props.infiniteScrollProps?.enableInfiniteScroll && !activePlugins.some(p => p.id === 'infinite-scroll')) {
164
+ activePlugins.push(new InfiniteScrollPlugin());
165
+ }
166
+
167
+ activePlugins.forEach((plugin) => {
168
+ if (plugin.onPluginInit) {
169
+ plugin.onPluginInit({
170
+ getData: () => this.props.data,
171
+ forceUpdate: () => this.processData(),
172
+ getScrollableElement: () => this.tableContainerRef.current,
173
+ infiniteScrollProps: this.props.infiniteScrollProps,
174
+ filterProps: this.props.filterProps,
175
+ columnDefinitions: this.props.columnDefinitions,
176
+ });
177
+ }
178
+ });
179
+
180
+ // Process data with all active plugins
181
+ let processedData = [...this.props.data];
182
+ activePlugins.forEach((plugin) => {
183
+ if (plugin.processData) {
184
+ processedData = plugin.processData(processedData);
185
+ }
186
+ });
187
+ this.setState({ processedData });
188
+ }
189
+
190
+ private processData() {
191
+ let processedData = [...this.props.data];
192
+ if (this.props.plugins) {
193
+ this.props.plugins.forEach((plugin) => {
194
+ if (plugin.processData) {
195
+ processedData = plugin.processData(processedData);
196
+ }
197
+ });
198
+ }
199
+ this.setState({ processedData });
200
+ }
201
+
202
+ handleResize = (): void => {
203
+ this.setState({
204
+ isMobile: window.innerWidth <= this.mobileBreakpoint,
205
+ });
206
+ };
207
+
208
+ private getColumnDefinition(
209
+ columnDefinition: ColumnDefinition<TData>,
210
+ rowIndex: number,
211
+ ): IResponsiveTableColumnDefinition<TData> {
212
+ if (!this.hasData) {
213
+ return { displayLabel: '', cellRenderer: () => '' };
214
+ }
215
+ return columnDefinition instanceof Function ? columnDefinition(this.data[0], rowIndex) : columnDefinition;
216
+ }
217
+
218
+ private getRawColumnDefinition(columnDefinition: ColumnDefinition<TData>): IResponsiveTableColumnDefinition<TData> {
219
+ let rawColumnDefinition: IResponsiveTableColumnDefinition<TData> = {} as IResponsiveTableColumnDefinition<TData>;
220
+ if (columnDefinition instanceof Function) {
221
+ rawColumnDefinition = columnDefinition(this.data[0], 0);
222
+ } else {
223
+ rawColumnDefinition = columnDefinition as IResponsiveTableColumnDefinition<TData>;
224
+ }
225
+ return rawColumnDefinition;
226
+ }
227
+
228
+ private onHeaderClickCallback(columnDefinition: ColumnDefinition<TData>): ((id: string) => void) | undefined {
229
+ const rawColumnDefinition = this.getRawColumnDefinition(columnDefinition);
230
+ if (rawColumnDefinition.interactivity && rawColumnDefinition.interactivity.onHeaderClick) {
231
+ return rawColumnDefinition.interactivity.onHeaderClick;
232
+ } else {
233
+ return undefined;
234
+ }
235
+ }
236
+
237
+ private getClickableHeaderClassName(
238
+ onHeaderClickCallback: ((id: string) => void) | undefined,
239
+ colDef: ColumnDefinition<TData>,
240
+ ): string {
241
+ const rawColumnDefinition = this.getRawColumnDefinition(colDef);
242
+ const clickableHeaderClassName = onHeaderClickCallback
243
+ ? rawColumnDefinition.interactivity!.className || styles.clickableHeader
244
+ : '';
245
+ return clickableHeaderClassName;
246
+ }
247
+
248
+ private get rowClickFunction(): (item: TData) => void {
249
+ if (this.props.onRowClick) {
250
+ return this.props.onRowClick;
251
+ } else {
252
+ return () => {};
253
+ }
254
+ }
255
+
256
+ private get rowClickStyle(): CSSProperties {
257
+ if (this.props.onRowClick) {
258
+ return { cursor: 'pointer' } as CSSProperties;
259
+ } else {
260
+ return {};
261
+ }
262
+ }
263
+
264
+ private get tableFooter(): ReactNode {
265
+ if (!this.props.footerRows || this.props.footerRows.length === 0) {
266
+ return null;
267
+ }
268
+
269
+ return (
270
+ <tfoot>
271
+ {this.props.footerRows.map((row, rowIndex) => (
272
+ <tr key={rowIndex}>
273
+ {row.columns.map((col, colIndex) => (
274
+ <td
275
+ key={colIndex}
276
+ colSpan={col.colSpan}
277
+ className={`${styles.footerCell} ${col.className || ''} ${col.onCellClick ? styles.clickableFooterCell : ''}`}
278
+ onClick={col.onCellClick}
279
+ >
280
+ {col.cellRenderer()}
281
+ </td>
282
+ ))}
283
+ </tr>
284
+ ))}
285
+ </tfoot>
286
+ );
287
+ }
288
+
289
+ private get mobileFooter(): ReactNode {
290
+ if (!this.props.footerRows || this.props.footerRows.length === 0) {
291
+ return null;
292
+ }
293
+
294
+ return (
295
+ <div className={styles.footerCard}>
296
+ <div className={styles['footer-card-body']}>
297
+ {this.props.footerRows.map((row, rowIndex) => {
298
+ let currentColumnIndex = 0;
299
+ return (
300
+ <div key={rowIndex}>
301
+ {row.columns.map((col, colIndex) => {
302
+ let label = col.displayLabel;
303
+ if (!label && col.colSpan === 1) {
304
+ const header = this.props.columnDefinitions[currentColumnIndex];
305
+ if (header) {
306
+ label = this.getRawColumnDefinition(header).displayLabel;
307
+ }
308
+ }
309
+ currentColumnIndex += col.colSpan;
310
+ return (
311
+ <p
312
+ key={colIndex}
313
+ className={`${styles['footer-card-row']} ${col.className || ''} ${
314
+ col.onCellClick ? styles.clickableFooterCell : ''
315
+ }`}
316
+ onClick={col.onCellClick}
317
+ >
318
+ {label && <span className={styles['card-label']}>{label}</span>}
319
+ <span className={styles['card-value']}>{col.cellRenderer()}</span>
320
+ </p>
321
+ );
322
+ })}
323
+ </div>
324
+ );
325
+ })}
326
+ </div>
327
+ </div>
328
+ );
329
+ }
330
+
331
+ private get skeletonView(): ReactNode {
332
+ const skeletonRowCount = 5; // Or make this configurable
333
+ const columnCount = this.props.columnDefinitions.length;
334
+
335
+ if (this.state.isMobile) {
336
+ return (
337
+ <div>
338
+ {[...Array(skeletonRowCount)].map((_, i) => (
339
+ <div key={i} className={styles.skeletonCard}>
340
+ {[...Array(columnCount)].map((_, j) => (
341
+ <div key={j} className={`${styles.skeleton} ${styles.skeletonText}`} style={{ marginBottom: '0.5rem' }} />
342
+ ))}
343
+ </div>
344
+ ))}
345
+ </div>
346
+ );
347
+ }
348
+
349
+ return (
350
+ <table className={styles.responsiveTable}>
351
+ <thead>
352
+ <tr>
353
+ {[...Array(columnCount)].map((_, i) => (
354
+ <th key={i}>
355
+ <div className={`${styles.skeleton} ${styles.skeletonText}`} />
356
+ </th>
357
+ ))}
358
+ </tr>
359
+ </thead>
360
+ <tbody>
361
+ {[...Array(skeletonRowCount)].map((_, i) => (
362
+ <tr key={i}>
363
+ {[...Array(columnCount)].map((_, j) => (
364
+ <td key={j}>
365
+ <div className={`${styles.skeleton} ${styles.skeletonText}`} />
366
+ </td>
367
+ ))}
368
+ </tr>
369
+ ))}
370
+ </tbody>
371
+ </table>
372
+ );
373
+ }
374
+
375
+ private get mobileView(): ReactNode {
376
+ return (
377
+ <div>
378
+ {this.data.map((row, rowIndex) => (
379
+ <div
380
+ key={rowIndex}
381
+ className={`${styles['card']} ${this.props.animationProps?.animateOnLoad ? styles.animatedRow : ''}`}
382
+ style={{ animationDelay: `${rowIndex * 0.05}s` }}
383
+ onClick={(e) => {
384
+ this.rowClickFunction(row);
385
+ e.stopPropagation();
386
+ e.preventDefault();
387
+ }}
388
+ >
389
+ <div className={styles['card-header']}> </div>
390
+ <div className={styles['card-body']}>
391
+ {this.props.columnDefinitions.map((columnDefinition, colIndex) => {
392
+ const colDef = this.getColumnDefinition(columnDefinition, rowIndex);
393
+ const onHeaderClickCallback = this.onHeaderClickCallback(colDef);
394
+ const clickableHeaderClassName = this.getClickableHeaderClassName(onHeaderClickCallback, colDef);
395
+ return (
396
+ <div key={colIndex} className={styles['card-row']}>
397
+ <p>
398
+ <span
399
+ className={`${styles['card-label']} ${clickableHeaderClassName}`}
400
+ onClick={
401
+ onHeaderClickCallback ? () => onHeaderClickCallback(colDef.interactivity!.id) : undefined
402
+ }
403
+ >
404
+ {colDef.displayLabel}
405
+ </span>
406
+ <span className={styles['card-value']}>{colDef.cellRenderer(row)}</span>
407
+ </p>
408
+ </div>
409
+ );
410
+ })}
411
+ </div>
412
+ </div>
413
+ ))}
414
+ {this.mobileFooter}
415
+ </div>
416
+ );
417
+ }
418
+
419
+ private get largeScreenView(): ReactNode {
420
+ const useFixedHeaders = this.props.maxHeight ? true : false;
421
+
422
+ const fixedHeadersStyle = useFixedHeaders
423
+ ? ({ maxHeight: this.props.maxHeight, overflowY: 'auto' } as CSSProperties)
424
+ : {};
425
+
426
+ const Row = ({ index, style }: { index: number; style: CSSProperties }) => {
427
+ const row = this.data[index];
428
+ if (!row) return null; // Should not happen with correct item count
429
+
430
+ return (
431
+ <tr
432
+ key={index}
433
+ className={this.props.animationProps?.animateOnLoad ? styles.animatedRow : ''}
434
+ style={{ ...style, animationDelay: `${index * 0.05}s` }}
435
+ >
436
+ {this.props.columnDefinitions.map((columnDefinition, colIndex) => (
437
+ <td onClick={() => this.rowClickFunction(row)} key={colIndex}>
438
+ <span style={{ ...this.rowClickStyle }}>
439
+ {this.getColumnDefinition(columnDefinition, index).cellRenderer(row)}
440
+ </span>
441
+ </td>
442
+ ))}
443
+ </tr>
444
+ );
445
+ };
446
+
447
+ return (
448
+ <div style={fixedHeadersStyle} ref={this.tableContainerRef}>
449
+ <table className={styles['responsiveTable']}>
450
+ <thead ref={this.headerRef} className={this.state.isHeaderSticky ? styles.stickyHeader : ''}>
451
+ <tr>
452
+ {this.props.columnDefinitions.map((columnDefinition, colIndex) => {
453
+ const onHeaderClickCallback = this.onHeaderClickCallback(columnDefinition);
454
+ const clickableHeaderClassName = this.getClickableHeaderClassName(
455
+ onHeaderClickCallback,
456
+ columnDefinition,
457
+ );
458
+ return (
459
+ <th
460
+ key={colIndex}
461
+ className={`${clickableHeaderClassName}`}
462
+ onClick={
463
+ onHeaderClickCallback
464
+ ? () => onHeaderClickCallback(this.getColumnDefinition(columnDefinition, 0).interactivity!.id)
465
+ : undefined
466
+ }
467
+ >
468
+ {this.getColumnDefinition(columnDefinition, 0).displayLabel}
469
+ </th>
470
+ );
471
+ })}
472
+ </tr>
473
+ </thead>
474
+ <tbody>
475
+ {this.props.infiniteScrollProps?.enableInfiniteScroll ? (
476
+ <List
477
+ height={fixedHeadersStyle.maxHeight ? (typeof fixedHeadersStyle.maxHeight === 'string' ? parseFloat(fixedHeadersStyle.maxHeight) : fixedHeadersStyle.maxHeight) : 500} // Default height if not provided
478
+ itemCount={this.data.length}
479
+ itemSize={50} // Average row height, can be made configurable
480
+ width={'100%'}
481
+ outerRef={this.tableContainerRef} // Pass ref to outer element for scroll events
482
+ >
483
+ {Row}
484
+ </List>
485
+ ) : (
486
+ this.data.map((row, rowIndex) => (
487
+ <tr
488
+ key={rowIndex}
489
+ className={this.props.animationProps?.animateOnLoad ? styles.animatedRow : ''}
490
+ style={{ animationDelay: `${rowIndex * 0.05}s` }}
491
+ >
492
+ {this.props.columnDefinitions.map((columnDefinition, colIndex) => (
493
+ <td onClick={() => this.rowClickFunction(row)} key={colIndex}>
494
+ <span style={{ ...this.rowClickStyle }}>
495
+ {this.getColumnDefinition(columnDefinition, rowIndex).cellRenderer(row)}
496
+ </span>
497
+ </td>
498
+ ))}
499
+ </tr>
500
+ ))
501
+ )}
502
+ </tbody>
503
+ {this.tableFooter}
504
+ </table>
505
+ {this.renderPluginFooters()}
506
+ </div>
507
+ );
508
+ }
509
+
510
+ private renderPluginHeaders() {
511
+ if (!this.props.plugins) {
512
+ return null;
513
+ }
514
+
515
+ return this.props.plugins.map((plugin) => {
516
+ if (plugin.renderHeader) {
517
+ return <div key={plugin.id}>{plugin.renderHeader()}</div>;
518
+ }
519
+ return null;
520
+ });
521
+ }
522
+
523
+ private renderPluginFooters() {
524
+ if (!this.props.plugins) {
525
+ return null;
526
+ }
527
+
528
+ return this.props.plugins.map((plugin) => {
529
+ if (plugin.renderFooter) {
530
+ return <div key={plugin.id + '-footer'}>{plugin.renderFooter()}</div>;
531
+ }
532
+ return null;
533
+ });
534
+ }
535
+
536
+ render() {
537
+ if (this.props.animationProps?.isLoading) {
538
+ return this.skeletonView;
539
+ }
540
+
541
+ return (
542
+ <div>
543
+ {this.renderPluginHeaders()}
544
+ {!this.hasData && this.noDataComponent}
545
+ {this.hasData && this.state.isMobile && this.mobileView}
546
+ {this.hasData && !this.state.isMobile && this.largeScreenView}
547
+ </div>
548
+ );
549
+ }
550
+ }
551
+
552
+ export default ResponsiveTable;