@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.
- package/dist/ktui.js +11232 -11095
- package/dist/ktui.min.js +1 -1
- package/dist/ktui.min.js.map +1 -1
- package/dist/styles.css +33 -27
- package/lib/cjs/components/collapse/collapse.js +0 -2
- package/lib/cjs/components/collapse/collapse.js.map +1 -1
- package/lib/cjs/components/component.js +3 -1
- package/lib/cjs/components/component.js.map +1 -1
- package/lib/cjs/components/datatable/datatable-sort.js +1 -2
- package/lib/cjs/components/datatable/datatable-sort.js.map +1 -1
- package/lib/cjs/components/datatable/datatable.js +45 -23
- package/lib/cjs/components/datatable/datatable.js.map +1 -1
- package/lib/cjs/components/drawer/drawer.js +21 -9
- package/lib/cjs/components/drawer/drawer.js.map +1 -1
- package/lib/cjs/components/dropdown/dropdown.js.map +1 -1
- package/lib/cjs/components/scrollto/scrollto.js +0 -2
- package/lib/cjs/components/scrollto/scrollto.js.map +1 -1
- package/lib/cjs/components/select/combobox.js.map +1 -1
- package/lib/cjs/components/select/dropdown.js.map +1 -1
- package/lib/cjs/components/select/remote.js.map +1 -1
- package/lib/cjs/components/select/search.js +9 -5
- package/lib/cjs/components/select/search.js.map +1 -1
- package/lib/cjs/components/select/select.js +22 -5
- package/lib/cjs/components/select/select.js.map +1 -1
- package/lib/cjs/components/select/tags.js.map +1 -1
- package/lib/cjs/components/select/templates.js.map +1 -1
- package/lib/cjs/components/select/utils.js +10 -0
- package/lib/cjs/components/select/utils.js.map +1 -1
- package/lib/cjs/components/sticky/sticky.js +104 -24
- package/lib/cjs/components/sticky/sticky.js.map +1 -1
- package/lib/cjs/components/theme-switch/theme-switch.js +0 -2
- package/lib/cjs/components/theme-switch/theme-switch.js.map +1 -1
- package/lib/cjs/components/toast/toast.js +1 -2
- package/lib/cjs/components/toast/toast.js.map +1 -1
- package/lib/cjs/helpers/dom.js +0 -2
- package/lib/cjs/helpers/dom.js.map +1 -1
- package/lib/esm/components/collapse/collapse.js +0 -2
- package/lib/esm/components/collapse/collapse.js.map +1 -1
- package/lib/esm/components/component.js +3 -1
- package/lib/esm/components/component.js.map +1 -1
- package/lib/esm/components/datatable/datatable-sort.js +1 -2
- package/lib/esm/components/datatable/datatable-sort.js.map +1 -1
- package/lib/esm/components/datatable/datatable.js +45 -23
- package/lib/esm/components/datatable/datatable.js.map +1 -1
- package/lib/esm/components/drawer/drawer.js +21 -9
- package/lib/esm/components/drawer/drawer.js.map +1 -1
- package/lib/esm/components/dropdown/dropdown.js.map +1 -1
- package/lib/esm/components/scrollto/scrollto.js +0 -2
- package/lib/esm/components/scrollto/scrollto.js.map +1 -1
- package/lib/esm/components/select/combobox.js.map +1 -1
- package/lib/esm/components/select/dropdown.js.map +1 -1
- package/lib/esm/components/select/remote.js.map +1 -1
- package/lib/esm/components/select/search.js +9 -5
- package/lib/esm/components/select/search.js.map +1 -1
- package/lib/esm/components/select/select.js +22 -5
- package/lib/esm/components/select/select.js.map +1 -1
- package/lib/esm/components/select/tags.js.map +1 -1
- package/lib/esm/components/select/templates.js.map +1 -1
- package/lib/esm/components/select/utils.js +10 -0
- package/lib/esm/components/select/utils.js.map +1 -1
- package/lib/esm/components/sticky/sticky.js +104 -24
- package/lib/esm/components/sticky/sticky.js.map +1 -1
- package/lib/esm/components/theme-switch/theme-switch.js +0 -2
- package/lib/esm/components/theme-switch/theme-switch.js.map +1 -1
- package/lib/esm/components/toast/toast.js +1 -2
- package/lib/esm/components/toast/toast.js.map +1 -1
- package/lib/esm/helpers/dom.js +0 -2
- package/lib/esm/helpers/dom.js.map +1 -1
- package/package.json +14 -7
- package/src/components/collapse/collapse.ts +0 -3
- package/src/components/component.ts +5 -5
- package/src/components/datatable/__tests__/currency-sort.test.ts +108 -0
- package/src/components/datatable/__tests__/multi-row-headers.test.ts +121 -0
- package/src/components/datatable/__tests__/pagination-reset.test.ts +13 -5
- package/src/components/datatable/__tests__/race-conditions.test.ts +138 -78
- package/src/components/datatable/__tests__/setup.ts +9 -4
- package/src/components/datatable/datatable-sort.ts +12 -16
- package/src/components/datatable/datatable.css +4 -4
- package/src/components/datatable/datatable.ts +56 -26
- package/src/components/datatable/types.ts +3 -1
- package/src/components/drawer/drawer.ts +61 -24
- package/src/components/dropdown/dropdown.ts +3 -1
- package/src/components/scrollto/scrollto.ts +0 -3
- package/src/components/select/__tests__/ux-behaviors.test.ts +274 -8
- package/src/components/select/combobox.ts +0 -1
- package/src/components/select/dropdown.ts +0 -2
- package/src/components/select/remote.ts +1 -6
- package/src/components/select/search.ts +14 -7
- package/src/components/select/select.ts +29 -29
- package/src/components/select/tags.ts +0 -1
- package/src/components/select/templates.ts +8 -8
- package/src/components/select/utils.ts +15 -2
- package/src/components/sticky/__tests__/sticky.test.ts +205 -0
- package/src/components/sticky/sticky.ts +119 -21
- package/src/components/sticky/types.ts +3 -0
- package/src/components/theme-switch/theme-switch.ts +0 -3
- package/src/components/toast/toast.ts +3 -2
- 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 {
|
|
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(
|
|
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
|
-
{
|
|
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(
|
|
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(
|
|
87
|
-
|
|
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(
|
|
99
|
-
|
|
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(
|
|
113
|
-
|
|
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(
|
|
131
|
-
|
|
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
|
-
|
|
184
|
+
const requestIds: number[] = [];
|
|
158
185
|
let callCount = 0;
|
|
159
186
|
|
|
160
187
|
// Mock to capture request sequence
|
|
161
|
-
mockFetch.mockImplementation(
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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(
|
|
182
|
-
|
|
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(
|
|
201
|
-
|
|
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(
|
|
224
|
-
|
|
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(
|
|
240
|
-
|
|
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(
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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(
|
|
303
|
+
new Response(
|
|
304
|
+
JSON.stringify({
|
|
305
|
+
data: [{ id: 1, name: 'Success' }],
|
|
306
|
+
totalCount: 1,
|
|
307
|
+
}),
|
|
308
|
+
{ status: 200 },
|
|
309
|
+
),
|
|
260
310
|
);
|
|
261
|
-
}
|
|
262
|
-
|
|
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(
|
|
274
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
438
|
-
|
|
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
|
|
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) =>
|
|
60
|
+
export const waitFor = (ms: number) =>
|
|
61
|
+
new Promise((resolve) => setTimeout(resolve, ms));
|
|
59
62
|
|
|
60
|
-
export const createMockElement = (
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
&.[
|
|
95
|
+
&.[aria-sort='asc']:where([data-kt-datatable] *) {
|
|
96
96
|
@slot;
|
|
97
97
|
}
|
|
98
|
-
[data-kt-datatable] [
|
|
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
|
-
&.[
|
|
104
|
+
&.[aria-sort='desc']:where([data-kt-datatable] *) {
|
|
105
105
|
@slot;
|
|
106
106
|
}
|
|
107
|
-
[data-kt-datatable] [
|
|
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
|
-
|
|
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
|
|
1030
|
-
const columnsToRender: HTMLTableCellElement[] =
|
|
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
|
-
|
|
1046
|
-
|
|
1047
|
-
const colName = th
|
|
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
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
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 {}
|
|
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>(
|
|
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:
|
|
115
|
+
cellValue:
|
|
116
|
+
| KTDataTableDataInterface[keyof KTDataTableDataInterface]
|
|
117
|
+
| string,
|
|
116
118
|
rowData: KTDataTableDataInterface,
|
|
117
119
|
) => number | string;
|
|
118
120
|
};
|