jattac.libs.web.responsive-table 0.8.0 → 0.8.2

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.
@@ -0,0 +1,1207 @@
1
+ import React, { createContext, useCallback, useMemo, useContext, useRef, useEffect, useState } from 'react';
2
+
3
+ var styles$2 = {"responsiveTable":"ResponsiveTable-module_responsiveTable__4y-Od","tableContainer":"ResponsiveTable-module_tableContainer__VjWjH","cardContainer":"ResponsiveTable-module_cardContainer__Het4h","card":"ResponsiveTable-module_card__b-U2v","card-header":"ResponsiveTable-module_card-header__Ttk51","card-row":"ResponsiveTable-module_card-row__qvIUJ","card-label":"ResponsiveTable-module_card-label__v9L71","card-value":"ResponsiveTable-module_card-value__BO-c-","clickableRow":"ResponsiveTable-module_clickableRow__0kjWm","clickableHeader":"ResponsiveTable-module_clickableHeader__xHQhF","sortable":"ResponsiveTable-module_sortable__yvA60","sorted-asc":"ResponsiveTable-module_sorted-asc__jzOIa","sorted-desc":"ResponsiveTable-module_sorted-desc__7WCFK","headerInnerWrapper":"ResponsiveTable-module_headerInnerWrapper__3VAhD","headerContent":"ResponsiveTable-module_headerContent__ODMzS","sortIcon":"ResponsiveTable-module_sortIcon__A9WtD","footerCell":"ResponsiveTable-module_footerCell__8H-uG","clickableFooterCell":"ResponsiveTable-module_clickableFooterCell__WB9Ss","footerCard":"ResponsiveTable-module_footerCard__-NE2M","footer-card-row":"ResponsiveTable-module_footer-card-row__Vv6Ur","selectedRow":"ResponsiveTable-module_selectedRow__-JyNW","animatedRow":"ResponsiveTable-module_animatedRow__SFjrJ","fadeInUp":"ResponsiveTable-module_fadeInUp__jMCS7","skeleton":"ResponsiveTable-module_skeleton__XxsXW","shimmer":"ResponsiveTable-module_shimmer__H8PhC","noDataWrapper":"ResponsiveTable-module_noDataWrapper__Rj-k3","noData":"ResponsiveTable-module_noData__IpwNq","spinner":"ResponsiveTable-module_spinner__Pn-3D","spin":"ResponsiveTable-module_spin__i3NHn","infoContainer":"ResponsiveTable-module_infoContainer__b9IF5","stickyHeader":"ResponsiveTable-module_stickyHeader__-jjN-","internalStickyHeader":"ResponsiveTable-module_internalStickyHeader__idiJY"};
4
+
5
+ /******************************************************************************
6
+ Copyright (c) Microsoft Corporation.
7
+
8
+ Permission to use, copy, modify, and/or distribute this software for any
9
+ purpose with or without fee is hereby granted.
10
+
11
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
12
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
13
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
14
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
15
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
16
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
17
+ PERFORMANCE OF THIS SOFTWARE.
18
+ ***************************************************************************** */
19
+ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */
20
+
21
+
22
+ function __rest(s, e) {
23
+ var t = {};
24
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
25
+ t[p] = s[p];
26
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
27
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
28
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
29
+ t[p[i]] = s[p[i]];
30
+ }
31
+ return t;
32
+ }
33
+
34
+ function __awaiter(thisArg, _arguments, P, generator) {
35
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
36
+ return new (P || (P = Promise))(function (resolve, reject) {
37
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
38
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
39
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
40
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
41
+ });
42
+ }
43
+
44
+ typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
45
+ var e = new Error(message);
46
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
47
+ };
48
+
49
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
50
+ const TableContext = createContext(undefined);
51
+ const useTableContext = () => {
52
+ const context = useContext(TableContext);
53
+ if (!context) {
54
+ throw new Error('useTableContext must be used within a TableProvider');
55
+ }
56
+ return context;
57
+ };
58
+ function TableProvider({ children, value }) {
59
+ const { processedData, activePlugins, selectionProps, } = value;
60
+ const getRawColumnDefinition = useCallback((columnDefinition) => {
61
+ if (typeof columnDefinition === 'function') {
62
+ if (processedData.length === 0) {
63
+ return { displayLabel: '', cellRenderer: () => '' };
64
+ }
65
+ return columnDefinition(processedData[0], 0);
66
+ }
67
+ return columnDefinition;
68
+ }, [processedData]);
69
+ const getColumnDefinition = useCallback((columnDefinition, rowIndex) => {
70
+ if (processedData.length === 0) {
71
+ return { displayLabel: '', cellRenderer: () => '' };
72
+ }
73
+ return columnDefinition instanceof Function ? columnDefinition(processedData[0], rowIndex) : columnDefinition;
74
+ }, [processedData]);
75
+ const onHeaderClickCallback = useCallback((colDef) => {
76
+ var _a;
77
+ const rawColumnDefinition = getRawColumnDefinition(colDef);
78
+ return (_a = rawColumnDefinition.interactivity) === null || _a === void 0 ? void 0 : _a.onHeaderClick;
79
+ }, [getRawColumnDefinition]);
80
+ const getClickableHeaderClassName = useCallback((onHeaderClick, colDef) => {
81
+ var _a;
82
+ const rawColumnDefinition = getRawColumnDefinition(colDef);
83
+ return onHeaderClick
84
+ ? ((_a = rawColumnDefinition.interactivity) === null || _a === void 0 ? void 0 : _a.className) || styles$2.clickableHeader
85
+ : '';
86
+ }, [getRawColumnDefinition]);
87
+ const getHeaderProps = useCallback((colDef) => {
88
+ const headerProps = {};
89
+ activePlugins.forEach((plugin) => {
90
+ if (plugin.getHeaderProps) {
91
+ Object.assign(headerProps, plugin.getHeaderProps(getRawColumnDefinition(colDef)));
92
+ }
93
+ });
94
+ return headerProps;
95
+ }, [activePlugins, getRawColumnDefinition]);
96
+ const getRowId = useCallback((row, index) => {
97
+ if (selectionProps && selectionProps.rowIdKey) {
98
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
99
+ return row[selectionProps.rowIdKey];
100
+ }
101
+ return index;
102
+ }, [selectionProps]);
103
+ const getRowProps = useCallback((row) => {
104
+ const rowProps = {};
105
+ const clickHandlers = [];
106
+ activePlugins.forEach((plugin) => {
107
+ if (plugin.getRowProps) {
108
+ const props = plugin.getRowProps(row);
109
+ if (props.className) {
110
+ rowProps.className = `${rowProps.className || ''} ${props.className}`.trim();
111
+ }
112
+ if (props.onClick) {
113
+ clickHandlers.push(props.onClick);
114
+ }
115
+ const rest = __rest(props, []);
116
+ Object.assign(rowProps, rest);
117
+ }
118
+ });
119
+ if (clickHandlers.length > 0) {
120
+ rowProps.onClick = (e) => {
121
+ clickHandlers.forEach(handler => handler(e));
122
+ };
123
+ }
124
+ return rowProps;
125
+ }, [activePlugins]);
126
+ const renderCell = useCallback((content, row, colDef) => {
127
+ let processedContent = content;
128
+ activePlugins.forEach((plugin) => {
129
+ if (plugin.renderCell) {
130
+ processedContent = plugin.renderCell(processedContent, row, colDef);
131
+ }
132
+ });
133
+ return processedContent;
134
+ }, [activePlugins]);
135
+ const contextValue = useMemo(() => (Object.assign(Object.assign({}, value), { currentData: processedData, getRawColumnDefinition,
136
+ getColumnDefinition,
137
+ onHeaderClickCallback,
138
+ getClickableHeaderClassName,
139
+ getHeaderProps,
140
+ getRowProps,
141
+ getRowId,
142
+ renderCell })), [
143
+ value,
144
+ processedData,
145
+ getRawColumnDefinition,
146
+ getColumnDefinition,
147
+ onHeaderClickCallback,
148
+ getClickableHeaderClassName,
149
+ getHeaderProps,
150
+ getRowProps,
151
+ getRowId,
152
+ renderCell,
153
+ ]);
154
+ return (React.createElement(TableContext.Provider, { value: contextValue }, children));
155
+ }
156
+
157
+ function TableHeaderCell(props) {
158
+ const { columnDefinition, colIndex } = props;
159
+ const { onHeaderClickCallback, getClickableHeaderClassName, getHeaderProps, getRawColumnDefinition, getColumnDefinition, } = useTableContext();
160
+ const onHeaderClick = onHeaderClickCallback(columnDefinition);
161
+ const clickableHeaderClassName = getClickableHeaderClassName(onHeaderClick, columnDefinition);
162
+ const headerProps = getHeaderProps(columnDefinition);
163
+ const combinedClassName = `${clickableHeaderClassName} ${headerProps.className ? styles$2[headerProps.className] : ''}`.trim();
164
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
165
+ const { className } = headerProps, restHeaderProps = __rest(headerProps, ["className"]);
166
+ return (React.createElement("th", Object.assign({ key: colIndex, className: combinedClassName }, restHeaderProps, { onClick: onHeaderClick ? () => onHeaderClick(getRawColumnDefinition(columnDefinition).interactivity.id) : restHeaderProps.onClick }),
167
+ React.createElement("div", { className: styles$2.headerInnerWrapper },
168
+ React.createElement("div", { className: styles$2.headerContent }, getColumnDefinition(columnDefinition, 0).displayLabel),
169
+ React.createElement("span", { className: styles$2.sortIcon }))));
170
+ }
171
+
172
+ function TableBodyCell(props) {
173
+ const { row, rowIndex, columnDefinition } = props;
174
+ const { getColumnDefinition, renderCell } = useTableContext();
175
+ const colDef = getColumnDefinition(columnDefinition, rowIndex);
176
+ const cellContent = colDef.cellRenderer(row);
177
+ return (React.createElement(React.Fragment, null, renderCell(cellContent, row, colDef)));
178
+ }
179
+
180
+ function TableBodyRow(props) {
181
+ const { row, rowIndex, columnDefinitions, onRowClick, selectionProps, animationProps, } = props;
182
+ const { getRowProps } = useTableContext();
183
+ const rowProps = getRowProps(row);
184
+ const isClickable = onRowClick || selectionProps;
185
+ const pluginOnClick = rowProps.onClick;
186
+ return (React.createElement("tr", { className: `${isClickable ? styles$2.clickableRow : ''} ${(animationProps === null || animationProps === void 0 ? void 0 : animationProps.animateOnLoad) ? styles$2.animatedRow : ''} ${rowProps.className || ''}`.trim(), style: { animationDelay: `${rowIndex * 0.05}s` }, "aria-selected": rowProps['aria-selected'], onClick: (e) => {
187
+ if (pluginOnClick) {
188
+ pluginOnClick(e);
189
+ }
190
+ if (onRowClick) {
191
+ onRowClick(row);
192
+ }
193
+ } }, columnDefinitions.map((columnDefinition, colIndex) => (React.createElement("td", { key: colIndex },
194
+ React.createElement(TableBodyCell, { row: row, rowIndex: rowIndex, columnDefinition: columnDefinition }))))));
195
+ }
196
+
197
+ const TableSentinel = ({ onIntersect, isLoading }) => {
198
+ const sentinelRef = useRef(null);
199
+ useEffect(() => {
200
+ if (isLoading)
201
+ return;
202
+ const observer = new IntersectionObserver((entries) => {
203
+ if (entries[0].isIntersecting) {
204
+ onIntersect();
205
+ }
206
+ }, {
207
+ root: null, // use the viewport
208
+ rootMargin: '200px', // start loading 200px before reaching the end
209
+ threshold: 0.1,
210
+ });
211
+ const currentSentinel = sentinelRef.current;
212
+ if (currentSentinel) {
213
+ observer.observe(currentSentinel);
214
+ }
215
+ return () => {
216
+ if (currentSentinel) {
217
+ observer.unobserve(currentSentinel);
218
+ }
219
+ };
220
+ }, [onIntersect, isLoading]);
221
+ return React.createElement("div", { ref: sentinelRef, style: { height: '1px' }, "aria-hidden": "true" });
222
+ };
223
+
224
+ function DesktopView(props) {
225
+ const { maxHeight, isHeaderSticky, tableContainerRef, headerRef, footerRows, renderPluginFooters, onScroll, } = props;
226
+ const { visibleColumns, originalColumnDefinitions, currentData, getRawColumnDefinition, onRowClick, selectionProps, animationProps, pagination, } = useTableContext();
227
+ const getEffectiveColSpan = useCallback((footerCol, startIndex) => {
228
+ const originalSpan = footerCol.colSpan || 1;
229
+ const endIndex = startIndex + originalSpan;
230
+ let visibleCount = 0;
231
+ for (let i = startIndex; i < endIndex; i++) {
232
+ const col = originalColumnDefinitions[i];
233
+ if (col && getRawColumnDefinition(col).visible !== false) {
234
+ visibleCount++;
235
+ }
236
+ }
237
+ return visibleCount;
238
+ }, [originalColumnDefinitions, getRawColumnDefinition]);
239
+ const tableFooter = useMemo(() => {
240
+ if (!footerRows || footerRows.length === 0) {
241
+ return null;
242
+ }
243
+ return (React.createElement("tfoot", null, footerRows.map((row, rowIndex) => {
244
+ let currentOriginalIndex = 0;
245
+ return (React.createElement("tr", { key: rowIndex }, row.columns.map((col, colIndex) => {
246
+ const effectiveColSpan = getEffectiveColSpan(col, currentOriginalIndex);
247
+ currentOriginalIndex += (col.colSpan || 1);
248
+ if (effectiveColSpan === 0)
249
+ return null;
250
+ return (React.createElement("td", { key: colIndex, colSpan: effectiveColSpan, className: `${styles$2.footerCell} ${col.className || ''} ${col.onCellClick ? styles$2.clickableFooterCell : ''}`, onClick: col.onCellClick }, col.cellRenderer()));
251
+ })));
252
+ })));
253
+ }, [footerRows, getEffectiveColSpan]);
254
+ const useFixedHeaders = !!maxHeight;
255
+ const fixedHeadersStyle = useFixedHeaders
256
+ ? { maxHeight, overflowY: 'auto' }
257
+ : {};
258
+ const headerClassName = useFixedHeaders
259
+ ? styles$2.internalStickyHeader
260
+ : (isHeaderSticky ? styles$2.stickyHeader : '');
261
+ return (React.createElement("div", { className: styles$2.tableContainer, style: fixedHeadersStyle, ref: tableContainerRef, onScroll: onScroll },
262
+ React.createElement("table", { className: styles$2['responsiveTable'] },
263
+ React.createElement("thead", { ref: headerRef, className: headerClassName },
264
+ React.createElement("tr", null, visibleColumns.map((columnDefinition, colIndex) => (React.createElement(TableHeaderCell, { key: colIndex, columnDefinition: columnDefinition, colIndex: colIndex }))))),
265
+ React.createElement("tbody", null, currentData.map((row, rowIndex) => (React.createElement(TableBodyRow, { key: rowIndex, row: row, rowIndex: rowIndex, columnDefinitions: visibleColumns, onRowClick: onRowClick, selectionProps: selectionProps, animationProps: animationProps })))),
266
+ tableFooter),
267
+ (pagination === null || pagination === void 0 ? void 0 : pagination.hasMore) && (React.createElement(TableSentinel, { onIntersect: () => pagination.loadNextPage(), isLoading: pagination.isFetchingMore })),
268
+ (pagination === null || pagination === void 0 ? void 0 : pagination.isFetchingMore) && (React.createElement("div", { className: styles$2.infoContainer },
269
+ React.createElement("div", { className: styles$2.spinner }),
270
+ React.createElement("span", null, "Loading more items..."))),
271
+ renderPluginFooters()));
272
+ }
273
+
274
+ function MobileView(props) {
275
+ const { mobileFooter } = props;
276
+ const { currentData, visibleColumns, onRowClick, selectionProps, animationProps, getRowProps, getRowId, getColumnDefinition, onHeaderClickCallback, getClickableHeaderClassName, pagination, } = useTableContext();
277
+ const isClickable = onRowClick || selectionProps;
278
+ return (React.createElement("div", { className: styles$2.cardContainer },
279
+ currentData.map((row, rowIndex) => {
280
+ const rowProps = getRowProps(row);
281
+ const pluginOnClick = rowProps.onClick;
282
+ return (React.createElement("div", { key: getRowId(row, rowIndex), className: `${styles$2.card} ${isClickable ? styles$2.clickableRow : ''} ${(animationProps === null || animationProps === void 0 ? void 0 : animationProps.animateOnLoad) ? styles$2.animatedRow : ''} ${rowProps.className || ''}`.trim(), style: { animationDelay: `${rowIndex * 0.05}s` }, "aria-selected": rowProps['aria-selected'], onClick: (e) => {
283
+ if (pluginOnClick)
284
+ pluginOnClick(e);
285
+ if (onRowClick)
286
+ onRowClick(row);
287
+ } },
288
+ React.createElement("div", { className: styles$2['card-body'] }, visibleColumns.map((columnDefinition, colIndex) => {
289
+ const colDef = getColumnDefinition(columnDefinition, rowIndex);
290
+ const onHeaderClick = onHeaderClickCallback(columnDefinition);
291
+ const clickableHeaderClassName = getClickableHeaderClassName(onHeaderClick, columnDefinition);
292
+ return (React.createElement("div", { key: colIndex, className: styles$2['card-row'] },
293
+ React.createElement("p", { className: styles$2['card-row-content'] },
294
+ React.createElement("span", { className: `${styles$2['card-label']} ${clickableHeaderClassName}`, onClick: (e) => {
295
+ if (onHeaderClick) {
296
+ e.stopPropagation();
297
+ onHeaderClick(colDef.interactivity.id);
298
+ }
299
+ } }, colDef.displayLabel),
300
+ React.createElement("span", { className: styles$2['card-value'] },
301
+ React.createElement(TableBodyCell, { row: row, rowIndex: rowIndex, columnDefinition: columnDefinition })))));
302
+ }))));
303
+ }),
304
+ (pagination === null || pagination === void 0 ? void 0 : pagination.hasMore) && (React.createElement(TableSentinel, { onIntersect: () => pagination.loadNextPage(), isLoading: pagination.isFetchingMore })),
305
+ (pagination === null || pagination === void 0 ? void 0 : pagination.isFetchingMore) && (React.createElement("div", { className: styles$2.infoContainer },
306
+ React.createElement("div", { className: styles$2.spinner }),
307
+ React.createElement("span", null, "Loading more items..."))),
308
+ mobileFooter));
309
+ }
310
+
311
+ function SkeletonView(props) {
312
+ const { isMobile, columnDefinitions } = props;
313
+ const skeletonRowCount = 5;
314
+ const columnCount = columnDefinitions.length;
315
+ if (isMobile) {
316
+ return (React.createElement("div", null, [...Array(skeletonRowCount)].map((_, i) => (React.createElement("div", { key: i, className: styles$2.skeletonCard }, [...Array(columnCount)].map((_, j) => (React.createElement("div", { key: j, className: `${styles$2.skeleton} ${styles$2.skeletonText}`, style: { marginBottom: '0.5rem' } }))))))));
317
+ }
318
+ return (React.createElement("table", { className: styles$2.responsiveTable },
319
+ React.createElement("thead", null,
320
+ React.createElement("tr", null, [...Array(columnCount)].map((_, i) => (React.createElement("th", { key: i },
321
+ React.createElement("div", { className: `${styles$2.skeleton} ${styles$2.skeletonText}` })))))),
322
+ React.createElement("tbody", null, [...Array(skeletonRowCount)].map((_, i) => (React.createElement("tr", { key: i }, [...Array(columnCount)].map((_, j) => (React.createElement("td", { key: j },
323
+ React.createElement("div", { className: `${styles$2.skeleton} ${styles$2.skeletonText}` }))))))))));
324
+ }
325
+
326
+ var styles$1 = {"spinner":"LoadingSpinner-module_spinner__F9V3x"};
327
+
328
+ var styles = {"infoContainer":"NoMoreDataMessage-module_infoContainer__dk1r5","noMoreData":"NoMoreDataMessage-module_noMoreData__ATuIg"};
329
+
330
+ const LoadingSpinner = () => {
331
+ return (React.createElement("div", { className: styles.infoContainer },
332
+ React.createElement("div", { className: styles$1.spinner }),
333
+ React.createElement("span", null, "Loading more items...")));
334
+ };
335
+
336
+ const NoMoreDataMessage = () => {
337
+ return (React.createElement("div", { className: `${styles.infoContainer} ${styles.noMoreData}` },
338
+ React.createElement("p", null, "You've reached the end!")));
339
+ };
340
+
341
+ function debounce(func, delay) {
342
+ let timeout;
343
+ return function (...args) {
344
+ clearTimeout(timeout);
345
+ timeout = setTimeout(() => {
346
+ func.apply(this, args);
347
+ }, delay);
348
+ };
349
+ }
350
+ const useResponsiveTable = (props) => {
351
+ const { mobileBreakpoint = 600, enablePageLevelStickyHeader = true, maxHeight, headerRef, scrollableRef } = props;
352
+ const [isMobile, setIsMobile] = useState(false);
353
+ const [isHeaderSticky, setIsHeaderSticky] = useState(false);
354
+ const handleResize = useCallback(() => {
355
+ setIsMobile(window.innerWidth <= mobileBreakpoint);
356
+ }, [mobileBreakpoint]);
357
+ const handleScroll = useCallback((currentTarget) => {
358
+ // Page-level sticky header logic
359
+ if (enablePageLevelStickyHeader && !maxHeight && (headerRef === null || headerRef === void 0 ? void 0 : headerRef.current)) {
360
+ const { top } = headerRef.current.getBoundingClientRect();
361
+ const sticky = top <= 0;
362
+ if (sticky !== isHeaderSticky) {
363
+ setIsHeaderSticky(sticky);
364
+ }
365
+ }
366
+ }, [enablePageLevelStickyHeader, maxHeight, headerRef, isHeaderSticky]);
367
+ const debouncedScrollHandler = useRef(debounce(handleScroll, 200)).current;
368
+ useEffect(() => {
369
+ handleResize(); // Initial check
370
+ window.addEventListener('resize', handleResize);
371
+ const scrollTarget = (scrollableRef === null || scrollableRef === void 0 ? void 0 : scrollableRef.current) || window;
372
+ if (enablePageLevelStickyHeader || maxHeight) { // Only add scroll listener if sticky header or internal scroll is enabled
373
+ scrollTarget.addEventListener('scroll', (e) => debouncedScrollHandler(e.currentTarget));
374
+ }
375
+ return () => {
376
+ window.removeEventListener('resize', handleResize);
377
+ if (enablePageLevelStickyHeader || maxHeight) {
378
+ scrollTarget.removeEventListener('scroll', (e) => debouncedScrollHandler(e.currentTarget));
379
+ }
380
+ };
381
+ }, [handleResize, debouncedScrollHandler, enablePageLevelStickyHeader, maxHeight, scrollableRef]);
382
+ return { isMobile, isHeaderSticky, debouncedScrollHandler: debouncedScrollHandler };
383
+ };
384
+
385
+ class FilterPlugin {
386
+ constructor() {
387
+ this.id = 'filter';
388
+ this.filterText = '';
389
+ this.debounceTimeout = null;
390
+ this.onPluginInit = (api) => {
391
+ this.api = api;
392
+ };
393
+ this.renderHeader = () => {
394
+ var _a;
395
+ if (!((_a = this.api.filterProps) === null || _a === void 0 ? void 0 : _a.showFilter)) {
396
+ return null;
397
+ }
398
+ return (React.createElement("div", { style: { marginBottom: '1rem' } },
399
+ React.createElement("input", { type: "text", placeholder: this.api.filterProps.filterPlaceholder || "Search...", onChange: this.handleFilterChange, className: this.api.filterProps.className, style: {
400
+ padding: '0.5rem',
401
+ border: '1px solid #ccc',
402
+ borderRadius: '4px',
403
+ } })));
404
+ };
405
+ this.processData = (data) => {
406
+ if (!this.filterText || !this.api.columnDefinitions) {
407
+ return data;
408
+ }
409
+ const lowercasedFilter = this.filterText.toLowerCase();
410
+ return data.filter((row) => {
411
+ return this.api.columnDefinitions.some((colDef) => {
412
+ // If colDef is a function, it won't have getFilterableValue, so skip it.
413
+ if (typeof colDef === 'function') {
414
+ return false;
415
+ }
416
+ // Now we know colDef is an object (IResponsiveTableColumnDefinition<TData>)
417
+ const typedColDef = colDef;
418
+ // Use a type guard to check if getFilterableValue exists on this branch of the union
419
+ if ('getFilterableValue' in typedColDef && typedColDef.getFilterableValue) {
420
+ const value = typedColDef.getFilterableValue(row);
421
+ return value === null || value === void 0 ? void 0 : value.toString().toLowerCase().includes(lowercasedFilter);
422
+ }
423
+ return false; // If getFilterableValue is not present or not a function
424
+ });
425
+ });
426
+ };
427
+ this.renderCell = (content,
428
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
429
+ _row,
430
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
431
+ _column) => {
432
+ if (!this.filterText || typeof content !== 'string') {
433
+ return content;
434
+ }
435
+ const parts = content.split(new RegExp(`(${this.filterText})`, 'gi'));
436
+ return (React.createElement("span", null, parts.map((part, i) => part.toLowerCase() === this.filterText.toLowerCase() ? (React.createElement("strong", { key: i }, part)) : (part))));
437
+ };
438
+ this.handleFilterChange = (e) => {
439
+ const currentFilterText = e.target.value;
440
+ if (this.debounceTimeout) {
441
+ clearTimeout(this.debounceTimeout);
442
+ }
443
+ this.debounceTimeout = setTimeout(() => {
444
+ this.filterText = currentFilterText;
445
+ this.api.forceUpdate();
446
+ }, 300);
447
+ };
448
+ }
449
+ }
450
+
451
+ class SelectionPlugin {
452
+ constructor() {
453
+ this.id = 'selection';
454
+ // Internal state for uncontrolled mode
455
+ this.internalSelectedIds = new Set();
456
+ this.onPluginInit = (api) => {
457
+ var _a;
458
+ this.api = api;
459
+ // For uncontrolled mode, initialize with selectedItems if provided on first render
460
+ if (((_a = this.api.selectionProps) === null || _a === void 0 ? void 0 : _a.selectedItems) && this.internalSelectedIds.size === 0) {
461
+ const { selectedItems, rowIdKey } = this.api.selectionProps;
462
+ if (selectedItems && rowIdKey) {
463
+ const initialIds = selectedItems.map(item => item[rowIdKey]);
464
+ this.internalSelectedIds = new Set(initialIds);
465
+ }
466
+ }
467
+ };
468
+ this.getRowId = (row) => {
469
+ const { rowIdKey } = this.api.selectionProps;
470
+ return row[rowIdKey];
471
+ };
472
+ this.isControlled = () => {
473
+ var _a;
474
+ return ((_a = this.api.selectionProps) === null || _a === void 0 ? void 0 : _a.selectedItems) !== undefined;
475
+ };
476
+ this.getSelectedIds = () => {
477
+ if (this.isControlled()) {
478
+ const { selectedItems, rowIdKey } = this.api.selectionProps;
479
+ return new Set((selectedItems === null || selectedItems === void 0 ? void 0 : selectedItems.map(item => item[rowIdKey])) || []);
480
+ }
481
+ return this.internalSelectedIds;
482
+ };
483
+ this.handleRowClick = (row) => {
484
+ const { onSelectionChange, mode = 'multiple', rowIdKey } = this.api.selectionProps;
485
+ const currentSelectedIds = this.getSelectedIds();
486
+ const newSelectedIds = new Set(currentSelectedIds);
487
+ const rowId = this.getRowId(row);
488
+ if (mode === 'single') {
489
+ if (newSelectedIds.has(rowId)) {
490
+ newSelectedIds.clear();
491
+ }
492
+ else {
493
+ newSelectedIds.clear();
494
+ newSelectedIds.add(rowId);
495
+ }
496
+ }
497
+ else { // multiple
498
+ if (newSelectedIds.has(rowId)) {
499
+ newSelectedIds.delete(rowId);
500
+ }
501
+ else {
502
+ newSelectedIds.add(rowId);
503
+ }
504
+ }
505
+ if (!this.isControlled()) {
506
+ this.internalSelectedIds = newSelectedIds;
507
+ }
508
+ const allData = this.api.getData();
509
+ const selectedItems = allData.filter(item => newSelectedIds.has(item[rowIdKey]));
510
+ onSelectionChange(selectedItems);
511
+ // In uncontrolled mode, we need to force a re-render to show the selection change
512
+ if (!this.isControlled()) {
513
+ this.api.forceUpdate();
514
+ }
515
+ };
516
+ this.getRowProps = (row) => {
517
+ const { selectedRowClassName } = this.api.selectionProps || {};
518
+ const selectedIds = this.getSelectedIds();
519
+ const rowId = this.getRowId(row);
520
+ const isSelected = selectedIds.has(rowId);
521
+ const combinedClassName = [
522
+ isSelected ? styles$2.selectedRow : '',
523
+ isSelected ? selectedRowClassName : ''
524
+ ].filter(Boolean).join(' ');
525
+ return {
526
+ className: combinedClassName,
527
+ onClick: () => this.handleRowClick(row),
528
+ 'aria-selected': isSelected,
529
+ };
530
+ };
531
+ }
532
+ }
533
+
534
+ // Type-safe sort comparer helpers
535
+ const createSortComparers = () => ({
536
+ numeric: (key) => (a, b, direction) => {
537
+ const numA = parseFloat(String(a[key]));
538
+ const numB = parseFloat(String(b[key]));
539
+ const aIsNaN = isNaN(numA);
540
+ const bIsNaN = isNaN(numB);
541
+ if (aIsNaN && bIsNaN)
542
+ return 0;
543
+ if (aIsNaN)
544
+ return 1; // Put non-numbers at the end
545
+ if (bIsNaN)
546
+ return -1;
547
+ return direction === 'asc' ? numA - numB : numB - numA;
548
+ },
549
+ caseInsensitiveString: (key) => (a, b, direction) => {
550
+ var _a, _b;
551
+ const valA = String((_a = a[key]) !== null && _a !== void 0 ? _a : '').toLowerCase();
552
+ const valB = String((_b = b[key]) !== null && _b !== void 0 ? _b : '').toLowerCase();
553
+ if (valA < valB)
554
+ return direction === 'asc' ? -1 : 1;
555
+ if (valA > valB)
556
+ return direction === 'asc' ? 1 : -1;
557
+ return 0;
558
+ },
559
+ date: (key) => (a, b, direction) => {
560
+ const dateA = new Date(String(a[key])).getTime();
561
+ const dateB = new Date(String(b[key])).getTime();
562
+ const aIsNaN = isNaN(dateA);
563
+ const bIsNaN = isNaN(dateB);
564
+ if (aIsNaN && bIsNaN)
565
+ return 0;
566
+ if (aIsNaN)
567
+ return 1; // Put invalid dates at the end
568
+ if (bIsNaN)
569
+ return -1;
570
+ return direction === 'asc' ? dateA - dateB : dateB - dateA;
571
+ },
572
+ });
573
+ class SortPlugin {
574
+ constructor(options) {
575
+ var _a, _b;
576
+ this.id = 'sort';
577
+ this.comparers = createSortComparers();
578
+ this.onPluginInit = (api) => {
579
+ this.api = api;
580
+ };
581
+ this.processData = (data) => {
582
+ if (!this.sortColumn || !this.sortDirection) {
583
+ return data;
584
+ }
585
+ const columnDef = this.api.columnDefinitions.find((col) => (typeof col === 'object' && col.columnId === this.sortColumn));
586
+ if (!columnDef) {
587
+ return data;
588
+ }
589
+ const sortedData = [...data].sort((a, b) => {
590
+ if ('sortComparer' in columnDef && columnDef.sortComparer) {
591
+ if (columnDef.sortComparer.length < 3) {
592
+ console.warn(`The custom sortComparer for column '${this.sortColumn}' should accept all three parameters (a, b, direction) to ensure correct sorting behavior. You provided a function with ${columnDef.sortComparer.length} parameters.`);
593
+ }
594
+ return columnDef.sortComparer(a, b, this.sortDirection);
595
+ }
596
+ if ('getSortableValue' in columnDef && columnDef.getSortableValue) {
597
+ const aValue = columnDef.getSortableValue(a);
598
+ const bValue = columnDef.getSortableValue(b);
599
+ if (aValue < bValue)
600
+ return this.sortDirection === 'asc' ? -1 : 1;
601
+ if (aValue > bValue)
602
+ return this.sortDirection === 'asc' ? 1 : -1;
603
+ return 0;
604
+ }
605
+ // Fallback to dataKey if it exists and no other sorter is provided
606
+ if ('dataKey' in columnDef && columnDef.dataKey) {
607
+ const key = columnDef.dataKey;
608
+ const aValue = a[key];
609
+ const bValue = b[key];
610
+ if (aValue < bValue)
611
+ return this.sortDirection === 'asc' ? -1 : 1;
612
+ if (aValue > bValue)
613
+ return this.sortDirection === 'asc' ? 1 : -1;
614
+ return 0;
615
+ }
616
+ return 0;
617
+ });
618
+ return sortedData;
619
+ };
620
+ this.getHeaderProps = (columnDef) => {
621
+ const { columnId } = columnDef;
622
+ const isSortable = ('sortComparer' in columnDef && !!columnDef.sortComparer) || ('getSortableValue' in columnDef && !!columnDef.getSortableValue);
623
+ // A column must have a columnId and a sort function to be sortable
624
+ if (!isSortable || !columnId) {
625
+ return {};
626
+ }
627
+ const onHeaderClick = (e) => {
628
+ const target = e.target;
629
+ console.log('SortPlugin: Header clicked. Target:', target);
630
+ // If the click is on an interactive element, don't sort
631
+ if (target.closest('input, button, a, [onclick]')) {
632
+ console.log('SortPlugin: Interactive element clicked, ignoring sort.');
633
+ return;
634
+ }
635
+ console.log('SortPlugin: Non-interactive element clicked, proceeding with sort.');
636
+ if (this.sortColumn === columnId) {
637
+ if (this.sortDirection === 'desc') {
638
+ this.sortColumn = null;
639
+ this.sortDirection = null;
640
+ }
641
+ else {
642
+ this.sortDirection = 'desc';
643
+ }
644
+ }
645
+ else {
646
+ this.sortColumn = columnId;
647
+ this.sortDirection = 'asc';
648
+ }
649
+ this.api.forceUpdate();
650
+ };
651
+ let sortClassName = 'sortable';
652
+ if (this.sortColumn === columnId) {
653
+ sortClassName = `sorted-${this.sortDirection}`;
654
+ }
655
+ return {
656
+ onClick: onHeaderClick,
657
+ className: sortClassName,
658
+ 'aria-sort': (this.sortColumn === columnId ? (this.sortDirection === 'asc' ? 'ascending' : 'descending') : 'none'),
659
+ };
660
+ };
661
+ this.sortColumn = (_a = options === null || options === void 0 ? void 0 : options.initialSortColumn) !== null && _a !== void 0 ? _a : null;
662
+ this.sortDirection = (_b = options === null || options === void 0 ? void 0 : options.initialSortDirection) !== null && _b !== void 0 ? _b : null;
663
+ }
664
+ }
665
+
666
+ const useTablePlugins = (props) => {
667
+ const { data, plugins, filterProps, selectionProps, sortProps, columnDefinitions, getScrollableElement, infiniteScrollProps, } = props;
668
+ const [processedData, setProcessedData] = useState(data);
669
+ const [activePlugins, setActivePlugins] = useState([]);
670
+ // Persist internal plugins using refs to prevent state loss
671
+ const filterPluginRef = useRef(null);
672
+ const selectionPluginRef = useRef(null);
673
+ const sortPluginRef = useRef(null);
674
+ const getRawColumnDefinition = useCallback((columnDefinition) => {
675
+ if (typeof columnDefinition === 'function') {
676
+ return columnDefinition(data[0] || {}, 0);
677
+ }
678
+ return columnDefinition;
679
+ }, [data]);
680
+ const visibleColumns = useMemo(() => {
681
+ return columnDefinitions.filter(col => {
682
+ const raw = getRawColumnDefinition(col);
683
+ return raw.visible !== false;
684
+ });
685
+ }, [columnDefinitions, getRawColumnDefinition]);
686
+ const initializePlugins = useCallback(() => {
687
+ const newActivePlugins = [];
688
+ // 1. Add external plugins
689
+ if (plugins) {
690
+ newActivePlugins.push(...plugins);
691
+ }
692
+ // 2. Manage internal FilterPlugin
693
+ if (filterProps === null || filterProps === void 0 ? void 0 : filterProps.showFilter) {
694
+ if (!filterPluginRef.current) {
695
+ filterPluginRef.current = new FilterPlugin();
696
+ }
697
+ if (!newActivePlugins.some(p => p.id === 'filter')) {
698
+ newActivePlugins.push(filterPluginRef.current);
699
+ }
700
+ }
701
+ else {
702
+ filterPluginRef.current = null;
703
+ }
704
+ // 3. Manage internal SelectionPlugin
705
+ if (selectionProps === null || selectionProps === void 0 ? void 0 : selectionProps.onSelectionChange) {
706
+ if (!selectionPluginRef.current) {
707
+ selectionPluginRef.current = new SelectionPlugin();
708
+ }
709
+ if (!newActivePlugins.some(p => p.id === 'selection')) {
710
+ newActivePlugins.push(selectionPluginRef.current);
711
+ }
712
+ }
713
+ else {
714
+ selectionPluginRef.current = null;
715
+ }
716
+ // 4. Manage internal SortPlugin
717
+ const isAnyColumnSortable = columnDefinitions.some(col => {
718
+ const rawCol = getRawColumnDefinition(col);
719
+ return rawCol.sortComparer || rawCol.getSortableValue;
720
+ });
721
+ if (isAnyColumnSortable) {
722
+ if (!sortPluginRef.current) {
723
+ sortPluginRef.current = new SortPlugin(sortProps);
724
+ }
725
+ if (!newActivePlugins.some(p => p.id === 'sort')) {
726
+ newActivePlugins.push(sortPluginRef.current);
727
+ }
728
+ }
729
+ else {
730
+ sortPluginRef.current = null;
731
+ }
732
+ setActivePlugins(newActivePlugins);
733
+ const api = {
734
+ getData: () => data,
735
+ forceUpdate: forceUpdatePlugins,
736
+ getScrollableElement: getScrollableElement,
737
+ infiniteScrollProps: infiniteScrollProps,
738
+ filterProps: filterProps,
739
+ selectionProps: selectionProps,
740
+ columnDefinitions: columnDefinitions,
741
+ };
742
+ // Initialize/Refresh all active plugins with the current API
743
+ newActivePlugins.forEach((plugin) => {
744
+ if (plugin.onPluginInit) {
745
+ plugin.onPluginInit(api);
746
+ }
747
+ });
748
+ // Run the data processing pipeline
749
+ let currentProcessedData = [...data];
750
+ newActivePlugins.forEach((plugin) => {
751
+ if (plugin.processData) {
752
+ currentProcessedData = plugin.processData(currentProcessedData);
753
+ }
754
+ });
755
+ return currentProcessedData;
756
+ }, [
757
+ data,
758
+ plugins,
759
+ filterProps,
760
+ selectionProps,
761
+ sortProps,
762
+ columnDefinitions,
763
+ getScrollableElement,
764
+ infiniteScrollProps,
765
+ getRawColumnDefinition,
766
+ ]);
767
+ const forceUpdatePlugins = useCallback(() => {
768
+ setProcessedData(initializePlugins());
769
+ }, [initializePlugins]);
770
+ // Handle re-initialization when props change
771
+ useEffect(() => {
772
+ setProcessedData(initializePlugins());
773
+ }, [initializePlugins]);
774
+ return { processedData, activePlugins, visibleColumns, forceUpdatePlugins };
775
+ };
776
+
777
+ function InfiniteTable(props) {
778
+ const { columnDefinitions, data, noDataComponent, maxHeight, onRowClick, footerRows, mobileBreakpoint, plugins, enablePageLevelStickyHeader, infiniteScrollProps, filterProps, selectionProps, animationProps, sortProps, } = props;
779
+ const tableContainerRef = useRef(null);
780
+ const headerRef = useRef(null);
781
+ const { isMobile, isHeaderSticky, debouncedScrollHandler } = useResponsiveTable({
782
+ mobileBreakpoint,
783
+ enablePageLevelStickyHeader,
784
+ maxHeight,
785
+ headerRef,
786
+ scrollableRef: tableContainerRef,
787
+ });
788
+ const [internalData, setInternalData] = useState(data || []);
789
+ const [isLoadingMore, setIsLoadingMore] = useState(false);
790
+ const [internalHasMore, setInternalHasMore] = useState(true);
791
+ const getScrollableElement = useCallback(() => tableContainerRef.current, []);
792
+ const { processedData, activePlugins, visibleColumns } = useTablePlugins({
793
+ data: internalData,
794
+ plugins,
795
+ filterProps,
796
+ selectionProps,
797
+ sortProps,
798
+ columnDefinitions,
799
+ getScrollableElement,
800
+ infiniteScrollProps,
801
+ });
802
+ const currentHasMore = (infiniteScrollProps === null || infiniteScrollProps === void 0 ? void 0 : infiniteScrollProps.hasMore) !== undefined
803
+ ? infiniteScrollProps.hasMore
804
+ : internalHasMore;
805
+ const hasData = useMemo(() => processedData.length > 0, [processedData]);
806
+ const noDataComponentNode = noDataComponent || React.createElement("div", { className: styles$2.noData }, "No data");
807
+ const defaultLoadingComponent = React.createElement(LoadingSpinner, null);
808
+ const defaultNoMoreDataComponent = React.createElement(NoMoreDataMessage, null);
809
+ const loadMoreData = useCallback(() => __awaiter(this, void 0, void 0, function* () {
810
+ var _a;
811
+ if (!infiniteScrollProps || isLoadingMore)
812
+ return;
813
+ setIsLoadingMore(true);
814
+ try {
815
+ const newItems = yield ((_a = infiniteScrollProps.onLoadMore) === null || _a === void 0 ? void 0 : _a.call(infiniteScrollProps, internalData));
816
+ if (infiniteScrollProps.hasMore === undefined) {
817
+ if (!newItems || newItems.length === 0) {
818
+ setInternalHasMore(false);
819
+ }
820
+ }
821
+ if (newItems && newItems.length > 0) {
822
+ setInternalData(prevData => [...prevData, ...newItems]);
823
+ }
824
+ }
825
+ catch (error) {
826
+ console.error("Failed to load more items for infinite scroll:", error);
827
+ }
828
+ finally {
829
+ setIsLoadingMore(false);
830
+ }
831
+ }), [infiniteScrollProps, isLoadingMore, internalData]);
832
+ useEffect(() => {
833
+ setInternalData(data || []);
834
+ }, [data]);
835
+ useEffect(() => {
836
+ if (data.length === 0) {
837
+ loadMoreData();
838
+ }
839
+ }, [data.length, loadMoreData]);
840
+ const handleScrollForInfinite = useCallback((currentTarget) => {
841
+ if (!currentTarget)
842
+ return;
843
+ const { scrollHeight, scrollTop, clientHeight } = currentTarget;
844
+ if (currentHasMore && !isLoadingMore && scrollHeight - scrollTop - clientHeight < 100) {
845
+ loadMoreData();
846
+ }
847
+ }, [currentHasMore, isLoadingMore, loadMoreData]);
848
+ const mobileFooter = useMemo(() => {
849
+ if (!footerRows || footerRows.length === 0) {
850
+ return null;
851
+ }
852
+ // Helper to get raw column definition in this context
853
+ const getRaw = (colDef) => {
854
+ if (typeof colDef === 'function') {
855
+ return processedData.length > 0 ? colDef(processedData[0], 0) : { displayLabel: '', cellRenderer: () => '' };
856
+ }
857
+ return colDef;
858
+ };
859
+ return (React.createElement("div", { className: styles$2.footerCard },
860
+ React.createElement("div", { className: styles$2['footer-card-body'] }, footerRows.map((row, rowIndex) => {
861
+ let currentColumnIndex = 0;
862
+ return (React.createElement("div", { key: rowIndex }, row.columns.map((col, colIndex) => {
863
+ let label = col.displayLabel;
864
+ if (!label && col.colSpan === 1) {
865
+ const header = columnDefinitions[currentColumnIndex];
866
+ if (header) {
867
+ label = getRaw(header).displayLabel;
868
+ }
869
+ }
870
+ currentColumnIndex += col.colSpan;
871
+ return (React.createElement("p", { key: colIndex, className: `${styles$2['footer-card-row']} ${col.className || ''} ${col.onCellClick ? styles$2.clickableFooterCell : ''}`, onClick: col.onCellClick },
872
+ label && React.createElement("span", { className: styles$2['card-label'] }, label),
873
+ React.createElement("span", { className: styles$2['card-value'] }, col.cellRenderer())));
874
+ })));
875
+ }))));
876
+ }, [footerRows, columnDefinitions, processedData]);
877
+ const renderPluginHeaders = useCallback(() => {
878
+ if (!activePlugins) {
879
+ return null;
880
+ }
881
+ return activePlugins.map((plugin) => {
882
+ if (plugin.renderHeader) {
883
+ if (plugin.id === 'sort' && !isMobile) {
884
+ return null;
885
+ }
886
+ return React.createElement("div", { key: plugin.id }, plugin.renderHeader());
887
+ }
888
+ return null;
889
+ });
890
+ }, [activePlugins, isMobile]);
891
+ const renderPluginFooters = useCallback(() => {
892
+ if (!plugins) {
893
+ return null;
894
+ }
895
+ return plugins.map((plugin) => {
896
+ if (plugin.renderFooter) {
897
+ return React.createElement("div", { key: plugin.id + '-footer' }, plugin.renderFooter());
898
+ }
899
+ return null;
900
+ });
901
+ }, [plugins]);
902
+ const infiniteStatusUI = (React.createElement(React.Fragment, null,
903
+ isLoadingMore && ((infiniteScrollProps === null || infiniteScrollProps === void 0 ? void 0 : infiniteScrollProps.loadingMoreComponent) || defaultLoadingComponent),
904
+ !isLoadingMore && !currentHasMore && ((infiniteScrollProps === null || infiniteScrollProps === void 0 ? void 0 : infiniteScrollProps.noMoreDataComponent) || defaultNoMoreDataComponent)));
905
+ if ((animationProps === null || animationProps === void 0 ? void 0 : animationProps.isLoading) && !hasData) {
906
+ return React.createElement(SkeletonView, { isMobile: isMobile, columnDefinitions: visibleColumns });
907
+ }
908
+ return (React.createElement(TableProvider, { value: {
909
+ data: internalData,
910
+ processedData,
911
+ visibleColumns,
912
+ originalColumnDefinitions: columnDefinitions,
913
+ activePlugins,
914
+ onRowClick,
915
+ selectionProps,
916
+ animationProps,
917
+ } },
918
+ React.createElement("div", null,
919
+ React.createElement("div", { style: { display: 'flex', justifyContent: 'flex-end' } }, renderPluginHeaders()),
920
+ !hasData && noDataComponentNode,
921
+ hasData && (isMobile ? (React.createElement(MobileView, { mobileFooter: mobileFooter })) : (React.createElement(DesktopView, { maxHeight: maxHeight, isHeaderSticky: isHeaderSticky, tableContainerRef: tableContainerRef, headerRef: headerRef, footerRows: footerRows, renderPluginFooters: renderPluginFooters, onScroll: (e) => {
922
+ debouncedScrollHandler(e.currentTarget); // For sticky header
923
+ handleScrollForInfinite(e.currentTarget); // For infinite scroll
924
+ } }))),
925
+ hasData && infiniteStatusUI)));
926
+ }
927
+
928
+ const useTableDataSource = (props) => {
929
+ const { dataSource, pageSize = 20, initialData = [], sort, filter } = props;
930
+ const [data, setData] = useState(initialData);
931
+ const [currentPage, setCurrentPage] = useState(1);
932
+ const [hasMore, setHasMore] = useState(true);
933
+ const [totalCount, setTotalCount] = useState(undefined);
934
+ const [isLoading, setIsLoading] = useState(false);
935
+ const [isFetchingMore, setIsFetchingMore] = useState(false);
936
+ const isInitialMount = useRef(true);
937
+ const fetchData = useCallback((page, isAppend) => __awaiter(void 0, void 0, void 0, function* () {
938
+ if (!dataSource)
939
+ return;
940
+ if (isAppend) {
941
+ setIsFetchingMore(true);
942
+ }
943
+ else {
944
+ setIsLoading(true);
945
+ }
946
+ try {
947
+ const params = {
948
+ page,
949
+ pageSize,
950
+ sort,
951
+ filter,
952
+ };
953
+ const result = yield dataSource(params);
954
+ let newItems = [];
955
+ let newTotalCount = undefined;
956
+ if (Array.isArray(result)) {
957
+ newItems = result;
958
+ }
959
+ else {
960
+ newItems = result.items;
961
+ newTotalCount = result.totalCount;
962
+ }
963
+ setData(prev => isAppend ? [...prev, ...newItems] : newItems);
964
+ setTotalCount(newTotalCount);
965
+ setCurrentPage(page);
966
+ // Intelligent hasMore detection
967
+ if (newTotalCount !== undefined) {
968
+ const currentTotalLoaded = (isAppend ? data.length : 0) + newItems.length;
969
+ setHasMore(currentTotalLoaded < newTotalCount);
970
+ }
971
+ else {
972
+ // If we got fewer items than pageSize, we've reached the end
973
+ setHasMore(newItems.length === pageSize);
974
+ }
975
+ }
976
+ catch (error) {
977
+ console.error('Error fetching data from dataSource:', error);
978
+ setHasMore(false);
979
+ }
980
+ finally {
981
+ setIsLoading(false);
982
+ setIsFetchingMore(false);
983
+ }
984
+ }), [dataSource, pageSize, sort, filter, data.length]);
985
+ const loadNextPage = useCallback(() => __awaiter(void 0, void 0, void 0, function* () {
986
+ if (isLoading || isFetchingMore || !hasMore || !dataSource)
987
+ return;
988
+ yield fetchData(currentPage + 1, true);
989
+ }), [currentPage, hasMore, isLoading, isFetchingMore, dataSource, fetchData]);
990
+ const resetAndFetch = useCallback(() => __awaiter(void 0, void 0, void 0, function* () {
991
+ if (!dataSource)
992
+ return;
993
+ yield fetchData(1, false);
994
+ }), [dataSource, fetchData]);
995
+ // Handle changes in sort or filter (reset to page 1)
996
+ useEffect(() => {
997
+ if (isInitialMount.current) {
998
+ isInitialMount.current = false;
999
+ if (dataSource && initialData.length === 0) {
1000
+ resetAndFetch();
1001
+ }
1002
+ return;
1003
+ }
1004
+ resetAndFetch();
1005
+ }, [sort, filter, dataSource]); // initialData and pageSize changes don't trigger reset by default
1006
+ return {
1007
+ data,
1008
+ currentPage,
1009
+ hasMore,
1010
+ totalCount,
1011
+ isLoading,
1012
+ isFetchingMore,
1013
+ loadNextPage,
1014
+ resetAndFetch,
1015
+ };
1016
+ };
1017
+
1018
+ /**
1019
+ * A highly customizable, mobile-first responsive React table.
1020
+ * Supports static data or async data sources with built-in infinite scroll.
1021
+ */
1022
+ function ResponsiveTable(props) {
1023
+ const { columnDefinitions, data: initialData, dataSource, pageSize, noDataComponent, maxHeight, onRowClick, footerRows, mobileBreakpoint, plugins, enablePageLevelStickyHeader, infiniteScrollProps, filterProps, selectionProps, animationProps, sortProps, } = props;
1024
+ const tableContainerRef = useRef(null);
1025
+ const headerRef = useRef(null);
1026
+ const { isMobile, isHeaderSticky } = useResponsiveTable({
1027
+ mobileBreakpoint,
1028
+ enablePageLevelStickyHeader,
1029
+ maxHeight,
1030
+ headerRef,
1031
+ scrollableRef: tableContainerRef,
1032
+ });
1033
+ const getScrollableElement = useCallback(() => tableContainerRef.current, []);
1034
+ // Track active sort state for dataSource
1035
+ const [activeSort /*, setActiveSort*/] = useState((sortProps === null || sortProps === void 0 ? void 0 : sortProps.initialSortColumn) ? { columnId: sortProps.initialSortColumn, direction: sortProps.initialSortDirection || 'asc' } : undefined);
1036
+ const { data: sourceData, isLoading: isSourceLoading, isFetchingMore, hasMore, totalCount, currentPage, loadNextPage, } = useTableDataSource({
1037
+ dataSource,
1038
+ pageSize,
1039
+ initialData,
1040
+ sort: activeSort,
1041
+ // We'll need to extract filter state if we want to support dataSource filtering
1042
+ });
1043
+ const currentDataToProcess = dataSource ? sourceData : initialData;
1044
+ const { processedData, activePlugins, visibleColumns } = useTablePlugins({
1045
+ data: currentDataToProcess,
1046
+ plugins,
1047
+ filterProps,
1048
+ selectionProps,
1049
+ sortProps,
1050
+ columnDefinitions,
1051
+ getScrollableElement,
1052
+ infiniteScrollProps,
1053
+ });
1054
+ // Sync sort state from SortPlugin back to our local state to trigger dataSource re-fetch
1055
+ useEffect(() => {
1056
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1057
+ activePlugins.find(p => p.id === 'sort');
1058
+ }, [activePlugins, dataSource]);
1059
+ const hasData = useMemo(() => processedData.length > 0, [processedData]);
1060
+ const noDataSvg = (React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", fill: "#ccc", height: "40", width: "40", viewBox: "0 0 24 24" },
1061
+ React.createElement("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" })));
1062
+ const noDataComponentNode = noDataComponent || (React.createElement("div", { className: styles$2.noDataWrapper },
1063
+ noDataSvg,
1064
+ React.createElement("div", { className: styles$2.noData }, "No data")));
1065
+ const mobileFooter = useMemo(() => {
1066
+ if (!footerRows || footerRows.length === 0) {
1067
+ return null;
1068
+ }
1069
+ // Helper to get raw column definition in this context
1070
+ const getRaw = (colDef) => {
1071
+ if (typeof colDef === 'function') {
1072
+ return processedData.length > 0 ? colDef(processedData[0], 0) : { displayLabel: '', cellRenderer: () => '' };
1073
+ }
1074
+ return colDef;
1075
+ };
1076
+ return (React.createElement("div", { className: styles$2.footerCard },
1077
+ React.createElement("div", { className: styles$2['footer-card-body'] }, footerRows.map((row, rowIndex) => {
1078
+ let currentColumnIndex = 0;
1079
+ return (React.createElement("div", { key: rowIndex }, row.columns.map((col, colIndex) => {
1080
+ let label = col.displayLabel;
1081
+ if (!label && col.colSpan === 1) {
1082
+ const header = columnDefinitions[currentColumnIndex];
1083
+ if (header) {
1084
+ label = getRaw(header).displayLabel;
1085
+ }
1086
+ }
1087
+ currentColumnIndex += col.colSpan;
1088
+ return (React.createElement("p", { key: colIndex, className: `${styles$2['footer-card-row']} ${col.className || ''} ${col.onCellClick ? styles$2.clickableFooterCell : ''}`, onClick: col.onCellClick },
1089
+ label && React.createElement("span", { className: styles$2['card-label'] }, label),
1090
+ React.createElement("span", { className: styles$2['card-value'] }, col.cellRenderer())));
1091
+ })));
1092
+ }))));
1093
+ }, [footerRows, columnDefinitions, processedData]);
1094
+ const renderPluginHeaders = useCallback(() => {
1095
+ if (!activePlugins) {
1096
+ return null;
1097
+ }
1098
+ return activePlugins.map((plugin) => {
1099
+ if (plugin.renderHeader) {
1100
+ if (plugin.id === 'sort' && !isMobile) {
1101
+ return null;
1102
+ }
1103
+ return React.createElement("div", { key: plugin.id }, plugin.renderHeader());
1104
+ }
1105
+ return null;
1106
+ });
1107
+ }, [activePlugins, isMobile]);
1108
+ const renderPluginFooters = useCallback(() => {
1109
+ if (!plugins) {
1110
+ return null;
1111
+ }
1112
+ return plugins.map((plugin) => {
1113
+ if (plugin.renderFooter) {
1114
+ return React.createElement("div", { key: plugin.id + '-footer' }, plugin.renderFooter());
1115
+ }
1116
+ return null;
1117
+ });
1118
+ }, [plugins]);
1119
+ if (infiniteScrollProps) {
1120
+ return React.createElement(InfiniteTable, Object.assign({}, props));
1121
+ }
1122
+ const isLoading = (animationProps === null || animationProps === void 0 ? void 0 : animationProps.isLoading) || isSourceLoading;
1123
+ if (isLoading && !hasData) {
1124
+ return React.createElement(SkeletonView, { isMobile: isMobile, columnDefinitions: visibleColumns });
1125
+ }
1126
+ return (React.createElement(TableProvider, { value: {
1127
+ data: currentDataToProcess,
1128
+ processedData,
1129
+ visibleColumns,
1130
+ originalColumnDefinitions: columnDefinitions,
1131
+ activePlugins,
1132
+ onRowClick,
1133
+ selectionProps,
1134
+ animationProps: Object.assign(Object.assign({}, animationProps), { isLoading }),
1135
+ dataSource,
1136
+ pagination: dataSource ? {
1137
+ currentPage,
1138
+ pageSize: pageSize || 20,
1139
+ hasMore,
1140
+ totalCount,
1141
+ isLoading: isSourceLoading,
1142
+ isFetchingMore,
1143
+ loadNextPage,
1144
+ } : undefined,
1145
+ } },
1146
+ React.createElement("div", null,
1147
+ React.createElement("div", { style: { display: 'flex', justifyContent: 'flex-end' } }, renderPluginHeaders()),
1148
+ !hasData && !isLoading && noDataComponentNode,
1149
+ (hasData || isLoading) && isMobile && (React.createElement(MobileView, { mobileFooter: mobileFooter })),
1150
+ (hasData || isLoading) && !isMobile && (React.createElement(DesktopView, { maxHeight: maxHeight, isHeaderSticky: isHeaderSticky, tableContainerRef: tableContainerRef, headerRef: headerRef, footerRows: footerRows, renderPluginFooters: renderPluginFooters })))));
1151
+ }
1152
+
1153
+ class InfiniteScrollPlugin {
1154
+ constructor() {
1155
+ this.id = 'infinite-scroll';
1156
+ this.isLoadingMore = false;
1157
+ this.onPluginInit = (api) => {
1158
+ this.api = api;
1159
+ this.attachScrollListener();
1160
+ };
1161
+ this.attachScrollListener = () => {
1162
+ var _a, _b;
1163
+ const scrollableElement = (_b = (_a = this.api).getScrollableElement) === null || _b === void 0 ? void 0 : _b.call(_a);
1164
+ if (scrollableElement) {
1165
+ scrollableElement.addEventListener('scroll', this.handleScroll);
1166
+ }
1167
+ };
1168
+ this.handleScroll = () => __awaiter(this, void 0, void 0, function* () {
1169
+ var _a, _b, _c, _d;
1170
+ const scrollableElement = (_b = (_a = this.api).getScrollableElement) === null || _b === void 0 ? void 0 : _b.call(_a);
1171
+ if (!scrollableElement || !this.api.infiniteScrollProps) {
1172
+ return;
1173
+ }
1174
+ const { scrollTop, scrollHeight, clientHeight } = scrollableElement;
1175
+ const scrollThreshold = 200; // Load more data when 200px from the bottom
1176
+ if (scrollHeight - scrollTop - clientHeight < scrollThreshold &&
1177
+ this.api.infiniteScrollProps.hasMore &&
1178
+ !this.isLoadingMore) {
1179
+ this.isLoadingMore = true;
1180
+ this.api.forceUpdate(); // Trigger re-render to show loading component
1181
+ yield ((_d = (_c = this.api.infiniteScrollProps).onLoadMore) === null || _d === void 0 ? void 0 : _d.call(_c, this.api.getData()));
1182
+ this.isLoadingMore = false;
1183
+ this.api.forceUpdate(); // Trigger re-render to hide loading component
1184
+ }
1185
+ });
1186
+ this.processData = (data) => {
1187
+ // This plugin doesn't modify the data directly, but rather triggers loading more.
1188
+ // The main component's data prop should be updated by the consumer of the table.
1189
+ return data;
1190
+ };
1191
+ this.renderFooter = () => {
1192
+ if (!this.api.infiniteScrollProps) {
1193
+ return null;
1194
+ }
1195
+ if (this.isLoadingMore) {
1196
+ return this.api.infiniteScrollProps.loadingMoreComponent || React.createElement("div", null, "Loading more...");
1197
+ }
1198
+ else if (!this.api.infiniteScrollProps.hasMore) {
1199
+ return this.api.infiniteScrollProps.noMoreDataComponent || React.createElement("div", null, "No more data.");
1200
+ }
1201
+ return null;
1202
+ };
1203
+ }
1204
+ }
1205
+
1206
+ export { FilterPlugin, InfiniteScrollPlugin, SelectionPlugin, SortPlugin, ResponsiveTable as default };
1207
+ //# sourceMappingURL=index.es.js.map