@keenthemes/ktui 1.2.4 → 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 (152) hide show
  1. package/dist/ktui.js +2551 -2817
  2. package/dist/ktui.min.js +1 -1
  3. package/dist/ktui.min.js.map +1 -1
  4. package/dist/styles.css +136 -40
  5. package/lib/cjs/components/datatable/datatable-checkbox.d.ts.map +1 -1
  6. package/lib/cjs/components/datatable/datatable-checkbox.js +34 -15
  7. package/lib/cjs/components/datatable/datatable-checkbox.js.map +1 -1
  8. package/lib/cjs/components/datatable/datatable-contracts.d.ts +3 -3
  9. package/lib/cjs/components/datatable/datatable-contracts.d.ts.map +1 -1
  10. package/lib/cjs/components/datatable/datatable-layout-plugin.d.ts +7 -0
  11. package/lib/cjs/components/datatable/datatable-layout-plugin.d.ts.map +1 -0
  12. package/lib/cjs/components/datatable/datatable-layout-plugin.js +328 -0
  13. package/lib/cjs/components/datatable/datatable-layout-plugin.js.map +1 -0
  14. package/lib/cjs/components/datatable/datatable-local-provider.d.ts +2 -2
  15. package/lib/cjs/components/datatable/datatable-local-provider.d.ts.map +1 -1
  16. package/lib/cjs/components/datatable/datatable-local-provider.js +18 -10
  17. package/lib/cjs/components/datatable/datatable-local-provider.js.map +1 -1
  18. package/lib/cjs/components/datatable/datatable-pagination-renderer.d.ts.map +1 -1
  19. package/lib/cjs/components/datatable/datatable-pagination-renderer.js +40 -25
  20. package/lib/cjs/components/datatable/datatable-pagination-renderer.js.map +1 -1
  21. package/lib/cjs/components/datatable/datatable-remote-provider.d.ts.map +1 -1
  22. package/lib/cjs/components/datatable/datatable-remote-provider.js +3 -0
  23. package/lib/cjs/components/datatable/datatable-remote-provider.js.map +1 -1
  24. package/lib/cjs/components/datatable/datatable-table-renderer.d.ts.map +1 -1
  25. package/lib/cjs/components/datatable/datatable-table-renderer.js +14 -6
  26. package/lib/cjs/components/datatable/datatable-table-renderer.js.map +1 -1
  27. package/lib/cjs/components/datatable/datatable.d.ts +9 -0
  28. package/lib/cjs/components/datatable/datatable.d.ts.map +1 -1
  29. package/lib/cjs/components/datatable/datatable.js +200 -61
  30. package/lib/cjs/components/datatable/datatable.js.map +1 -1
  31. package/lib/cjs/components/datatable/index.d.ts +1 -1
  32. package/lib/cjs/components/datatable/index.d.ts.map +1 -1
  33. package/lib/cjs/components/datatable/types.d.ts +27 -0
  34. package/lib/cjs/components/datatable/types.d.ts.map +1 -1
  35. package/lib/cjs/components/dropdown/dropdown.d.ts +2 -2
  36. package/lib/cjs/components/dropdown/dropdown.d.ts.map +1 -1
  37. package/lib/cjs/components/dropdown/dropdown.js +68 -31
  38. package/lib/cjs/components/dropdown/dropdown.js.map +1 -1
  39. package/lib/cjs/components/input-number/index.d.ts +7 -0
  40. package/lib/cjs/components/input-number/index.d.ts.map +1 -0
  41. package/lib/cjs/components/input-number/index.js +10 -0
  42. package/lib/cjs/components/input-number/index.js.map +1 -0
  43. package/lib/cjs/components/input-number/input-number.d.ts +40 -0
  44. package/lib/cjs/components/input-number/input-number.d.ts.map +1 -0
  45. package/lib/cjs/components/input-number/input-number.js +248 -0
  46. package/lib/cjs/components/input-number/input-number.js.map +1 -0
  47. package/lib/cjs/components/input-number/types.d.ts +30 -0
  48. package/lib/cjs/components/input-number/types.d.ts.map +1 -0
  49. package/lib/cjs/components/input-number/types.js +7 -0
  50. package/lib/cjs/components/input-number/types.js.map +1 -0
  51. package/lib/cjs/components/select/config.d.ts +1 -0
  52. package/lib/cjs/components/select/config.d.ts.map +1 -1
  53. package/lib/cjs/components/select/config.js +2 -1
  54. package/lib/cjs/components/select/config.js.map +1 -1
  55. package/lib/cjs/components/select/select.d.ts +8 -1
  56. package/lib/cjs/components/select/select.d.ts.map +1 -1
  57. package/lib/cjs/components/select/select.js +14 -1
  58. package/lib/cjs/components/select/select.js.map +1 -1
  59. package/lib/cjs/components/select/tags.d.ts.map +1 -1
  60. package/lib/cjs/components/select/tags.js +10 -0
  61. package/lib/cjs/components/select/tags.js.map +1 -1
  62. package/lib/cjs/index.d.ts +5 -1
  63. package/lib/cjs/index.d.ts.map +1 -1
  64. package/lib/cjs/index.js +5 -1
  65. package/lib/cjs/index.js.map +1 -1
  66. package/lib/esm/components/datatable/datatable-checkbox.d.ts.map +1 -1
  67. package/lib/esm/components/datatable/datatable-checkbox.js +34 -15
  68. package/lib/esm/components/datatable/datatable-checkbox.js.map +1 -1
  69. package/lib/esm/components/datatable/datatable-contracts.d.ts +3 -3
  70. package/lib/esm/components/datatable/datatable-contracts.d.ts.map +1 -1
  71. package/lib/esm/components/datatable/datatable-layout-plugin.d.ts +7 -0
  72. package/lib/esm/components/datatable/datatable-layout-plugin.d.ts.map +1 -0
  73. package/lib/esm/components/datatable/datatable-layout-plugin.js +324 -0
  74. package/lib/esm/components/datatable/datatable-layout-plugin.js.map +1 -0
  75. package/lib/esm/components/datatable/datatable-local-provider.d.ts +2 -2
  76. package/lib/esm/components/datatable/datatable-local-provider.d.ts.map +1 -1
  77. package/lib/esm/components/datatable/datatable-local-provider.js +18 -10
  78. package/lib/esm/components/datatable/datatable-local-provider.js.map +1 -1
  79. package/lib/esm/components/datatable/datatable-pagination-renderer.d.ts.map +1 -1
  80. package/lib/esm/components/datatable/datatable-pagination-renderer.js +40 -25
  81. package/lib/esm/components/datatable/datatable-pagination-renderer.js.map +1 -1
  82. package/lib/esm/components/datatable/datatable-remote-provider.d.ts.map +1 -1
  83. package/lib/esm/components/datatable/datatable-remote-provider.js +3 -0
  84. package/lib/esm/components/datatable/datatable-remote-provider.js.map +1 -1
  85. package/lib/esm/components/datatable/datatable-table-renderer.d.ts.map +1 -1
  86. package/lib/esm/components/datatable/datatable-table-renderer.js +14 -6
  87. package/lib/esm/components/datatable/datatable-table-renderer.js.map +1 -1
  88. package/lib/esm/components/datatable/datatable.d.ts +9 -0
  89. package/lib/esm/components/datatable/datatable.d.ts.map +1 -1
  90. package/lib/esm/components/datatable/datatable.js +200 -61
  91. package/lib/esm/components/datatable/datatable.js.map +1 -1
  92. package/lib/esm/components/datatable/index.d.ts +1 -1
  93. package/lib/esm/components/datatable/index.d.ts.map +1 -1
  94. package/lib/esm/components/datatable/types.d.ts +27 -0
  95. package/lib/esm/components/datatable/types.d.ts.map +1 -1
  96. package/lib/esm/components/dropdown/dropdown.d.ts +2 -2
  97. package/lib/esm/components/dropdown/dropdown.d.ts.map +1 -1
  98. package/lib/esm/components/dropdown/dropdown.js +68 -31
  99. package/lib/esm/components/dropdown/dropdown.js.map +1 -1
  100. package/lib/esm/components/input-number/index.d.ts +7 -0
  101. package/lib/esm/components/input-number/index.d.ts.map +1 -0
  102. package/lib/esm/components/input-number/index.js +6 -0
  103. package/lib/esm/components/input-number/index.js.map +1 -0
  104. package/lib/esm/components/input-number/input-number.d.ts +40 -0
  105. package/lib/esm/components/input-number/input-number.d.ts.map +1 -0
  106. package/lib/esm/components/input-number/input-number.js +245 -0
  107. package/lib/esm/components/input-number/input-number.js.map +1 -0
  108. package/lib/esm/components/input-number/types.d.ts +30 -0
  109. package/lib/esm/components/input-number/types.d.ts.map +1 -0
  110. package/lib/esm/components/input-number/types.js +6 -0
  111. package/lib/esm/components/input-number/types.js.map +1 -0
  112. package/lib/esm/components/select/config.d.ts +1 -0
  113. package/lib/esm/components/select/config.d.ts.map +1 -1
  114. package/lib/esm/components/select/config.js +2 -1
  115. package/lib/esm/components/select/config.js.map +1 -1
  116. package/lib/esm/components/select/select.d.ts +8 -1
  117. package/lib/esm/components/select/select.d.ts.map +1 -1
  118. package/lib/esm/components/select/select.js +14 -1
  119. package/lib/esm/components/select/select.js.map +1 -1
  120. package/lib/esm/components/select/tags.d.ts.map +1 -1
  121. package/lib/esm/components/select/tags.js +11 -1
  122. package/lib/esm/components/select/tags.js.map +1 -1
  123. package/lib/esm/index.d.ts +5 -1
  124. package/lib/esm/index.d.ts.map +1 -1
  125. package/lib/esm/index.js +3 -0
  126. package/lib/esm/index.js.map +1 -1
  127. package/package.json +5 -11
  128. package/src/components/datatable/__tests__/locked-layout.test.ts +257 -0
  129. package/src/components/datatable/__tests__/pagination-reset.test.ts +18 -0
  130. package/src/components/datatable/datatable-checkbox.ts +35 -27
  131. package/src/components/datatable/datatable-contracts.ts +3 -3
  132. package/src/components/datatable/datatable-layout-plugin.ts +449 -0
  133. package/src/components/datatable/datatable-local-provider.ts +21 -14
  134. package/src/components/datatable/datatable-pagination-renderer.ts +40 -29
  135. package/src/components/datatable/datatable-remote-provider.ts +3 -0
  136. package/src/components/datatable/datatable-table-renderer.ts +40 -32
  137. package/src/components/datatable/datatable.css +98 -0
  138. package/src/components/datatable/datatable.ts +223 -86
  139. package/src/components/datatable/index.ts +5 -0
  140. package/src/components/datatable/types.ts +33 -0
  141. package/src/components/dropdown/dropdown.ts +86 -58
  142. package/src/components/input/input-group.css +14 -1
  143. package/src/components/input-number/__tests__/input-number.test.ts +278 -0
  144. package/src/components/input-number/index.ts +11 -0
  145. package/src/components/input-number/input-number.ts +267 -0
  146. package/src/components/input-number/types.ts +32 -0
  147. package/src/components/select/__tests__/ux-behaviors.test.ts +72 -0
  148. package/src/components/select/config.ts +3 -1
  149. package/src/components/select/select.css +23 -20
  150. package/src/components/select/select.ts +15 -1
  151. package/src/components/select/tags.ts +14 -1
  152. package/src/index.ts +14 -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();
