jattac.libs.web.responsive-table 0.1.6 → 0.2.1
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 +581 -275
- 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 +1066 -34
- package/dist/index.js.map +1 -1
- package/gemini/index.md +1 -0
- package/gemini/release_git_steps.md +18 -2
- package/package.json +60 -58
- package/src/Data/IResponsiveTableColumnDefinition.tsx +1 -0
- 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 +26 -1
- package/src/UI/ResponsiveTable.tsx +532 -381
- package/src/index.tsx +4 -1
|
@@ -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
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* Using CSS variables for a more maintainable and themeable design */
|
|
2
|
-
|
|
2
|
+
.responsiveTable {
|
|
3
3
|
--table-border-color: #e0e0e0;
|
|
4
4
|
--table-header-bg: #f8f9fa;
|
|
5
5
|
--table-row-hover-bg: #e9ecef;
|
|
@@ -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
|
+
}
|