@keenthemes/ktui 1.1.5 → 1.1.6

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.
Files changed (98) hide show
  1. package/dist/ktui.js +11232 -11095
  2. package/dist/ktui.min.js +1 -1
  3. package/dist/ktui.min.js.map +1 -1
  4. package/dist/styles.css +33 -27
  5. package/lib/cjs/components/collapse/collapse.js +0 -2
  6. package/lib/cjs/components/collapse/collapse.js.map +1 -1
  7. package/lib/cjs/components/component.js +3 -1
  8. package/lib/cjs/components/component.js.map +1 -1
  9. package/lib/cjs/components/datatable/datatable-sort.js +1 -2
  10. package/lib/cjs/components/datatable/datatable-sort.js.map +1 -1
  11. package/lib/cjs/components/datatable/datatable.js +45 -23
  12. package/lib/cjs/components/datatable/datatable.js.map +1 -1
  13. package/lib/cjs/components/drawer/drawer.js +21 -9
  14. package/lib/cjs/components/drawer/drawer.js.map +1 -1
  15. package/lib/cjs/components/dropdown/dropdown.js.map +1 -1
  16. package/lib/cjs/components/scrollto/scrollto.js +0 -2
  17. package/lib/cjs/components/scrollto/scrollto.js.map +1 -1
  18. package/lib/cjs/components/select/combobox.js.map +1 -1
  19. package/lib/cjs/components/select/dropdown.js.map +1 -1
  20. package/lib/cjs/components/select/remote.js.map +1 -1
  21. package/lib/cjs/components/select/search.js +9 -5
  22. package/lib/cjs/components/select/search.js.map +1 -1
  23. package/lib/cjs/components/select/select.js +22 -5
  24. package/lib/cjs/components/select/select.js.map +1 -1
  25. package/lib/cjs/components/select/tags.js.map +1 -1
  26. package/lib/cjs/components/select/templates.js.map +1 -1
  27. package/lib/cjs/components/select/utils.js +10 -0
  28. package/lib/cjs/components/select/utils.js.map +1 -1
  29. package/lib/cjs/components/sticky/sticky.js +104 -24
  30. package/lib/cjs/components/sticky/sticky.js.map +1 -1
  31. package/lib/cjs/components/theme-switch/theme-switch.js +0 -2
  32. package/lib/cjs/components/theme-switch/theme-switch.js.map +1 -1
  33. package/lib/cjs/components/toast/toast.js +1 -2
  34. package/lib/cjs/components/toast/toast.js.map +1 -1
  35. package/lib/cjs/helpers/dom.js +0 -2
  36. package/lib/cjs/helpers/dom.js.map +1 -1
  37. package/lib/esm/components/collapse/collapse.js +0 -2
  38. package/lib/esm/components/collapse/collapse.js.map +1 -1
  39. package/lib/esm/components/component.js +3 -1
  40. package/lib/esm/components/component.js.map +1 -1
  41. package/lib/esm/components/datatable/datatable-sort.js +1 -2
  42. package/lib/esm/components/datatable/datatable-sort.js.map +1 -1
  43. package/lib/esm/components/datatable/datatable.js +45 -23
  44. package/lib/esm/components/datatable/datatable.js.map +1 -1
  45. package/lib/esm/components/drawer/drawer.js +21 -9
  46. package/lib/esm/components/drawer/drawer.js.map +1 -1
  47. package/lib/esm/components/dropdown/dropdown.js.map +1 -1
  48. package/lib/esm/components/scrollto/scrollto.js +0 -2
  49. package/lib/esm/components/scrollto/scrollto.js.map +1 -1
  50. package/lib/esm/components/select/combobox.js.map +1 -1
  51. package/lib/esm/components/select/dropdown.js.map +1 -1
  52. package/lib/esm/components/select/remote.js.map +1 -1
  53. package/lib/esm/components/select/search.js +9 -5
  54. package/lib/esm/components/select/search.js.map +1 -1
  55. package/lib/esm/components/select/select.js +22 -5
  56. package/lib/esm/components/select/select.js.map +1 -1
  57. package/lib/esm/components/select/tags.js.map +1 -1
  58. package/lib/esm/components/select/templates.js.map +1 -1
  59. package/lib/esm/components/select/utils.js +10 -0
  60. package/lib/esm/components/select/utils.js.map +1 -1
  61. package/lib/esm/components/sticky/sticky.js +104 -24
  62. package/lib/esm/components/sticky/sticky.js.map +1 -1
  63. package/lib/esm/components/theme-switch/theme-switch.js +0 -2
  64. package/lib/esm/components/theme-switch/theme-switch.js.map +1 -1
  65. package/lib/esm/components/toast/toast.js +1 -2
  66. package/lib/esm/components/toast/toast.js.map +1 -1
  67. package/lib/esm/helpers/dom.js +0 -2
  68. package/lib/esm/helpers/dom.js.map +1 -1
  69. package/package.json +14 -7
  70. package/src/components/collapse/collapse.ts +0 -3
  71. package/src/components/component.ts +5 -5
  72. package/src/components/datatable/__tests__/currency-sort.test.ts +108 -0
  73. package/src/components/datatable/__tests__/multi-row-headers.test.ts +121 -0
  74. package/src/components/datatable/__tests__/pagination-reset.test.ts +13 -5
  75. package/src/components/datatable/__tests__/race-conditions.test.ts +138 -78
  76. package/src/components/datatable/__tests__/setup.ts +9 -4
  77. package/src/components/datatable/datatable-sort.ts +12 -16
  78. package/src/components/datatable/datatable.css +4 -4
  79. package/src/components/datatable/datatable.ts +56 -26
  80. package/src/components/datatable/types.ts +3 -1
  81. package/src/components/drawer/drawer.ts +61 -24
  82. package/src/components/dropdown/dropdown.ts +3 -1
  83. package/src/components/scrollto/scrollto.ts +0 -3
  84. package/src/components/select/__tests__/ux-behaviors.test.ts +274 -8
  85. package/src/components/select/combobox.ts +0 -1
  86. package/src/components/select/dropdown.ts +0 -2
  87. package/src/components/select/remote.ts +1 -6
  88. package/src/components/select/search.ts +14 -7
  89. package/src/components/select/select.ts +29 -29
  90. package/src/components/select/tags.ts +0 -1
  91. package/src/components/select/templates.ts +8 -8
  92. package/src/components/select/utils.ts +15 -2
  93. package/src/components/sticky/__tests__/sticky.test.ts +205 -0
  94. package/src/components/sticky/sticky.ts +119 -21
  95. package/src/components/sticky/types.ts +3 -0
  96. package/src/components/theme-switch/theme-switch.ts +0 -3
  97. package/src/components/toast/toast.ts +3 -2
  98. package/src/helpers/dom.ts +0 -3