@@ -63,20 +66,24 @@ export class KTDataTableLocalDataProvider<
63
66
 
64
67
  if (search) {
65
68
  const searchTerm = typeof search === 'string' ? search : '';
66
- filteredData = data = this.options.config.search.callback.call(
67
- this,
68
- data,
69
- searchTerm,
70
- ) as T[];
69
+ const searchCallback = this.options.config.search?.callback;
70
+ if (searchCallback) {
71
+ filteredData = data = searchCallback.call(
72
+ this,
73
+ data,
74
+ searchTerm,
75
+ ) as T[];
76
+ }
71
77
  }
72
78
 
79
+ const sortCallback = this.options.config.sort?.callback;
73
80
  if (
74
81
  sortField !== undefined &&
75
82
  sortOrder !== undefined &&
76
83
  sortOrder !== '' &&
77
- typeof this.options.config.sort.callback === 'function'
84
+ typeof sortCallback === 'function'
78
85
  ) {
79
- data = this.options.config.sort.callback.call(
86
+ data = sortCallback.call(
80
87
  this,
81
88
  data,
82
89
  sortField as string,
@@ -114,7 +121,7 @@ export class KTDataTableLocalDataProvider<
114
121
  const { _state, ...restConfig } = this.options.config;
115
122
  const checksum: string = KTUtils.checksum(JSON.stringify(restConfig));
116
123
 
117
- if (_state._configChecksum !== checksum) {
124
+ if ((_state?._configChecksum ?? '') !== checksum) {
118
125
  this.options.stateStore.patchState({ _configChecksum: checksum });
119
126
  return true;
120
127
  }
@@ -13,21 +13,28 @@ export class KTDataTableDomPaginationRenderer implements KTDataTablePaginationRe
13
13
  public render(
14
14
  input: KTDataTablePaginationRendererInput,
15
15
  ): KTDataTableCleanup | void {
16
- this.removeChildElements(input.sizeElement);
17
- this.createPageSizeControls(input);
16
+ if (input.sizeElement) {
17
+ this.removeChildElements(input.sizeElement);
18
+ this.createPageSizeControls(input);
19
+ }
18
20
 
19
- this.removeChildElements(input.paginationElement);
20
- this.createPaginationControls(input);
21
+ if (input.paginationElement) {
22
+ this.removeChildElements(input.paginationElement);
23
+ this.createPaginationControls(input);
24
+ }
21
25
 
22
26
  return () => {
23
27
  if (input.sizeElement) {
24
28
  input.sizeElement.onchange = null;
29
+ this.removeChildElements(input.sizeElement);
30
+ }
31
+ if (input.paginationElement) {
32
+ this.removeChildElements(input.paginationElement);
25
33
  }
26
- this.removeChildElements(input.paginationElement);
27
34
  };
28
35
  }
29
36
 
30
- private removeChildElements(container: HTMLElement): void {
37
+ private removeChildElements(container?: HTMLElement | null): void {
31
38
  if (!container) {
32
39
  return;
33
40
  }
@@ -39,22 +46,21 @@ export class KTDataTableDomPaginationRenderer implements KTDataTablePaginationRe
39
46
 
40
47
  private createPageSizeControls(
41
48
  input: KTDataTablePaginationRendererInput,
42
- ): HTMLSelectElement {
49
+ ): void {
43
50
  if (!input.sizeElement) {
44
- return input.sizeElement;
51
+ return;
45
52
  }
46
53
 
47
- setTimeout(() => {
48
- const options = input.config.pageSizes.map((size: number) => {
49
- const option = document.createElement('option') as HTMLOptionElement;
50
- option.value = String(size);
51
- option.text = String(size);
52
- option.selected = input.state.pageSize === size;
53
- return option;
54
- });
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
+ });
55
62
 
56
- input.sizeElement.append(...options);
57
- }, 100);
63
+ input.sizeElement.append(...options);
58
64
 
59
65
  input.sizeElement.onchange = (event: Event) => {
60
66
  input.reloadPageSize(
@@ -62,18 +68,12 @@ export class KTDataTableDomPaginationRenderer implements KTDataTablePaginationRe
62
68
  1,
63
69
  );
64
70
  };
65
-
66
- return input.sizeElement;
67
71
  }
68
72
 
69
73
  private createPaginationControls(
70
74
  input: KTDataTablePaginationRendererInput,
71
- ): HTMLElement {
72
- if (
73
- !input.infoElement ||
74
- !input.paginationElement ||
75
- input.dataLength === 0
76
- ) {
75
+ ): HTMLElement | null {
76
+ if (!input.paginationElement || input.dataLength === 0) {
77
77
  return null;
78
78
  }
79
79
 
@@ -86,7 +86,12 @@ export class KTDataTableDomPaginationRenderer implements KTDataTablePaginationRe
86
86
  private setPaginationInfoText(
87
87
  input: KTDataTablePaginationRendererInput,
88
88
  ): void {
89
- input.infoElement.textContent = input.config.info
89
+ if (!input.infoElement) {
90
+ return;
91
+ }
92
+
93
+ const infoTemplate = input.config.info ?? '{start}-{end} of {total}';
94
+ input.infoElement.textContent = infoTemplate
90
95
  .replace(
91
96
  '{start}',
92
97
  (input.state.page - 1) * input.state.pageSize + 1 + '',
@@ -105,8 +110,14 @@ export class KTDataTableDomPaginationRenderer implements KTDataTablePaginationRe
105
110
  paginationContainer: HTMLElement,
106
111
  input: KTDataTablePaginationRendererInput,
107
112
  ): void {
113
+ const pagination = input.config.pagination;
114
+ if (!pagination) {
115
+ return;
116
+ }
117
+
108
118
  const { page: currentPage, totalPages } = input.state;
109
- const { previous, next, number, more } = input.config.pagination;
119
+ const { previous, next, number, more } = pagination;
120
+ const pageMoreLimit = input.config.pageMoreLimit ?? 3;
110
121
 
111
122
  const createButton = (
112
123
  text: string,
@@ -135,7 +146,7 @@ export class KTDataTableDomPaginationRenderer implements KTDataTablePaginationRe
135
146
  const range = this.calculatePageRange(
136
147
  currentPage,
137
148
  totalPages,
138
- input.config.pageMoreLimit,
149
+ pageMoreLimit,
139
150
  );
140
151
 
141
152
  if (range.start > 1) {
@@ -137,6 +137,9 @@ export class KTDataTableRemoteDataProvider<
137
137
  this.options.config.requestMethod;
138
138
  let requestBody: RequestInit['body'] | undefined = undefined;
139
139
  let apiEndpoint = this.options.config.apiEndpoint;
140
+ if (!apiEndpoint) {
141
+ throw new Error('KTDataTable: apiEndpoint is required for remote fetch');
142
+ }
140
143
 
141
144
  if (this.abortController) {
142
145
  this.abortController.abort();