@keenthemes/ktui 1.2.5 → 1.2.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 (64) hide show
  1. package/dist/ktui.js +1965 -2690
  2. package/dist/ktui.min.js +1 -1
  3. package/dist/ktui.min.js.map +1 -1
  4. package/dist/styles.css +60 -0
  5. package/lib/cjs/components/datatable/datatable-checkbox.d.ts.map +1 -1
  6. package/lib/cjs/components/datatable/datatable-checkbox.js.map +1 -1
  7. package/lib/cjs/components/datatable/datatable-layout-plugin.d.ts +7 -0
  8. package/lib/cjs/components/datatable/datatable-layout-plugin.d.ts.map +1 -0
  9. package/lib/cjs/components/datatable/datatable-layout-plugin.js +328 -0
  10. package/lib/cjs/components/datatable/datatable-layout-plugin.js.map +1 -0
  11. package/lib/cjs/components/datatable/datatable-local-provider.d.ts +2 -2
  12. package/lib/cjs/components/datatable/datatable-local-provider.d.ts.map +1 -1
  13. package/lib/cjs/components/datatable/datatable-local-provider.js +5 -3
  14. package/lib/cjs/components/datatable/datatable-local-provider.js.map +1 -1
  15. package/lib/cjs/components/datatable/datatable-pagination-renderer.d.ts.map +1 -1
  16. package/lib/cjs/components/datatable/datatable-pagination-renderer.js +11 -12
  17. package/lib/cjs/components/datatable/datatable-pagination-renderer.js.map +1 -1
  18. package/lib/cjs/components/datatable/datatable.d.ts +9 -0
  19. package/lib/cjs/components/datatable/datatable.d.ts.map +1 -1
  20. package/lib/cjs/components/datatable/datatable.js +90 -17
  21. package/lib/cjs/components/datatable/datatable.js.map +1 -1
  22. package/lib/cjs/components/datatable/index.d.ts +1 -1
  23. package/lib/cjs/components/datatable/index.d.ts.map +1 -1
  24. package/lib/cjs/components/datatable/types.d.ts +27 -0
  25. package/lib/cjs/components/datatable/types.d.ts.map +1 -1
  26. package/lib/cjs/index.d.ts +1 -1
  27. package/lib/cjs/index.d.ts.map +1 -1
  28. package/lib/cjs/index.js.map +1 -1
  29. package/lib/esm/components/datatable/datatable-checkbox.d.ts.map +1 -1
  30. package/lib/esm/components/datatable/datatable-checkbox.js.map +1 -1
  31. package/lib/esm/components/datatable/datatable-layout-plugin.d.ts +7 -0
  32. package/lib/esm/components/datatable/datatable-layout-plugin.d.ts.map +1 -0
  33. package/lib/esm/components/datatable/datatable-layout-plugin.js +324 -0
  34. package/lib/esm/components/datatable/datatable-layout-plugin.js.map +1 -0
  35. package/lib/esm/components/datatable/datatable-local-provider.d.ts +2 -2
  36. package/lib/esm/components/datatable/datatable-local-provider.d.ts.map +1 -1
  37. package/lib/esm/components/datatable/datatable-local-provider.js +5 -3
  38. package/lib/esm/components/datatable/datatable-local-provider.js.map +1 -1
  39. package/lib/esm/components/datatable/datatable-pagination-renderer.d.ts.map +1 -1
  40. package/lib/esm/components/datatable/datatable-pagination-renderer.js +11 -12
  41. package/lib/esm/components/datatable/datatable-pagination-renderer.js.map +1 -1
  42. package/lib/esm/components/datatable/datatable.d.ts +9 -0
  43. package/lib/esm/components/datatable/datatable.d.ts.map +1 -1
  44. package/lib/esm/components/datatable/datatable.js +90 -17
  45. package/lib/esm/components/datatable/datatable.js.map +1 -1
  46. package/lib/esm/components/datatable/index.d.ts +1 -1
  47. package/lib/esm/components/datatable/index.d.ts.map +1 -1
  48. package/lib/esm/components/datatable/types.d.ts +27 -0
  49. package/lib/esm/components/datatable/types.d.ts.map +1 -1
  50. package/lib/esm/index.d.ts +1 -1
  51. package/lib/esm/index.d.ts.map +1 -1
  52. package/lib/esm/index.js.map +1 -1
  53. package/package.json +1 -1
  54. package/src/components/datatable/__tests__/locked-layout.test.ts +257 -0
  55. package/src/components/datatable/__tests__/pagination-reset.test.ts +18 -0
  56. package/src/components/datatable/datatable-checkbox.ts +5 -8
  57. package/src/components/datatable/datatable-layout-plugin.ts +449 -0
  58. package/src/components/datatable/datatable-local-provider.ts +15 -7
  59. package/src/components/datatable/datatable-pagination-renderer.ts +10 -13
  60. package/src/components/datatable/datatable.css +98 -0
  61. package/src/components/datatable/datatable.ts +109 -15
  62. package/src/components/datatable/index.ts +5 -0
  63. package/src/components/datatable/types.ts +33 -0
  64. package/src/index.ts +5 -0