@@ -3,7 +3,15 @@
3
3
  * Tests the fixes for concurrent request handling, request cancellation, and stale response detection
4
4
  */
5
5
 
6
- import { describe, it, expect, beforeEach, afterEach, vi, type MockedFunction } from 'vitest';
6
+ import {
7
+ describe,
8
+ it,
9
+ expect,
10
+ beforeEach,
11
+ afterEach,
12
+ vi,
13
+ type MockedFunction,
14
+ } from 'vitest';
7
15
  import { KTDataTable } from '../datatable';
8
16
  import { waitFor } from './setup';
9
17
 
@@ -45,7 +53,9 @@ describe('KTDataTable Race Condition Fixes', () => {
45
53
  return new Promise<Response>((resolve, reject) => {
46
54
  const timeout = setTimeout(() => {
47
55
  if (options?.signal?.aborted) {
48
- reject(new DOMException('The operation was aborted.', 'AbortError'));
56
+ reject(
57
+ new DOMException('The operation was aborted.', 'AbortError'),
58
+ );
49
59
  } else {
50
60
  resolve(
51
61
  new Response(
@@ -56,7 +66,10 @@ describe('KTDataTable Race Condition Fixes', () => {
56
66
  ],
57
67
  totalCount: 2,
58
68
  }),
59
- { status: 200, headers: { 'Content-Type': 'application/json' } },
69
+ {
70
+ status: 200,
71
+ headers: { 'Content-Type': 'application/json' },
72
+ },
60
73
  ),
61
74
  );
62
75
  }
@@ -66,7 +79,9 @@ describe('KTDataTable Race Condition Fixes', () => {
66
79
  if (options?.signal) {
67
80
  options.signal.addEventListener('abort', () => {
68
81
  clearTimeout(timeout);
69
- reject(new DOMException('The operation was aborted.', 'AbortError'));
82
+ reject(
83
+ new DOMException('The operation was aborted.', 'AbortError'),
84
+ );
70
85
  });
71
86
  }
72
87
  });
@@ -83,9 +98,12 @@ describe('KTDataTable Race Condition Fixes', () => {
83
98
 
84
99
  describe('AbortController Integration', () => {
85
100
  it('should create AbortController for remote data requests', async () => {
86
- const datatable = new KTDataTable(container.querySelector('[data-kt-datatable="true"]')!, {
87
- apiEndpoint: '/api/data',
88
- });
101
+ const datatable = new KTDataTable(
102
+ container.querySelector('[data-kt-datatable="true"]')!,
103
+ {
104
+ apiEndpoint: '/api/data',
105
+ },
106
+ );
89
107
 
90
108
  await waitFor(150);
91
109
 
@@ -95,9 +113,12 @@ describe('KTDataTable Race Condition Fixes', () => {
95
113
  });
96
114
 
97
115
  it('should use _isFetching flag to prevent concurrent requests', async () => {
98
- const datatable = new KTDataTable(container.querySelector('[data-kt-datatable="true"]')!, {
99
- apiEndpoint: '/api/data',
100
- });
116
+ const datatable = new KTDataTable(
117
+ container.querySelector('[data-kt-datatable="true"]')!,
118
+ {
119
+ apiEndpoint: '/api/data',
120
+ },
121
+ );
101
122
 
102
123
  // Try to trigger search during initial fetch
103
124
  datatable.search('test'); // Should be blocked by _isFetching
@@ -109,9 +130,12 @@ describe('KTDataTable Race Condition Fixes', () => {
109
130
  });
110
131
 
111
132
  it('should allow new request after previous completes', async () => {
112
- const datatable = new KTDataTable(container.querySelector('[data-kt-datatable="true"]')!, {
113
- apiEndpoint: '/api/data',
114
- });
133
+ const datatable = new KTDataTable(
134
+ container.querySelector('[data-kt-datatable="true"]')!,
135
+ {
136
+ apiEndpoint: '/api/data',
137
+ },
138
+ );
115
139
 
116
140
  // Wait for initial fetch to complete
117
141
  await waitFor(150);
@@ -127,9 +151,12 @@ describe('KTDataTable Race Condition Fixes', () => {
127
151
 
128
152
  it('should abort previous request when _performFetchRequest is called again', async () => {
129
153
  // This tests the AbortController logic directly by making multiple sequential requests
130
- const datatable = new KTDataTable(container.querySelector('[data-kt-datatable="true"]')!, {
131
- apiEndpoint: '/api/data',
132
- });
154
+ const datatable = new KTDataTable(
155
+ container.querySelector('[data-kt-datatable="true"]')!,
156
+ {
157
+ apiEndpoint: '/api/data',
158
+ },
159
+ );
133
160
 
134
161
  await waitFor(150); // Complete initial request
135
162
 
@@ -154,33 +181,38 @@ describe('KTDataTable Race Condition Fixes', () => {
154
181
 
155
182
  describe('Request ID Sequencing', () => {
156
183
  it('should assign incremental request IDs for sequential requests', async () => {
157
- let requestIds: number[] = [];
184
+ const requestIds: number[] = [];
158
185
  let callCount = 0;
159
186
 
160
187
  // Mock to capture request sequence
161
- mockFetch.mockImplementation((url: RequestInfo | URL, options?: RequestInit) => {
162
- callCount++;
163
- const id = callCount;
164
- requestIds.push(id);
165
-
166
- return new Promise((resolve) => {
167
- setTimeout(() => {
168
- resolve(
169
- new Response(
170
- JSON.stringify({
171
- data: [{ id: id, name: `Item ${id}` }],
172
- totalCount: 1,
173
- }),
174
- { status: 200 },
175
- ),
176
- );
177
- }, 50);
178
- });
179
- });
188
+ mockFetch.mockImplementation(
189
+ (url: RequestInfo | URL, options?: RequestInit) => {
190
+ callCount++;
191
+ const id = callCount;
192
+ requestIds.push(id);
193
+
194
+ return new Promise((resolve) => {
195
+ setTimeout(() => {
196
+ resolve(
197
+ new Response(
198
+ JSON.stringify({
199
+ data: [{ id: id, name: `Item ${id}` }],
200
+ totalCount: 1,
201
+ }),
202
+ { status: 200 },
203
+ ),
204
+ );
205
+ }, 50);
206
+ });
207
+ },
208
+ );
180
209
 
181
- const datatable = new KTDataTable(container.querySelector('[data-kt-datatable="true"]')!, {
182
- apiEndpoint: '/api/data',
183
- });
210
+ const datatable = new KTDataTable(
211
+ container.querySelector('[data-kt-datatable="true"]')!,
212
+ {
213
+ apiEndpoint: '/api/data',
214
+ },
215
+ );
184
216
 
185
217
  await waitFor(100); // Complete initial request
186
218
 
@@ -197,9 +229,12 @@ describe('KTDataTable Race Condition Fixes', () => {
197
229
  it('should have request ID validation logic in place', async () => {
198
230
  // This tests that request IDs are tracked internally
199
231
  // The actual stale response scenario is prevented by _isFetching flag
200
- const datatable = new KTDataTable(container.querySelector('[data-kt-datatable="true"]')!, {
201
- apiEndpoint: '/api/data',
202
- });
232
+ const datatable = new KTDataTable(
233
+ container.querySelector('[data-kt-datatable="true"]')!,
234
+ {
235
+ apiEndpoint: '/api/data',
236
+ },
237
+ );
203
238
 
204
239
  await waitFor(150); // Complete initial
205
240
 
@@ -220,9 +255,12 @@ describe('KTDataTable Race Condition Fixes', () => {
220
255
 
221
256
  describe('_isFetching Flag Management', () => {
222
257
  it('should prevent concurrent fetch executions', async () => {
223
- const datatable = new KTDataTable(container.querySelector('[data-kt-datatable="true"]')!, {
224
- apiEndpoint: '/api/data',
225
- });
258
+ const datatable = new KTDataTable(
259
+ container.querySelector('[data-kt-datatable="true"]')!,
260
+ {
261
+ apiEndpoint: '/api/data',
262
+ },
263
+ );
226
264
 
227
265
  // Try to trigger reload immediately (should be blocked by initial fetch)
228
266
  datatable.reload(); // Blocked by _isFetching
@@ -236,9 +274,12 @@ describe('KTDataTable Race Condition Fixes', () => {
236
274
  });
237
275
 
238
276
  it('should reset _isFetching flag after fetch completes', async () => {
239
- const datatable = new KTDataTable(container.querySelector('[data-kt-datatable="true"]')!, {
240
- apiEndpoint: '/api/data',
241
- });
277
+ const datatable = new KTDataTable(
278
+ container.querySelector('[data-kt-datatable="true"]')!,
279
+ {
280
+ apiEndpoint: '/api/data',
281
+ },
282
+ );
242
283
 
243
284
  await waitFor(150); // Wait for initial fetch
244
285
 
@@ -251,28 +292,31 @@ describe('KTDataTable Race Condition Fixes', () => {
251
292
 
252
293
  it('should reset _isFetching flag even after fetch error', async () => {
253
294
  let callCount = 0;
254
- mockFetch.mockImplementation((url: RequestInfo | URL, options?: RequestInit) => {
255
- callCount++;
256
- if (callCount === 1) {
257
- // Return invalid JSON to trigger parse error
295
+ mockFetch.mockImplementation(
296
+ (url: RequestInfo | URL, options?: RequestInit) => {
297
+ callCount++;
298
+ if (callCount === 1) {
299
+ // Return invalid JSON to trigger parse error
300
+ return Promise.resolve(new Response('Not JSON', { status: 200 }));
301
+ }
258
302
  return Promise.resolve(
259
- new Response('Not JSON', { status: 200 }),
303
+ new Response(
304
+ JSON.stringify({
305
+ data: [{ id: 1, name: 'Success' }],
306
+ totalCount: 1,
307
+ }),
308
+ { status: 200 },
309
+ ),
260
310
  );
261
- }
262
- return Promise.resolve(
263
- new Response(
264
- JSON.stringify({
265
- data: [{ id: 1, name: 'Success' }],
266
- totalCount: 1,
267
- }),
268
- { status: 200 },
269
- ),
270
- );
271
- });
311
+ },
312
+ );
272
313
 
273
- const datatable = new KTDataTable(container.querySelector('[data-kt-datatable="true"]')!, {
274
- apiEndpoint: '/api/data',
275
- });
314
+ const datatable = new KTDataTable(
315
+ container.querySelector('[data-kt-datatable="true"]')!,
316
+ {
317
+ apiEndpoint: '/api/data',
318
+ },
319
+ );
276
320
 
277
321
  await waitFor(150); // Initial request triggers parse error
278
322
 
@@ -286,7 +330,9 @@ describe('KTDataTable Race Condition Fixes', () => {
286
330
 
287
331
  describe('Loading Spinner Management', () => {
288
332
  it('should show spinner during fetch', async () => {
289
- const element = container.querySelector('[data-kt-datatable="true"]') as HTMLElement;
333
+ const element = container.querySelector(
334
+ '[data-kt-datatable="true"]',
335
+ ) as HTMLElement;
290
336
  const datatable = new KTDataTable(element, {
291
337
  apiEndpoint: '/api/data',
292
338
  });
@@ -301,7 +347,9 @@ describe('KTDataTable Race Condition Fixes', () => {
301
347
  });
302
348
 
303
349
  it('should keep spinner visible during overlapping requests', async () => {
304
- const element = container.querySelector('[data-kt-datatable="true"]') as HTMLElement;
350
+ const element = container.querySelector(
351
+ '[data-kt-datatable="true"]',
352
+ ) as HTMLElement;
305
353
  const datatable = new KTDataTable(element, {
306
354
  apiEndpoint: '/api/data',
307
355
  });
@@ -323,7 +371,9 @@ describe('KTDataTable Race Condition Fixes', () => {
323
371
  });
324
372
 
325
373
  it('should not flicker spinner during rapid interactions', async () => {
326
- const element = container.querySelector('[data-kt-datatable="true"]') as HTMLElement;
374
+ const element = container.querySelector(
375
+ '[data-kt-datatable="true"]',
376
+ ) as HTMLElement;
327
377
  const datatable = new KTDataTable(element, {
328
378
  apiEndpoint: '/api/data',
329
379
  });
@@ -362,7 +412,9 @@ describe('KTDataTable Race Condition Fixes', () => {
362
412
  it('should fire fetch event for successful requests', async () => {
363
413
  const fetchEvents: any[] = [];
364
414
 
365
- const element = container.querySelector('[data-kt-datatable="true"]') as HTMLElement;
415
+ const element = container.querySelector(
416
+ '[data-kt-datatable="true"]',
417
+ ) as HTMLElement;
366
418
  element.addEventListener('fetch', (e) => {
367
419
  fetchEvents.push(e);
368
420
  });
@@ -383,7 +435,9 @@ describe('KTDataTable Race Condition Fixes', () => {
383
435
  it('should fire fetched event after successful data load', async () => {
384
436
  const fetchedEvents: any[] = [];
385
437
 
386
- const element = container.querySelector('[data-kt-datatable="true"]') as HTMLElement;
438
+ const element = container.querySelector(
439
+ '[data-kt-datatable="true"]',
440
+ ) as HTMLElement;
387
441
  element.addEventListener('fetched', (e) => {
388
442
  fetchedEvents.push(e);
389
443
  });
@@ -401,7 +455,9 @@ describe('KTDataTable Race Condition Fixes', () => {
401
455
  it('should not fire error events for AbortError', async () => {
402
456
  const errorEvents: any[] = [];
403
457
 
404
- const element = container.querySelector('[data-kt-datatable="true"]') as HTMLElement;
458
+ const element = container.querySelector(
459
+ '[data-kt-datatable="true"]',
460
+ ) as HTMLElement;
405
461
  element.addEventListener('error.kt.datatable', (e) => {
406
462
  errorEvents.push(e);
407
463
  });
@@ -422,7 +478,9 @@ describe('KTDataTable Race Condition Fixes', () => {
422
478
 
423
479
  describe('Backward Compatibility', () => {
424
480
  it('should work with local data mode (no AbortController needed)', async () => {
425
- const datatable = new KTDataTable(container.querySelector('[data-kt-datatable="true"]')!);
481
+ const datatable = new KTDataTable(
482
+ container.querySelector('[data-kt-datatable="true"]')!,
483
+ );
426
484
 
427
485
  // Should not call fetch for local data
428
486
  expect(mockFetch).not.toHaveBeenCalled();
@@ -434,9 +492,12 @@ describe('KTDataTable Race Condition Fixes', () => {
434
492
  });
435
493
 
436
494
  it('should maintain existing API compatibility', async () => {
437
- const datatable = new KTDataTable(container.querySelector('[data-kt-datatable="true"]')!, {
438
- apiEndpoint: '/api/data',
439
- });
495
+ const datatable = new KTDataTable(
496
+ container.querySelector('[data-kt-datatable="true"]')!,
497
+ {
498
+ apiEndpoint: '/api/data',
499
+ },
500
+ );
440
501
 
441
502
  await waitFor(150);
442
503
 
@@ -452,4 +513,3 @@ describe('KTDataTable Race Condition Fixes', () => {
452
513
  });
453
514
  });
454
515
  });
455
-
@@ -45,7 +45,9 @@ Object.defineProperty(window, 'matchMedia', {
45
45
  value: (query: string) => ({
46
46
  matches: false,
47
47
  media: query,
48
- onchange: null as ((this: MediaQueryList, ev: MediaQueryListEvent) => any) | null,
48
+ onchange: null as
49
+ | ((this: MediaQueryList, ev: MediaQueryListEvent) => any)
50
+ | null,
49
51
  addListener: () => {}, // deprecated
50
52
  removeListener: () => {}, // deprecated
51
53
  addEventListener: () => {},
@@ -55,13 +57,16 @@ Object.defineProperty(window, 'matchMedia', {
55
57
  });
56
58
 
57
59
  // Export utilities that tests can use
58
- export const waitFor = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
60
+ export const waitFor = (ms: number) =>
61
+ new Promise((resolve) => setTimeout(resolve, ms));
59
62
 
60
- export const createMockElement = (tag: string, attributes: Record<string, string> = {}) => {
63
+ export const createMockElement = (
64
+ tag: string,
65
+ attributes: Record<string, string> = {},
66
+ ) => {
61
67
  const el = document.createElement(tag);
62
68
  Object.entries(attributes).forEach(([key, value]) => {
63
69
  el.setAttribute(key, value);
64
70
  });
65
71
  return el;
66
72
  };
67
-
@@ -89,15 +89,15 @@ export function createSortHandler<T = KTDataTableDataInterface>(
89
89
  return 0;
90
90
  }
91
91
 
92
- function getColumnDef(
93
- sortField: keyof T | number,
94
- ): {
95
- sortType?: 'string' | 'numeric';
96
- sortValue?: (
97
- cellValue: unknown,
98
- rowData: KTDataTableDataInterface,
99
- ) => number | string;
100
- } | undefined {
92
+ function getColumnDef(sortField: keyof T | number):
93
+ | {
94
+ sortType?: 'string' | 'numeric';
95
+ sortValue?: (
96
+ cellValue: unknown,
97
+ rowData: KTDataTableDataInterface,
98
+ ) => number | string;
99
+ }
100
+ | undefined {
101
101
  const columns = config.columns;
102
102
  if (!columns) return undefined;
103
103
  const key =
@@ -114,8 +114,7 @@ export function createSortHandler<T = KTDataTableDataInterface>(
114
114
  ): T[] {
115
115
  const columnDef = getColumnDef(sortField);
116
116
  const sortValueFn = columnDef?.sortValue;
117
- const useNumeric =
118
- !sortValueFn && columnDef?.sortType === 'numeric';
117
+ const useNumeric = !sortValueFn && columnDef?.sortType === 'numeric';
119
118
 
120
119
  return data.sort((a, b) => {
121
120
  const aRaw = a[sortField as keyof T] as unknown;
@@ -186,12 +185,9 @@ export function createSortHandler<T = KTDataTableDataInterface>(
186
185
  `th[data-kt-datatable-column="${String(sortField)}"], th[data-kt-datatable-column-sort="${String(sortField)}"]`,
187
186
  ) as HTMLElement);
188
187
  if (th) {
189
- const sortElement = th.querySelector(
190
- `.${baseClass}`,
191
- ) as HTMLElement;
188
+ const sortElement = th.querySelector(`.${baseClass}`) as HTMLElement;
192
189
  if (sortElement) {
193
- sortElement.className =
194
- `${baseClass} ${sortClass}`.trim();
190
+ sortElement.className = `${baseClass} ${sortClass}`.trim();
195
191
  }
196
192
  if (sortOrder) {
197
193
  th.setAttribute('aria-sort', sortOrder);
@@ -92,19 +92,19 @@
92
92
  }
93
93
 
94
94
  @custom-variant kt-datatable-sort-asc {
95
- &.[data-kt-datatable-column-sort='asc']:where([data-kt-datatable] *) {
95
+ &.[aria-sort='asc']:where([data-kt-datatable] *) {
96
96
  @slot;
97
97
  }
98
- [data-kt-datatable] [data-kt-datatable-column-sort='asc'] & {
98
+ [data-kt-datatable] [aria-sort='asc'] & {
99
99
  @slot;
100
100
  }
101
101
  }
102
102
 
103
103
  @custom-variant kt-datatable-sort-desc {
104
- &.[data-kt-datatable-column-sort='desc']:where([data-kt-datatable] *) {
104
+ &.[aria-sort='desc']:where([data-kt-datatable] *) {
105
105
  @slot;
106
106
  }
107
- [data-kt-datatable] [data-kt-datatable-column-sort='desc'] & {
107
+ [data-kt-datatable] [aria-sort='desc'] & {
108
108
  @slot;
109
109
  }
110
110
  }
@@ -681,8 +681,8 @@ export class KTDataTable<T extends KTDataTableDataInterface>
681
681
  ? this._theadElement.querySelectorAll('th')
682
682
  : ([] as unknown as NodeListOf<HTMLTableCellElement>);
683
683
 
684
- const ths: HTMLTableCellElement[] = Array.from(allThs).filter(th =>
685
- th.hasAttribute('data-kt-datatable-column')
684
+ const ths: HTMLTableCellElement[] = Array.from(allThs).filter((th) =>
685
+ th.hasAttribute('data-kt-datatable-column'),
686
686
  );
687
687
 
688
688
  rows.forEach((row: HTMLTableRowElement) => {
@@ -716,21 +716,46 @@ export class KTDataTable<T extends KTDataTableDataInterface>
716
716
  private _localTableHeaderInvalidate(): boolean {
717
717
  const { originalData } = this.getState();
718
718
 
719
- // Count only th elements with data-kt-datatable-column attribute
720
- const allThs: NodeListOf<HTMLTableCellElement> = this._theadElement
721
- ? this._theadElement.querySelectorAll('th')
722
- : ([] as unknown as NodeListOf<HTMLTableCellElement>);
723
- const currentTableHeaders = Array.from(allThs).filter(th =>
724
- th.hasAttribute('data-kt-datatable-column')
725
- ).length;
726
-
727
719
  const totalColumns = originalData.length
728
720
  ? Object.keys(originalData[0]).length
729
721
  : 0;
730
722
 
723
+ // Count th elements with data-kt-datatable-column; when none (e.g. multi-row headers), use logical column count so we don't falsely invalidate
724
+ const allThs: NodeListOf<HTMLTableCellElement> = this._theadElement
725
+ ? this._theadElement.querySelectorAll('th')
726
+ : ([] as unknown as NodeListOf<HTMLTableCellElement>);
727
+ const thsWithColumn = Array.from(allThs).filter((th) =>
728
+ th.hasAttribute('data-kt-datatable-column'),
729
+ );
730
+ const currentTableHeaders =
731
+ thsWithColumn.length > 0
732
+ ? thsWithColumn.length
733
+ : this._getLogicalColumnCount();
734
+
731
735
  return currentTableHeaders !== totalColumns;
732
736
  }
733
737
 
738
+ /**
739
+ * Returns the logical data column count (number of data columns), used for multi-row headers
740
+ * where querySelectorAll('th') would overcount. Prefers state.originalData, then first tbody row td count.
741
+ * @returns {number} Number of data columns, or 0 if unknown
742
+ */
743
+ private _getLogicalColumnCount(): number {
744
+ const { originalData } = this.getState();
745
+ if (originalData && originalData.length > 0) {
746
+ return Object.keys(originalData[0]).length;
747
+ }
748
+ if (this._tbodyElement) {
749
+ const firstRow = this._tbodyElement.querySelector<HTMLTableRowElement>(
750
+ 'tr',
751
+ );
752
+ if (firstRow) {
753
+ return firstRow.querySelectorAll<HTMLTableCellElement>('td').length;
754
+ }
755
+ }
756
+ return 0;
757
+ }
758
+
734
759
  /**
735
760
  * Fetch data from the server
736
761
  */
@@ -770,13 +795,13 @@ export class KTDataTable<T extends KTDataTableDataInterface>
770
795
  response,
771
796
  error: String(error),
772
797
  status: response.status,
773
- statusText: response.statusText
798
+ statusText: response.statusText,
774
799
  });
775
800
  this._dispatchEvent('parseError', {
776
801
  response,
777
802
  error: String(error),
778
803
  status: response.status,
779
- statusText: response.statusText
804
+ statusText: response.statusText,
780
805
  });
781
806
  return;
782
807
  }
@@ -862,7 +887,7 @@ export class KTDataTable<T extends KTDataTableDataInterface>
862
887
  private async _performFetchRequest(
863
888
  queryParams: URLSearchParams,
864
889
  ): Promise<Response> {
865
- let requestMethod: RequestInit['method'] = this._config.requestMethod;
890
+ const requestMethod: RequestInit['method'] = this._config.requestMethod;
866
891
  let requestBody: RequestInit['body'] | undefined = undefined;
867
892
 
868
893
  // Cancel previous request to prevent race conditions
@@ -1023,11 +1048,14 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1023
1048
  ? this._theadElement.querySelectorAll('th')
1024
1049
  : ([] as unknown as NodeListOf<HTMLTableCellElement>);
1025
1050
 
1026
- const ths: HTMLTableCellElement[] = Array.from(allThs).filter(th =>
1027
- th.hasAttribute('data-kt-datatable-column')
1051
+ const ths: HTMLTableCellElement[] = Array.from(allThs).filter((th) =>
1052
+ th.hasAttribute('data-kt-datatable-column'),
1028
1053
  );
1029
- // When no th has data-kt-datatable-column, use all ths so we still render by column index (data extracted with numeric keys)
1030
- const columnsToRender: HTMLTableCellElement[] = ths.length > 0 ? ths : Array.from(allThs);
1054
+ // When no th has data-kt-datatable-column (e.g. multi-row headers), use logical column count from tbody so we don't overcount thead cells
1055
+ const columnsToRender: HTMLTableCellElement[] =
1056
+ ths.length > 0 ? ths : [];
1057
+ const logicalColumnCount =
1058
+ ths.length > 0 ? ths.length : this._getLogicalColumnCount();
1031
1059
 
1032
1060
  this._data.forEach((item: T, rowIndex: number) => {
1033
1061
  const row = document.createElement('tr');
@@ -1042,9 +1070,9 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1042
1070
  ? this.getState().originalDataAttributes[rowIndex]
1043
1071
  : null;
1044
1072
 
1045
- // Use columnsToRender so tables without data-kt-datatable-column still get cells (by index)
1046
- columnsToRender.forEach((th, colIndex) => {
1047
- const colName = th.getAttribute('data-kt-datatable-column');
1073
+ for (let colIndex = 0; colIndex < logicalColumnCount; colIndex++) {
1074
+ const th = columnsToRender[colIndex];
1075
+ const colName = th?.getAttribute('data-kt-datatable-column');
1048
1076
  const td = document.createElement('td');
1049
1077
  let value: any;
1050
1078
  if (colName && Object.prototype.hasOwnProperty.call(item, colName)) {
@@ -1072,7 +1100,7 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1072
1100
  }
1073
1101
 
1074
1102
  row.appendChild(td);
1075
- });
1103
+ }
1076
1104
  } else {
1077
1105
  Object.keys(this._config.columns).forEach(
1078
1106
  (key: keyof T, colIndex: number) => {
@@ -1126,9 +1154,9 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1126
1154
  private _noticeOnTable(message: string = ''): void {
1127
1155
  const row = this._tableElement.tBodies[0].insertRow();
1128
1156
  const cell = row.insertCell();
1129
- cell.colSpan = this._theadElement
1130
- ? this._theadElement.querySelectorAll('th').length
1131
- : 0;
1157
+ const logicalCount = this._getLogicalColumnCount();
1158
+ // Use logical column count so multi-row headers don't overcount; fallback to 1 when 0 so message still displays
1159
+ cell.colSpan = logicalCount > 0 ? logicalCount : 1;
1132
1160
  cell.innerHTML = message;
1133
1161
  }
1134
1162
 
@@ -1481,7 +1509,7 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1481
1509
  const state = JSON.parse(stateString) as KTDataTableStateInterface;
1482
1510
  if (state) this._config._state = state;
1483
1511
  return state;
1484
- } catch {} // eslint-disable-line no-empty
1512
+ } catch {}
1485
1513
 
1486
1514
  return null;
1487
1515
  }
@@ -1829,7 +1857,9 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1829
1857
  */
1830
1858
  public static reinit(): void {
1831
1859
  if (typeof document === 'undefined') return;
1832
- const elements = document.querySelectorAll<HTMLElement>('[data-kt-datatable="true"]');
1860
+ const elements = document.querySelectorAll<HTMLElement>(
1861
+ '[data-kt-datatable="true"]',
1862
+ );
1833
1863
  elements.forEach((element) => {
1834
1864
  try {
1835
1865
  const instance = KTDataTable.getInstance(element);
@@ -112,7 +112,9 @@ export interface KTDataTableConfigInterface {
112
112
  * Use for custom formats (e.g. dates, combined fields, custom parsing).
113
113
  */
114
114
  sortValue?: (
115
- cellValue: KTDataTableDataInterface[keyof KTDataTableDataInterface] | string,
115
+ cellValue:
116
+ | KTDataTableDataInterface[keyof KTDataTableDataInterface]
117
+ | string,
116
118
  rowData: KTDataTableDataInterface,
117
119
  ) => number | string;
118
120
  };