@@ -0,0 +1,449 @@
1
+ /**
2
+ * KTUI - Free & Open-Source Tailwind UI Components by Keenthemes
3
+ * Copyright 2025 by Keenthemes Inc
4
+ */
5
+
6
+ import {
7
+ KTDataTableConfigInterface,
8
+ KTDataTableLayoutPluginContextInterface,
9
+ KTDataTableLayoutPluginInterface,
10
+ } from './types';
11
+
12
+ type Edge = 'left' | 'right';
13
+
14
+ const LOCKED_CELL_CLASS = 'kt-datatable-locked-cell';
15
+ const LOCKED_HEADER_CLASS = 'kt-datatable-locked-header';
16
+ const LOCKED_TOP_ROW_CLASS = 'kt-datatable-locked-top-row';
17
+ const LOCKED_BOTTOM_ROW_CLASS = 'kt-datatable-locked-bottom-row';
18
+ const LOCKED_LEFT_CLASS = 'kt-datatable-locked-left';
19
+ const LOCKED_RIGHT_CLASS = 'kt-datatable-locked-right';
20
+ const LOCKED_LAYOUT_SEPARATE_CLASS = 'kt-datatable-locked-layout-separate';
21
+ const LOCKED_HEADER_SECTION_CLASS = 'kt-datatable-locked-header-section';
22
+
23
+ const HEADER_Z_INDEX = 40;
24
+ const ROW_Z_INDEX = 30;
25
+ const COLUMN_Z_INDEX = 35;
26
+ const INTERSECTION_Z_INDEX = 45;
27
+
28
+ const toPositiveInteger = (value: number | undefined): number => {
29
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
30
+ return 0;
31
+ }
32
+
33
+ return Math.max(0, Math.floor(value));
34
+ };
35
+
36
+ const hasStickyColumns = (config: KTDataTableConfigInterface): boolean => {
37
+ const lockedLayout = config.lockedLayout;
38
+ if (!lockedLayout?.stickyColumns) {
39
+ return false;
40
+ }
41
+
42
+ return (
43
+ (lockedLayout.stickyColumns.left?.length || 0) > 0 ||
44
+ (lockedLayout.stickyColumns.right?.length || 0) > 0
45
+ );
46
+ };
47
+
48
+ const hasLockedLayoutConfig = (config: KTDataTableConfigInterface): boolean => {
49
+ const lockedLayout = config.lockedLayout;
50
+ if (!lockedLayout) {
51
+ return false;
52
+ }
53
+
54
+ return (
55
+ lockedLayout.stickyHeader === true ||
56
+ toPositiveInteger(lockedLayout.stickyRows?.top) > 0 ||
57
+ toPositiveInteger(lockedLayout.stickyRows?.bottom) > 0 ||
58
+ (lockedLayout.stickyColumns?.left?.length || 0) > 0 ||
59
+ (lockedLayout.stickyColumns?.right?.length || 0) > 0
60
+ );
61
+ };
62
+
63
+ const getScrollContainer = (rootElement: HTMLElement): HTMLElement => {
64
+ return (
65
+ rootElement.closest<HTMLElement>('.kt-table-wrapper') ||
66
+ rootElement.querySelector<HTMLElement>('.kt-table-wrapper') ||
67
+ rootElement
68
+ );
69
+ };
70
+
71
+ const clearStickyStyles = (
72
+ tableElement: HTMLTableElement,
73
+ scrollContainer: HTMLElement,
74
+ ): void => {
75
+ tableElement.classList.remove(
76
+ 'kt-datatable-locked-layout',
77
+ LOCKED_LAYOUT_SEPARATE_CLASS,
78
+ );
79
+ scrollContainer.classList.remove('kt-datatable-locked-layout-host');
80
+ tableElement.style.borderCollapse = '';
81
+ tableElement.style.borderSpacing = '';
82
+
83
+ const theadElement = tableElement.tHead;
84
+ if (theadElement) {
85
+ theadElement.classList.remove(LOCKED_HEADER_SECTION_CLASS);
86
+ theadElement.style.position = '';
87
+ theadElement.style.top = '';
88
+ theadElement.style.zIndex = '';
89
+ }
90
+
91
+ const stickyElements = tableElement.querySelectorAll<HTMLElement>(
92
+ `.${LOCKED_CELL_CLASS}`,
93
+ );
94
+
95
+ stickyElements.forEach((element) => {
96
+ element.classList.remove(
97
+ LOCKED_CELL_CLASS,
98
+ LOCKED_HEADER_CLASS,
99
+ LOCKED_TOP_ROW_CLASS,
100
+ LOCKED_BOTTOM_ROW_CLASS,
101
+ LOCKED_LEFT_CLASS,
102
+ LOCKED_RIGHT_CLASS,
103
+ );
104
+ element.style.position = '';
105
+ element.style.top = '';
106
+ element.style.bottom = '';
107
+ element.style.left = '';
108
+ element.style.right = '';
109
+ element.style.zIndex = '';
110
+ element.style.backgroundColor = '';
111
+ });
112
+ };
113
+
114
+ const getDirection = (tableElement: HTMLTableElement): 'ltr' | 'rtl' => {
115
+ const scopedDir = tableElement
116
+ .closest<HTMLElement>('[dir]')
117
+ ?.getAttribute('dir');
118
+ const globalDir =
119
+ typeof document !== 'undefined'
120
+ ? document.documentElement.getAttribute('dir')
121
+ : null;
122
+ return scopedDir === 'rtl' || globalDir === 'rtl' ? 'rtl' : 'ltr';
123
+ };
124
+
125
+ const resolveEdgeProperty = (edge: Edge, direction: 'ltr' | 'rtl'): Edge => {
126
+ if (direction === 'rtl') {
127
+ return edge === 'left' ? 'right' : 'left';
128
+ }
129
+
130
+ return edge;
131
+ };
132
+
133
+ const setStickyEdge = (
134
+ element: HTMLElement,
135
+ edge: Edge,
136
+ offset: number,
137
+ direction: 'ltr' | 'rtl',
138
+ ): void => {
139
+ const resolvedEdge = resolveEdgeProperty(edge, direction);
140
+ if (resolvedEdge === 'left') {
141
+ element.style.left = `${offset}px`;
142
+ element.style.right = '';
143
+ } else {
144
+ element.style.right = `${offset}px`;
145
+ element.style.left = '';
146
+ }
147
+ };
148
+
149
+ const ensureStickyCell = (
150
+ element: HTMLElement,
151
+ className: string,
152
+ zIndex: number,
153
+ ): void => {
154
+ element.classList.add(LOCKED_CELL_CLASS, className);
155
+ element.style.position = 'sticky';
156
+ element.style.zIndex = String(zIndex);
157
+ };
158
+
159
+ const measureStickyHeaderHeight = (
160
+ theadElement: HTMLTableSectionElement,
161
+ ): number => Math.round(theadElement.offsetHeight);
162
+
163
+ /** Offset for top sticky body rows so they sit flush under a sticky header. */
164
+ const getStickyTopRowOffset = (
165
+ headerHeight: number,
166
+ useCollapsedBorders: boolean,
167
+ ): number => {
168
+ if (headerHeight <= 0) {
169
+ return 0;
170
+ }
171
+
172
+ // Collapsed row borders are shared between thead and the first tbody row.
173
+ return useCollapsedBorders ? headerHeight - 1 : headerHeight;
174
+ };
175
+
176
+ const markIntersectionZIndex = (element: HTMLElement): void => {
177
+ const isRowLocked =
178
+ element.classList.contains(LOCKED_HEADER_CLASS) ||
179
+ element.classList.contains(LOCKED_TOP_ROW_CLASS) ||
180
+ element.classList.contains(LOCKED_BOTTOM_ROW_CLASS);
181
+ const isColumnLocked =
182
+ element.classList.contains(LOCKED_LEFT_CLASS) ||
183
+ element.classList.contains(LOCKED_RIGHT_CLASS);
184
+
185
+ if (isRowLocked && isColumnLocked) {
186
+ element.style.zIndex = String(INTERSECTION_Z_INDEX);
187
+ }
188
+ };
189
+
190
+ const applyStickyHeader = (
191
+ theadElement: HTMLTableSectionElement,
192
+ enabled: boolean,
193
+ useSectionSticky: boolean,
194
+ ): number => {
195
+ if (!enabled) {
196
+ return 0;
197
+ }
198
+
199
+ if (useSectionSticky) {
200
+ theadElement.classList.add(LOCKED_HEADER_SECTION_CLASS);
201
+ theadElement.style.position = 'sticky';
202
+ theadElement.style.top = '0';
203
+ theadElement.style.zIndex = String(HEADER_Z_INDEX);
204
+
205
+ Array.from(theadElement.rows).forEach((row) => {
206
+ Array.from(row.cells).forEach((cell) => {
207
+ const headerCell = cell as HTMLTableCellElement;
208
+ headerCell.classList.add(LOCKED_CELL_CLASS, LOCKED_HEADER_CLASS);
209
+ });
210
+ });
211
+
212
+ return measureStickyHeaderHeight(theadElement);
213
+ }
214
+
215
+ let cumulativeTop = 0;
216
+ Array.from(theadElement.rows).forEach((row) => {
217
+ const rowTop = cumulativeTop;
218
+ Array.from(row.cells).forEach((cell) => {
219
+ const headerCell = cell as HTMLTableCellElement;
220
+ ensureStickyCell(headerCell, LOCKED_HEADER_CLASS, HEADER_Z_INDEX);
221
+ headerCell.style.top = `${rowTop}px`;
222
+ });
223
+ cumulativeTop += row.offsetHeight;
224
+ });
225
+
226
+ return cumulativeTop;
227
+ };
228
+
229
+ const applyStickyRows = (
230
+ tbodyElement: HTMLTableSectionElement,
231
+ headerHeight: number,
232
+ topCount: number,
233
+ bottomCount: number,
234
+ useCollapsedBorders: boolean,
235
+ ): void => {
236
+ const rows = Array.from(tbodyElement.rows);
237
+
238
+ let topOffset = getStickyTopRowOffset(headerHeight, useCollapsedBorders);
239
+ rows.slice(0, topCount).forEach((row) => {
240
+ const rowTop = topOffset;
241
+ Array.from(row.cells).forEach((cell) => {
242
+ const td = cell as HTMLTableCellElement;
243
+ ensureStickyCell(td, LOCKED_TOP_ROW_CLASS, ROW_Z_INDEX);
244
+ td.style.top = `${rowTop}px`;
245
+ });
246
+ topOffset += row.offsetHeight;
247
+ });
248
+
249
+ let bottomOffset = 0;
250
+ rows
251
+ .slice(Math.max(0, rows.length - bottomCount))
252
+ .reverse()
253
+ .forEach((row) => {
254
+ const rowBottom = bottomOffset;
255
+ Array.from(row.cells).forEach((cell) => {
256
+ const td = cell as HTMLTableCellElement;
257
+ ensureStickyCell(td, LOCKED_BOTTOM_ROW_CLASS, ROW_Z_INDEX);
258
+ td.style.bottom = `${rowBottom}px`;
259
+ });
260
+ bottomOffset += row.offsetHeight;
261
+ });
262
+ };
263
+
264
+ const getColumnIndexMap = (
265
+ theadElement: HTMLTableSectionElement,
266
+ config: KTDataTableConfigInterface,
267
+ ): Map<string, number> => {
268
+ const map = new Map<string, number>();
269
+ const typedHeaders = Array.from(
270
+ theadElement.querySelectorAll<HTMLTableCellElement>(
271
+ 'th[data-kt-datatable-column]',
272
+ ),
273
+ );
274
+
275
+ if (typedHeaders.length > 0) {
276
+ typedHeaders.forEach((th, index) => {
277
+ const column = th.getAttribute('data-kt-datatable-column');
278
+ if (column) {
279
+ map.set(column, index);
280
+ }
281
+ });
282
+ return map;
283
+ }
284
+
285
+ if (config.columns) {
286
+ Object.keys(config.columns).forEach((key, index) => {
287
+ map.set(key, index);
288
+ });
289
+ }
290
+
291
+ return map;
292
+ };
293
+
294
+ const getColumnCells = (
295
+ tableElement: HTMLTableElement,
296
+ columnIndex: number,
297
+ ): HTMLTableCellElement[] => {
298
+ const cells: HTMLTableCellElement[] = [];
299
+ tableElement.querySelectorAll('tr').forEach((row) => {
300
+ const cell = row.children.item(columnIndex);
301
+ if (cell instanceof HTMLTableCellElement) {
302
+ cells.push(cell);
303
+ }
304
+ });
305
+ return cells;
306
+ };
307
+
308
+ const applyStickyColumns = (
309
+ tableElement: HTMLTableElement,
310
+ theadElement: HTMLTableSectionElement,
311
+ config: KTDataTableConfigInterface,
312
+ ): void => {
313
+ const lockedColumns = config.lockedLayout?.stickyColumns;
314
+ if (!lockedColumns) {
315
+ return;
316
+ }
317
+
318
+ const direction = getDirection(tableElement);
319
+ const columnMap = getColumnIndexMap(theadElement, config);
320
+
321
+ let leftOffset = 0;
322
+ (lockedColumns.left || []).forEach((key) => {
323
+ const index = columnMap.get(key);
324
+ if (typeof index !== 'number') {
325
+ return;
326
+ }
327
+
328
+ const cells = getColumnCells(tableElement, index);
329
+ if (cells.length === 0) {
330
+ return;
331
+ }
332
+
333
+ const width = cells[0].getBoundingClientRect().width;
334
+ cells.forEach((cell) => {
335
+ ensureStickyCell(cell, LOCKED_LEFT_CLASS, COLUMN_Z_INDEX);
336
+ setStickyEdge(cell, 'left', leftOffset, direction);
337
+ });
338
+ leftOffset += width;
339
+ });
340
+
341
+ let rightOffset = 0;
342
+ [...(lockedColumns.right || [])].reverse().forEach((key) => {
343
+ const index = columnMap.get(key);
344
+ if (typeof index !== 'number') {
345
+ return;
346
+ }
347
+
348
+ const cells = getColumnCells(tableElement, index);
349
+ if (cells.length === 0) {
350
+ return;
351
+ }
352
+
353
+ const width = cells[0].getBoundingClientRect().width;
354
+ cells.forEach((cell) => {
355
+ ensureStickyCell(cell, LOCKED_RIGHT_CLASS, COLUMN_Z_INDEX);
356
+ setStickyEdge(cell, 'right', rightOffset, direction);
357
+ });
358
+ rightOffset += width;
359
+ });
360
+
361
+ tableElement
362
+ .querySelectorAll<HTMLElement>(`.${LOCKED_CELL_CLASS}`)
363
+ .forEach(markIntersectionZIndex);
364
+ };
365
+
366
+ export const createStickyLayoutPlugin =
367
+ (): KTDataTableLayoutPluginInterface => {
368
+ let resizeHandler: (() => void) | null = null;
369
+ let scrollContainerTarget: HTMLElement | null = null;
370
+ let isApplying = false;
371
+
372
+ const applyLayout = (
373
+ ctx: KTDataTableLayoutPluginContextInterface,
374
+ ): void => {
375
+ if (isApplying || !hasLockedLayoutConfig(ctx.config)) {
376
+ return;
377
+ }
378
+
379
+ isApplying = true;
380
+ try {
381
+ const scrollContainer = getScrollContainer(ctx.rootElement);
382
+ clearStickyStyles(ctx.tableElement, scrollContainer);
383
+ ctx.tableElement.classList.add('kt-datatable-locked-layout');
384
+ scrollContainer.classList.add('kt-datatable-locked-layout-host');
385
+
386
+ if (hasStickyColumns(ctx.config)) {
387
+ ctx.tableElement.classList.add(LOCKED_LAYOUT_SEPARATE_CLASS);
388
+ ctx.tableElement.style.borderCollapse = 'separate';
389
+ ctx.tableElement.style.borderSpacing = '0';
390
+ }
391
+
392
+ const lockedLayout = ctx.config.lockedLayout || {};
393
+ const useCollapsedBorders = !hasStickyColumns(ctx.config);
394
+ const headerHeight = applyStickyHeader(
395
+ ctx.theadElement,
396
+ lockedLayout.stickyHeader === true,
397
+ useCollapsedBorders,
398
+ );
399
+
400
+ applyStickyRows(
401
+ ctx.tbodyElement,
402
+ headerHeight,
403
+ toPositiveInteger(lockedLayout.stickyRows?.top),
404
+ toPositiveInteger(lockedLayout.stickyRows?.bottom),
405
+ useCollapsedBorders,
406
+ );
407
+
408
+ applyStickyColumns(ctx.tableElement, ctx.theadElement, ctx.config);
409
+ } finally {
410
+ isApplying = false;
411
+ }
412
+ };
413
+
414
+ const detachResizeListener = (): void => {
415
+ if (!resizeHandler) {
416
+ return;
417
+ }
418
+
419
+ window.removeEventListener('resize', resizeHandler);
420
+ if (scrollContainerTarget) {
421
+ scrollContainerTarget.removeEventListener('scroll', resizeHandler);
422
+ }
423
+
424
+ resizeHandler = null;
425
+ scrollContainerTarget = null;
426
+ };
427
+
428
+ return {
429
+ beforeDraw: (ctx) => {
430
+ const scrollContainer = getScrollContainer(ctx.rootElement);
431
+ clearStickyStyles(ctx.tableElement, scrollContainer);
432
+ },
433
+ afterDraw: (ctx) => {
434
+ detachResizeListener();
435
+ applyLayout(ctx);
436
+
437
+ const scrollContainer = getScrollContainer(ctx.rootElement);
438
+ resizeHandler = () => applyLayout(ctx);
439
+ window.addEventListener('resize', resizeHandler);
440
+ scrollContainerTarget = scrollContainer;
441
+ scrollContainer.addEventListener('scroll', resizeHandler);
442
+ },
443
+ dispose: (ctx) => {
444
+ detachResizeListener();
445
+ const scrollContainer = getScrollContainer(ctx.rootElement);
446
+ clearStickyStyles(ctx.tableElement, scrollContainer);
447
+ },
448
+ };
449
+ };
@@ -8,7 +8,6 @@ import {
8
8
  KTDataTableAttributeInterface,
9
9
  KTDataTableConfigInterface,
10
10
  KTDataTableDataInterface,
11
- KTDataTableStateInterface,
12
11
  } from './types';
13
12
  import {
14
13
  KTDataTableDataProvider,
@@ -17,7 +16,7 @@ import {
17
16
  KTDataTableStateStore,
18
17
  } from './datatable-contracts';
19
18
 
20
- interface KTDataTableLocalProviderOptions<T extends KTDataTableDataInterface> {
19
+ interface KTDataTableLocalProviderOptions {
21
20
  config: KTDataTableConfigInterface;
22
21
  elements: () => KTDataTableLocalProviderElements;
23
22
  getLogicalColumnCount: () => number;
@@ -28,7 +27,7 @@ interface KTDataTableLocalProviderOptions<T extends KTDataTableDataInterface> {
28
27
  export class KTDataTableLocalDataProvider<
29
28
  T extends KTDataTableDataInterface,
30
29
  > implements KTDataTableDataProvider<T> {
31
- constructor(private readonly options: KTDataTableLocalProviderOptions<T>) {}
30
+ constructor(private readonly options: KTDataTableLocalProviderOptions) {}
32
31
 
33
32
  public async fetch(): Promise<KTDataTableProviderResult<T>> {
34
33
  return this.fetchSync();
@@ -37,13 +36,17 @@ export class KTDataTableLocalDataProvider<
37
36
  public fetchSync(): KTDataTableProviderResult<T> {
38
37
  const state = this.options.stateStore.getState();
39
38
  let { originalData } = state;
39
+ const skipDomInvalidation = Boolean(
40
+ this.options.config.lockedLayout || this.options.config.layoutPlugin,
41
+ );
40
42
 
41
43
  if (
42
44
  !this.options.elements().tableElement ||
43
45
  originalData === undefined ||
44
- this.tableConfigInvalidate() ||
45
- this.localTableHeaderInvalidate() ||
46
- this.localTableContentInvalidate()
46
+ (!skipDomInvalidation &&
47
+ (this.tableConfigInvalidate() ||
48
+ this.localTableHeaderInvalidate() ||
49
+ this.localTableContentInvalidate()))
47
50
  ) {
48
51
  const { originalData, originalDataAttributes } =
49
52
  this.localExtractTableContent();
@@ -80,7 +83,12 @@ export class KTDataTableLocalDataProvider<
80
83
  sortOrder !== '' &&
81
84
  typeof sortCallback === 'function'
82
85
  ) {
83
- data = sortCallback.call(this, data, sortField as string, sortOrder) as T[];
86
+ data = sortCallback.call(
87
+ this,
88
+ data,
89
+ sortField as string,
90
+ sortOrder,
91
+ ) as T[];
84
92
  }
85
93
 
86
94
  if (data?.length > 0) {
@@ -26,6 +26,7 @@ export class KTDataTableDomPaginationRenderer implements KTDataTablePaginationRe
26
26
  return () => {
27
27
  if (input.sizeElement) {
28
28
  input.sizeElement.onchange = null;
29
+ this.removeChildElements(input.sizeElement);
29
30
  }
30
31
  if (input.paginationElement) {
31
32
  this.removeChildElements(input.paginationElement);
@@ -51,18 +52,15 @@ export class KTDataTableDomPaginationRenderer implements KTDataTablePaginationRe
51
52
  }
52
53
 
53
54
  const pageSizes = input.config.pageSizes ?? [5, 10, 20, 30, 50];
55
+ const options = pageSizes.map((size: number) => {
56
+ const option = document.createElement('option') as HTMLOptionElement;
57
+ option.value = String(size);
58
+ option.text = String(size);
59
+ option.selected = input.state.pageSize === size;
60
+ return option;
61
+ });
54
62
 
55
- setTimeout(() => {
56
- const options = pageSizes.map((size: number) => {
57
- const option = document.createElement('option') as HTMLOptionElement;
58
- option.value = String(size);
59
- option.text = String(size);
60
- option.selected = input.state.pageSize === size;
61
- return option;
62
- });
63
-
64
- input.sizeElement.append(...options);
65
- }, 100);
63
+ input.sizeElement.append(...options);
66
64
 
67
65
  input.sizeElement.onchange = (event: Event) => {
68
66
  input.reloadPageSize(
@@ -92,8 +90,7 @@ export class KTDataTableDomPaginationRenderer implements KTDataTablePaginationRe
92
90
  return;
93
91
  }
94
92
 
95
- const infoTemplate =
96
- input.config.info ?? '{start}-{end} of {total}';
93
+ const infoTemplate = input.config.info ?? '{start}-{end} of {total}';
97
94
  input.infoElement.textContent = infoTemplate
98
95
  .replace(
99
96
  '{start}',
@@ -71,6 +71,104 @@
71
71
  [data-kt-datatable].loading table {
72
72
  opacity: 0.6;
73
73
  }
74
+
75
+ /* Locked layout styles */
76
+ [data-kt-datatable] .kt-datatable-locked-layout {
77
+ position: relative;
78
+ }
79
+
80
+ [data-kt-datatable]
81
+ .kt-datatable-locked-layout
82
+ [data-kt-datatable-table]
83
+ thead.kt-datatable-locked-header-section {
84
+ /* Sticky thead group — opaque surface, no gap above first body row */
85
+ background-color: color-mix(
86
+ in srgb,
87
+ var(--color-muted, rgb(0, 0, 0)) 40%,
88
+ var(--color-card, var(--color-background, #ffffff))
89
+ ) !important;
90
+ }
91
+
92
+ /* Sticky columns use border-collapse: separate — per-cell borders replace tr border-b */
93
+ [data-kt-datatable]
94
+ .kt-datatable-locked-layout.kt-datatable-locked-layout-separate
95
+ [data-kt-datatable-table]
96
+ thead
97
+ tr {
98
+ @apply border-b-0;
99
+ }
100
+
101
+ [data-kt-datatable]
102
+ .kt-datatable-locked-layout.kt-datatable-locked-layout-separate
103
+ [data-kt-datatable-table]
104
+ thead
105
+ th {
106
+ @apply border-b border-border;
107
+ }
108
+
109
+ [data-kt-datatable]
110
+ .kt-datatable-locked-layout.kt-datatable-locked-layout-separate
111
+ [data-kt-datatable-table]
112
+ tbody
113
+ tr {
114
+ @apply border-b-0;
115
+ }
116
+
117
+ [data-kt-datatable]
118
+ .kt-datatable-locked-layout.kt-datatable-locked-layout-separate
119
+ [data-kt-datatable-table]
120
+ tbody
121
+ td {
122
+ @apply border-b border-border;
123
+ }
124
+
125
+ [data-kt-datatable]
126
+ .kt-datatable-locked-layout.kt-datatable-locked-layout-separate
127
+ [data-kt-datatable-table]
128
+ tbody
129
+ tr:last-child
130
+ td {
131
+ @apply border-b-0;
132
+ }
133
+
134
+ [data-kt-datatable] .kt-datatable-locked-cell {
135
+ overflow: hidden;
136
+ isolation: isolate;
137
+ /* Opaque surface so horizontally scrolling cells do not show through */
138
+ background-color: var(--color-card, var(--color-background, #ffffff)) !important;
139
+ }
140
+
141
+ [data-kt-datatable] [data-kt-datatable-table] thead th.kt-datatable-locked-cell,
142
+ [data-kt-datatable] .kt-datatable-locked-header {
143
+ /* Match kt-table thead bg-muted/40 without transparency */
144
+ background-color: color-mix(
145
+ in srgb,
146
+ var(--color-muted, rgb(0, 0, 0)) 40%,
147
+ var(--color-card, var(--color-background, #ffffff))
148
+ ) !important;
149
+ }
150
+
151
+ [data-kt-datatable] .kt-datatable-locked-left {
152
+ box-shadow: inset -1px 0 0 0 var(--color-border, rgba(0, 0, 0, 0.08));
153
+ }
154
+
155
+ [data-kt-datatable] .kt-datatable-locked-right {
156
+ box-shadow: inset 1px 0 0 0 var(--color-border, rgba(0, 0, 0, 0.08));
157
+ }
158
+
159
+ /* Keep row hover/checked visual states on sticky body cells */
160
+ [data-kt-datatable] [data-kt-datatable-table] tbody tr:hover > .kt-datatable-locked-cell {
161
+ /* Match kt-table row hover bg-muted/50 without transparency */
162
+ background-color: color-mix(
163
+ in srgb,
164
+ var(--color-muted, rgb(0, 0, 0)) 50%,
165
+ var(--color-card, var(--color-background, #ffffff))
166
+ ) !important;
167
+ }
168
+
169
+ [data-kt-datatable] [data-kt-datatable-table] tbody tr.checked > .kt-datatable-locked-cell {
170
+ background-color: var(--color-muted, rgb(0, 0, 0)) !important;
171
+ }
74
172
  }
75
173
 
76
174
  @custom-variant kt-datatable-loading {