@keenthemes/ktui 1.2.6 → 1.2.7

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 (190) hide show
  1. package/README.md +14 -5
  2. package/dist/ktui.js +3775 -2298
  3. package/dist/ktui.min.js +1 -1
  4. package/dist/ktui.min.js.map +1 -1
  5. package/dist/styles.css +25 -5
  6. package/lib/cjs/components/datatable/datatable-checkbox.d.ts +37 -1
  7. package/lib/cjs/components/datatable/datatable-checkbox.d.ts.map +1 -1
  8. package/lib/cjs/components/datatable/datatable-checkbox.js +143 -156
  9. package/lib/cjs/components/datatable/datatable-checkbox.js.map +1 -1
  10. package/lib/cjs/components/datatable/datatable-column-utils.d.ts +30 -0
  11. package/lib/cjs/components/datatable/datatable-column-utils.d.ts.map +1 -0
  12. package/lib/cjs/components/datatable/datatable-column-utils.js +42 -0
  13. package/lib/cjs/components/datatable/datatable-column-utils.js.map +1 -0
  14. package/lib/cjs/components/datatable/datatable-contracts.d.ts +2 -4
  15. package/lib/cjs/components/datatable/datatable-contracts.d.ts.map +1 -1
  16. package/lib/cjs/components/datatable/datatable-defaults.d.ts +20 -0
  17. package/lib/cjs/components/datatable/datatable-defaults.d.ts.map +1 -0
  18. package/lib/cjs/components/datatable/datatable-defaults.js +193 -0
  19. package/lib/cjs/components/datatable/datatable-defaults.js.map +1 -0
  20. package/lib/cjs/components/datatable/datatable-layout-plugin.d.ts.map +1 -1
  21. package/lib/cjs/components/datatable/datatable-layout-plugin.js +11 -1
  22. package/lib/cjs/components/datatable/datatable-layout-plugin.js.map +1 -1
  23. package/lib/cjs/components/datatable/datatable-local-provider.d.ts.map +1 -1
  24. package/lib/cjs/components/datatable/datatable-local-provider.js +80 -24
  25. package/lib/cjs/components/datatable/datatable-local-provider.js.map +1 -1
  26. package/lib/cjs/components/datatable/datatable-pagination-renderer.d.ts.map +1 -1
  27. package/lib/cjs/components/datatable/datatable-pagination-renderer.js +3 -2
  28. package/lib/cjs/components/datatable/datatable-pagination-renderer.js.map +1 -1
  29. package/lib/cjs/components/datatable/datatable-registry.d.ts +18 -0
  30. package/lib/cjs/components/datatable/datatable-registry.d.ts.map +1 -0
  31. package/lib/cjs/components/datatable/datatable-registry.js +66 -0
  32. package/lib/cjs/components/datatable/datatable-registry.js.map +1 -0
  33. package/lib/cjs/components/datatable/datatable-remote-provider.d.ts.map +1 -1
  34. package/lib/cjs/components/datatable/datatable-remote-provider.js +1 -2
  35. package/lib/cjs/components/datatable/datatable-remote-provider.js.map +1 -1
  36. package/lib/cjs/components/datatable/datatable-search-handler.d.ts +10 -0
  37. package/lib/cjs/components/datatable/datatable-search-handler.d.ts.map +1 -0
  38. package/lib/cjs/components/datatable/datatable-search-handler.js +65 -0
  39. package/lib/cjs/components/datatable/datatable-search-handler.js.map +1 -0
  40. package/lib/cjs/components/datatable/datatable-sort.d.ts +31 -4
  41. package/lib/cjs/components/datatable/datatable-sort.d.ts.map +1 -1
  42. package/lib/cjs/components/datatable/datatable-sort.js +86 -58
  43. package/lib/cjs/components/datatable/datatable-sort.js.map +1 -1
  44. package/lib/cjs/components/datatable/datatable-spinner.d.ts +30 -0
  45. package/lib/cjs/components/datatable/datatable-spinner.d.ts.map +1 -0
  46. package/lib/cjs/components/datatable/datatable-spinner.js +54 -0
  47. package/lib/cjs/components/datatable/datatable-spinner.js.map +1 -0
  48. package/lib/cjs/components/datatable/datatable-state-persistence.d.ts +19 -0
  49. package/lib/cjs/components/datatable/datatable-state-persistence.d.ts.map +1 -0
  50. package/lib/cjs/components/datatable/datatable-state-persistence.js +59 -0
  51. package/lib/cjs/components/datatable/datatable-state-persistence.js.map +1 -0
  52. package/lib/cjs/components/datatable/datatable-table-renderer.d.ts +2 -0
  53. package/lib/cjs/components/datatable/datatable-table-renderer.d.ts.map +1 -1
  54. package/lib/cjs/components/datatable/datatable-table-renderer.js +75 -16
  55. package/lib/cjs/components/datatable/datatable-table-renderer.js.map +1 -1
  56. package/lib/cjs/components/datatable/datatable-utils.d.ts +10 -0
  57. package/lib/cjs/components/datatable/datatable-utils.d.ts.map +1 -0
  58. package/lib/cjs/components/datatable/datatable-utils.js +15 -0
  59. package/lib/cjs/components/datatable/datatable-utils.js.map +1 -0
  60. package/lib/cjs/components/datatable/datatable.d.ts +26 -34
  61. package/lib/cjs/components/datatable/datatable.d.ts.map +1 -1
  62. package/lib/cjs/components/datatable/datatable.js +155 -492
  63. package/lib/cjs/components/datatable/datatable.js.map +1 -1
  64. package/lib/cjs/components/datatable/index.d.ts +1 -1
  65. package/lib/cjs/components/datatable/index.d.ts.map +1 -1
  66. package/lib/cjs/components/datatable/types.d.ts +100 -11
  67. package/lib/cjs/components/datatable/types.d.ts.map +1 -1
  68. package/lib/cjs/index.d.ts +1 -1
  69. package/lib/cjs/index.d.ts.map +1 -1
  70. package/lib/cjs/index.js +6 -0
  71. package/lib/cjs/index.js.map +1 -1
  72. package/lib/esm/components/datatable/datatable-checkbox.d.ts +37 -1
  73. package/lib/esm/components/datatable/datatable-checkbox.d.ts.map +1 -1
  74. package/lib/esm/components/datatable/datatable-checkbox.js +142 -155
  75. package/lib/esm/components/datatable/datatable-checkbox.js.map +1 -1
  76. package/lib/esm/components/datatable/datatable-column-utils.d.ts +30 -0
  77. package/lib/esm/components/datatable/datatable-column-utils.d.ts.map +1 -0
  78. package/lib/esm/components/datatable/datatable-column-utils.js +38 -0
  79. package/lib/esm/components/datatable/datatable-column-utils.js.map +1 -0
  80. package/lib/esm/components/datatable/datatable-contracts.d.ts +2 -4
  81. package/lib/esm/components/datatable/datatable-contracts.d.ts.map +1 -1
  82. package/lib/esm/components/datatable/datatable-defaults.d.ts +20 -0
  83. package/lib/esm/components/datatable/datatable-defaults.d.ts.map +1 -0
  84. package/lib/esm/components/datatable/datatable-defaults.js +190 -0
  85. package/lib/esm/components/datatable/datatable-defaults.js.map +1 -0
  86. package/lib/esm/components/datatable/datatable-layout-plugin.d.ts.map +1 -1
  87. package/lib/esm/components/datatable/datatable-layout-plugin.js +11 -1
  88. package/lib/esm/components/datatable/datatable-layout-plugin.js.map +1 -1
  89. package/lib/esm/components/datatable/datatable-local-provider.d.ts.map +1 -1
  90. package/lib/esm/components/datatable/datatable-local-provider.js +80 -24
  91. package/lib/esm/components/datatable/datatable-local-provider.js.map +1 -1
  92. package/lib/esm/components/datatable/datatable-pagination-renderer.d.ts.map +1 -1
  93. package/lib/esm/components/datatable/datatable-pagination-renderer.js +3 -2
  94. package/lib/esm/components/datatable/datatable-pagination-renderer.js.map +1 -1
  95. package/lib/esm/components/datatable/datatable-registry.d.ts +18 -0
  96. package/lib/esm/components/datatable/datatable-registry.d.ts.map +1 -0
  97. package/lib/esm/components/datatable/datatable-registry.js +63 -0
  98. package/lib/esm/components/datatable/datatable-registry.js.map +1 -0
  99. package/lib/esm/components/datatable/datatable-remote-provider.d.ts.map +1 -1
  100. package/lib/esm/components/datatable/datatable-remote-provider.js +1 -2
  101. package/lib/esm/components/datatable/datatable-remote-provider.js.map +1 -1
  102. package/lib/esm/components/datatable/datatable-search-handler.d.ts +10 -0
  103. package/lib/esm/components/datatable/datatable-search-handler.d.ts.map +1 -0
  104. package/lib/esm/components/datatable/datatable-search-handler.js +62 -0
  105. package/lib/esm/components/datatable/datatable-search-handler.js.map +1 -0
  106. package/lib/esm/components/datatable/datatable-sort.d.ts +31 -4
  107. package/lib/esm/components/datatable/datatable-sort.d.ts.map +1 -1
  108. package/lib/esm/components/datatable/datatable-sort.js +85 -57
  109. package/lib/esm/components/datatable/datatable-sort.js.map +1 -1
  110. package/lib/esm/components/datatable/datatable-spinner.d.ts +30 -0
  111. package/lib/esm/components/datatable/datatable-spinner.d.ts.map +1 -0
  112. package/lib/esm/components/datatable/datatable-spinner.js +51 -0
  113. package/lib/esm/components/datatable/datatable-spinner.js.map +1 -0
  114. package/lib/esm/components/datatable/datatable-state-persistence.d.ts +19 -0
  115. package/lib/esm/components/datatable/datatable-state-persistence.d.ts.map +1 -0
  116. package/lib/esm/components/datatable/datatable-state-persistence.js +55 -0
  117. package/lib/esm/components/datatable/datatable-state-persistence.js.map +1 -0
  118. package/lib/esm/components/datatable/datatable-table-renderer.d.ts +2 -0
  119. package/lib/esm/components/datatable/datatable-table-renderer.d.ts.map +1 -1
  120. package/lib/esm/components/datatable/datatable-table-renderer.js +75 -16
  121. package/lib/esm/components/datatable/datatable-table-renderer.js.map +1 -1
  122. package/lib/esm/components/datatable/datatable-utils.d.ts +10 -0
  123. package/lib/esm/components/datatable/datatable-utils.d.ts.map +1 -0
  124. package/lib/esm/components/datatable/datatable-utils.js +12 -0
  125. package/lib/esm/components/datatable/datatable-utils.js.map +1 -0
  126. package/lib/esm/components/datatable/datatable.d.ts +26 -34
  127. package/lib/esm/components/datatable/datatable.d.ts.map +1 -1
  128. package/lib/esm/components/datatable/datatable.js +157 -494
  129. package/lib/esm/components/datatable/datatable.js.map +1 -1
  130. package/lib/esm/components/datatable/index.d.ts +1 -1
  131. package/lib/esm/components/datatable/index.d.ts.map +1 -1
  132. package/lib/esm/components/datatable/types.d.ts +100 -11
  133. package/lib/esm/components/datatable/types.d.ts.map +1 -1
  134. package/lib/esm/index.d.ts +1 -1
  135. package/lib/esm/index.d.ts.map +1 -1
  136. package/lib/esm/index.js +6 -0
  137. package/lib/esm/index.js.map +1 -1
  138. package/package.json +5 -1
  139. package/skills/ktui/SKILL.md +711 -0
  140. package/skills/ktui-datatable/SKILL.md +302 -0
  141. package/skills/ktui-install/SKILL.md +150 -0
  142. package/skills/ktui-select/SKILL.md +271 -0
  143. package/src/components/__tests__/component.test.ts +347 -0
  144. package/src/components/collapse/collapse.css +2 -2
  145. package/src/components/datatable/__tests__/architecture-boundaries.test.ts +56 -8
  146. package/src/components/datatable/__tests__/currency-sort.test.ts +25 -28
  147. package/src/components/datatable/__tests__/datatable-checkbox.test.ts +527 -0
  148. package/src/components/datatable/__tests__/datatable-column-utils.test.ts +117 -0
  149. package/src/components/datatable/__tests__/datatable-defaults.test.ts +57 -0
  150. package/src/components/datatable/__tests__/datatable-finalize-extended.test.ts +361 -0
  151. package/src/components/datatable/__tests__/datatable-fixed-layout.test.ts +427 -0
  152. package/src/components/datatable/__tests__/datatable-improvements.test.ts +484 -0
  153. package/src/components/datatable/__tests__/datatable-pagination-extended.test.ts +508 -0
  154. package/src/components/datatable/__tests__/datatable-public-api.test.ts +269 -0
  155. package/src/components/datatable/__tests__/datatable-registry.test.ts +172 -0
  156. package/src/components/datatable/__tests__/datatable-remote-provider.test.ts +468 -0
  157. package/src/components/datatable/__tests__/datatable-search-handler.test.ts +124 -0
  158. package/src/components/datatable/__tests__/datatable-sort-extended.test.ts +417 -0
  159. package/src/components/datatable/__tests__/datatable-spinner.test.ts +95 -0
  160. package/src/components/datatable/__tests__/datatable-table-renderer-extended.test.ts +425 -0
  161. package/src/components/datatable/__tests__/datatable-types.test.ts +117 -0
  162. package/src/components/datatable/__tests__/datatable-utils.test.ts +52 -0
  163. package/src/components/datatable/__tests__/multi-row-headers.test.ts +7 -7
  164. package/src/components/datatable/__tests__/pagination-reset.test.ts +129 -6
  165. package/src/components/datatable/__tests__/race-conditions.test.ts +11 -11
  166. package/src/components/datatable/__tests__/setup.ts +12 -4
  167. package/src/components/datatable/datatable-checkbox.ts +144 -145
  168. package/src/components/datatable/datatable-column-utils.ts +63 -0
  169. package/src/components/datatable/datatable-contracts.ts +2 -3
  170. package/src/components/datatable/datatable-defaults.ts +204 -0
  171. package/src/components/datatable/datatable-layout-plugin.ts +11 -1
  172. package/src/components/datatable/datatable-local-provider.ts +91 -28
  173. package/src/components/datatable/datatable-pagination-renderer.ts +3 -2
  174. package/src/components/datatable/datatable-registry.ts +89 -0
  175. package/src/components/datatable/datatable-remote-provider.ts +1 -3
  176. package/src/components/datatable/datatable-search-handler.ts +97 -0
  177. package/src/components/datatable/datatable-sort.ts +111 -66
  178. package/src/components/datatable/datatable-spinner.ts +103 -0
  179. package/src/components/datatable/datatable-state-persistence.ts +67 -0
  180. package/src/components/datatable/datatable-table-renderer.ts +81 -18
  181. package/src/components/datatable/datatable-utils.ts +12 -0
  182. package/src/components/datatable/datatable.ts +191 -580
  183. package/src/components/datatable/index.ts +3 -0
  184. package/src/components/datatable/types.ts +124 -23
  185. package/src/helpers/__tests__/dom.test.ts +776 -0
  186. package/src/helpers/__tests__/utils.test.ts +332 -0
  187. package/src/index.ts +10 -0
  188. package/skills/ktui-components/SKILL.md +0 -41
  189. package/skills/ktui-theming/SKILL.md +0 -50
  190. package/src/components/datatable/datatable-event-adapter.ts +0 -21
@@ -0,0 +1,425 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { KTDataTableDomTableRenderer } from '../datatable-table-renderer';
3
+
4
+ function createTableElement() {
5
+ const table = document.createElement('table');
6
+ const thead = table.createTHead();
7
+ const theadRow = document.createElement('tr');
8
+ const th1 = document.createElement('th');
9
+ th1.setAttribute('data-kt-datatable-column', 'name');
10
+ th1.textContent = 'Name';
11
+ const th2 = document.createElement('th');
12
+ th2.setAttribute('data-kt-datatable-column', 'age');
13
+ th2.textContent = 'Age';
14
+ theadRow.appendChild(th1);
15
+ theadRow.appendChild(th2);
16
+ thead.appendChild(theadRow);
17
+ return { table, thead };
18
+ }
19
+
20
+ function createRendererInput(overrides: Record<string, unknown> = {}) {
21
+ const { table, thead } = createTableElement();
22
+ return {
23
+ config: {
24
+ infoEmpty: 'No records found',
25
+ pageSize: 10,
26
+ ...((overrides.config as object) || {}),
27
+ },
28
+ context: {} as never,
29
+ data: (overrides.data as never[]) || [],
30
+ getLogicalColumnCount: (overrides.getLogicalColumnCount as () => number) || (() => 2),
31
+ getState: (overrides.getState as () => never) || (() => ({
32
+ page: 1,
33
+ pageSize: 10,
34
+ totalItems: 0,
35
+ totalPages: 0,
36
+ sortField: null,
37
+ sortOrder: '',
38
+ selectedRows: [],
39
+ filters: [],
40
+ search: '',
41
+ originalData: [],
42
+ originalDataAttributes: [],
43
+ })),
44
+ originalClasses: (overrides.originalClasses as never) || {
45
+ tbody: '',
46
+ thead: '',
47
+ tr: [],
48
+ td: [],
49
+ th: [],
50
+ },
51
+ tableElement: table,
52
+ theadElement: thead,
53
+ ...overrides,
54
+ };
55
+ }
56
+
57
+ describe('KTDataTableDomTableRenderer', () => {
58
+ let renderer: KTDataTableDomTableRenderer<Record<string, unknown>>;
59
+
60
+ beforeEach(() => {
61
+ document.body.innerHTML = '';
62
+ renderer = new KTDataTableDomTableRenderer();
63
+ });
64
+
65
+ describe('render with empty data', () => {
66
+ it('shows "No records found" message when data is empty', () => {
67
+ const input = createRendererInput({
68
+ data: [],
69
+ config: { infoEmpty: 'No records found' },
70
+ });
71
+
72
+ const tbody = renderer.render(input as never);
73
+ expect(tbody.rows.length).toBe(1);
74
+ expect(tbody.rows[0].cells[0].innerHTML).toBe('No records found');
75
+ });
76
+
77
+ it('shows custom infoEmpty message', () => {
78
+ const input = createRendererInput({
79
+ data: [],
80
+ config: { infoEmpty: 'Custom empty message' },
81
+ });
82
+
83
+ const tbody = renderer.render(input as never);
84
+ expect(tbody.rows[0].cells[0].innerHTML).toBe('Custom empty message');
85
+ });
86
+
87
+ it('shows empty string when infoEmpty is empty', () => {
88
+ const input = createRendererInput({
89
+ data: [],
90
+ config: { infoEmpty: '' },
91
+ });
92
+
93
+ const tbody = renderer.render(input as never);
94
+ expect(tbody.rows[0].cells[0].innerHTML).toBe('');
95
+ });
96
+ });
97
+
98
+ describe('render with data', () => {
99
+ it('populates tbody with rows', () => {
100
+ const input = createRendererInput({
101
+ data: [
102
+ { name: 'Alice', age: '30' },
103
+ { name: 'Bob', age: '25' },
104
+ ],
105
+ });
106
+
107
+ const tbody = renderer.render(input as never);
108
+ expect(tbody.rows.length).toBe(2);
109
+ expect(tbody.rows[0].cells[0].textContent).toBe('Alice');
110
+ expect(tbody.rows[0].cells[1].textContent).toBe('30');
111
+ expect(tbody.rows[1].cells[0].textContent).toBe('Bob');
112
+ expect(tbody.rows[1].cells[1].textContent).toBe('25');
113
+ });
114
+
115
+ it('creates correct number of columns', () => {
116
+ const input = createRendererInput({
117
+ data: [{ name: 'Alice', age: '30' }],
118
+ });
119
+
120
+ const tbody = renderer.render(input as never);
121
+ expect(tbody.rows[0].cells.length).toBe(2);
122
+ });
123
+ });
124
+
125
+ describe('render with configured columns', () => {
126
+ it('uses column render function when provided', () => {
127
+ const renderFn = vi.fn().mockReturnValue('<b>Bold</b>');
128
+ const input = createRendererInput({
129
+ data: [{ name: 'Alice', age: '30' }],
130
+ config: {
131
+ infoEmpty: 'No records found',
132
+ columns: {
133
+ name: { render: renderFn },
134
+ },
135
+ },
136
+ });
137
+
138
+ const tbody = renderer.render(input as never);
139
+ expect(renderFn).toHaveBeenCalledWith(
140
+ 'Alice',
141
+ { name: 'Alice', age: '30' },
142
+ expect.anything(),
143
+ );
144
+ expect(tbody.rows[0].cells[0].innerHTML).toBe('<b>Bold</b>');
145
+ });
146
+
147
+ it('uses column render function returning HTMLElement', () => {
148
+ const input = createRendererInput({
149
+ data: [{ name: 'Alice', age: '30' }],
150
+ config: {
151
+ infoEmpty: 'No records found',
152
+ columns: {
153
+ name: {
154
+ render: () => {
155
+ const el = document.createElement('span');
156
+ el.textContent = 'Custom';
157
+ return el;
158
+ },
159
+ },
160
+ },
161
+ },
162
+ });
163
+
164
+ const tbody = renderer.render(input as never);
165
+ expect(tbody.rows[0].cells[0].querySelector('span')).toBeTruthy();
166
+ expect(tbody.rows[0].cells[0].querySelector('span')!.textContent).toBe(
167
+ 'Custom',
168
+ );
169
+ });
170
+
171
+ it('calls createdCell callback', () => {
172
+ const createdCell = vi.fn();
173
+ const input = createRendererInput({
174
+ data: [{ name: 'Alice', age: '30' }],
175
+ config: {
176
+ infoEmpty: 'No records found',
177
+ columns: {
178
+ name: { createdCell },
179
+ },
180
+ },
181
+ });
182
+
183
+ renderer.render(input as never);
184
+ expect(createdCell).toHaveBeenCalledWith(
185
+ expect.any(HTMLTableCellElement),
186
+ 'Alice',
187
+ { name: 'Alice', age: '30' },
188
+ expect.any(HTMLTableRowElement),
189
+ );
190
+ });
191
+
192
+ it('renders innerHTML when no render function', () => {
193
+ const input = createRendererInput({
194
+ data: [{ name: 'Alice', age: '30' }],
195
+ config: {
196
+ infoEmpty: 'No records found',
197
+ columns: {
198
+ name: {},
199
+ },
200
+ },
201
+ });
202
+
203
+ const tbody = renderer.render(input as never);
204
+ expect(tbody.rows[0].cells[0].innerHTML).toBe('Alice');
205
+ expect(tbody.rows[0].cells[0].textContent).toBe('Alice');
206
+ });
207
+
208
+ it('preserves HTML markup when no render function', () => {
209
+ const input = createRendererInput({
210
+ data: [
211
+ {
212
+ status:
213
+ '<span class="kt-badge kt-badge-success">Approved</span>',
214
+ },
215
+ ],
216
+ config: {
217
+ infoEmpty: 'No records found',
218
+ columns: {
219
+ status: {},
220
+ },
221
+ },
222
+ });
223
+
224
+ const tbody = renderer.render(input as never);
225
+ const badge = tbody.rows[0].cells[0].querySelector('.kt-badge');
226
+ expect(badge).toBeTruthy();
227
+ expect(badge!.textContent).toBe('Approved');
228
+ });
229
+ });
230
+
231
+ describe('render with originalClasses', () => {
232
+ it('applies original tbody class', () => {
233
+ const input = createRendererInput({
234
+ data: [{ name: 'Alice', age: '30' }],
235
+ originalClasses: {
236
+ tbody: 'custom-tbody',
237
+ thead: '',
238
+ tr: [],
239
+ td: [],
240
+ th: [],
241
+ },
242
+ });
243
+
244
+ const tbody = renderer.render(input as never);
245
+ expect(tbody.className).toBe('custom-tbody');
246
+ });
247
+
248
+ it('applies original tr classes', () => {
249
+ const input = createRendererInput({
250
+ data: [
251
+ { name: 'Alice', age: '30' },
252
+ { name: 'Bob', age: '25' },
253
+ ],
254
+ originalClasses: {
255
+ tbody: '',
256
+ thead: '',
257
+ tr: ['row-0', 'row-1'],
258
+ td: [],
259
+ th: [],
260
+ },
261
+ });
262
+
263
+ const tbody = renderer.render(input as never);
264
+ expect(tbody.rows[0].className).toBe('row-0');
265
+ expect(tbody.rows[1].className).toBe('row-1');
266
+ });
267
+
268
+ it('applies original td classes', () => {
269
+ const input = createRendererInput({
270
+ data: [{ name: 'Alice', age: '30' }],
271
+ originalClasses: {
272
+ tbody: '',
273
+ thead: '',
274
+ tr: [],
275
+ td: [['td-name', 'td-age']],
276
+ th: [],
277
+ },
278
+ });
279
+
280
+ const tbody = renderer.render(input as never);
281
+ expect(tbody.rows[0].cells[0].className).toBe('td-name');
282
+ expect(tbody.rows[0].cells[1].className).toBe('td-age');
283
+ });
284
+ });
285
+
286
+ describe('render with data-kt-datatable-column on th', () => {
287
+ it('matches columns by data-kt-datatable-column attribute', () => {
288
+ const input = createRendererInput({
289
+ data: [{ name: 'Alice', age: '30' }],
290
+ });
291
+
292
+ const tbody = renderer.render(input as never);
293
+ expect(tbody.rows[0].cells[0].textContent).toBe('Alice');
294
+ expect(tbody.rows[0].cells[1].textContent).toBe('30');
295
+ });
296
+
297
+ it('falls back to column index when data attribute missing', () => {
298
+ // Create table without data-kt-datatable-column attributes
299
+ const table = document.createElement('table');
300
+ const thead = table.createTHead();
301
+ const theadRow = document.createElement('tr');
302
+ const th1 = document.createElement('th');
303
+ th1.textContent = 'Name';
304
+ const th2 = document.createElement('th');
305
+ th2.textContent = 'Age';
306
+ theadRow.appendChild(th1);
307
+ theadRow.appendChild(th2);
308
+ thead.appendChild(theadRow);
309
+
310
+ const input = createRendererInput({
311
+ data: [
312
+ { '0': 'Alice', '1': '30' },
313
+ ],
314
+ tableElement: table,
315
+ theadElement: thead,
316
+ });
317
+
318
+ const tbody = renderer.render(input as never);
319
+ expect(tbody.rows[0].cells[0].textContent).toBe('Alice');
320
+ });
321
+ });
322
+
323
+ describe('render with dataRowAttributes', () => {
324
+ it('applies data row attributes to td elements', () => {
325
+ const input = createRendererInput({
326
+ data: [{ name: 'Alice', age: '30' }],
327
+ getState: () => ({
328
+ page: 1,
329
+ pageSize: 10,
330
+ totalItems: 1,
331
+ totalPages: 1,
332
+ sortField: null,
333
+ sortOrder: '',
334
+ selectedRows: [],
335
+ filters: [],
336
+ search: '',
337
+ originalData: [],
338
+ originalDataAttributes: [
339
+ { 0: { 'data-custom': 'val1' }, 1: { 'data-custom': 'val2' } },
340
+ ],
341
+ }),
342
+ });
343
+
344
+ const tbody = renderer.render(input as never);
345
+ expect(tbody.rows[0].cells[0].getAttribute('data-custom')).toBe('val1');
346
+ expect(tbody.rows[0].cells[1].getAttribute('data-custom')).toBe('val2');
347
+ });
348
+ });
349
+
350
+ describe('render with afterDraw callback', () => {
351
+ it('afterDraw is called with table element in context', () => {
352
+ // The afterDraw callback is handled by the main datatable, not the renderer directly.
353
+ // But we can verify the renderer returns a valid tbody that could be used in afterDraw.
354
+ const input = createRendererInput({
355
+ data: [{ name: 'Alice', age: '30' }],
356
+ });
357
+
358
+ const tbody = renderer.render(input as never);
359
+ expect(tbody).toBeInstanceOf(HTMLTableSectionElement);
360
+ });
361
+ });
362
+
363
+ describe('render re-creates tbody on each call', () => {
364
+ it('removes old tbody and creates new one', () => {
365
+ const input = createRendererInput({
366
+ data: [{ name: 'Alice', age: '30' }],
367
+ });
368
+
369
+ const tbody1 = renderer.render(input as never);
370
+ input.tableElement.appendChild(tbody1);
371
+ expect(input.tableElement.tBodies.length).toBe(1);
372
+
373
+ // Render again with new data
374
+ input.data = [
375
+ { name: 'Bob', age: '25' },
376
+ { name: 'Charlie', age: '35' },
377
+ ];
378
+ const tbody2 = renderer.render(input as never);
379
+ expect(input.tableElement.tBodies.length).toBe(1);
380
+ expect(tbody2.rows.length).toBe(2);
381
+ expect(tbody2.rows[0].cells[0].textContent).toBe('Bob');
382
+ });
383
+ });
384
+
385
+ describe('notice', () => {
386
+ it('adds a notice row to tbody', () => {
387
+ const table = document.createElement('table');
388
+ table.createTBody();
389
+
390
+ renderer.notice(table, () => 3, 'Custom message');
391
+ const tbody = table.tBodies[0];
392
+ expect(tbody.rows.length).toBe(1);
393
+ expect(tbody.rows[0].cells[0].innerHTML).toBe('Custom message');
394
+ expect(tbody.rows[0].cells[0].colSpan).toBe(3);
395
+ });
396
+
397
+ it('uses colSpan of 1 when logical count is 0', () => {
398
+ const table = document.createElement('table');
399
+ table.createTBody();
400
+
401
+ renderer.notice(table, () => 0, 'Empty');
402
+ expect(table.tBodies[0].rows[0].cells[0].colSpan).toBe(1);
403
+ });
404
+
405
+ it('defaults to empty message when not provided', () => {
406
+ const table = document.createElement('table');
407
+ table.createTBody();
408
+
409
+ renderer.notice(table, () => 2);
410
+ expect(table.tBodies[0].rows[0].cells[0].innerHTML).toBe('');
411
+ });
412
+ });
413
+
414
+ describe('render with value missing from item', () => {
415
+ it('uses empty string when column not in item', () => {
416
+ const input = createRendererInput({
417
+ data: [{ name: 'Alice' }], // no 'age' key
418
+ });
419
+
420
+ const tbody = renderer.render(input as never);
421
+ expect(tbody.rows[0].cells[0].textContent).toBe('Alice');
422
+ expect(tbody.rows[0].cells[1].innerHTML).toBe('');
423
+ });
424
+ });
425
+ });
@@ -0,0 +1,117 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import type {
3
+ KTDataTableColumnFilterInterface,
4
+ KTDataTableTextFilterInterface,
5
+ KTDataTableNumericFilterInterface,
6
+ KTDataTableDateRangeFilterInterface,
7
+ KTDataTableConfigInterface,
8
+ KTDataTableDataInterface,
9
+ } from '../types';
10
+
11
+ describe('Discriminated Union - KTDataTableColumnFilterInterface', () => {
12
+ it('should accept text filter with string value', () => {
13
+ const filter: KTDataTableColumnFilterInterface = {
14
+ column: 'name',
15
+ type: 'text',
16
+ value: 'John',
17
+ };
18
+ expect(filter.type).toBe('text');
19
+ expect(typeof filter.value).toBe('string');
20
+ });
21
+
22
+ it('should accept numeric filter with number value', () => {
23
+ const filter: KTDataTableColumnFilterInterface = {
24
+ column: 'id',
25
+ type: 'numeric',
26
+ value: 42,
27
+ };
28
+ expect(filter.type).toBe('numeric');
29
+ expect(typeof filter.value).toBe('number');
30
+ });
31
+
32
+ it('should accept dateRange filter with from/to value', () => {
33
+ const filter: KTDataTableColumnFilterInterface = {
34
+ column: 'date',
35
+ type: 'dateRange',
36
+ value: { from: '2024-01-01', to: '2024-12-31' },
37
+ };
38
+ expect(filter.type).toBe('dateRange');
39
+ expect(filter.value).toHaveProperty('from');
40
+ expect(filter.value).toHaveProperty('to');
41
+ });
42
+
43
+ it('should narrow type correctly for text filter', () => {
44
+ const filter: KTDataTableColumnFilterInterface = {
45
+ column: 'name',
46
+ type: 'text',
47
+ value: 'test',
48
+ };
49
+ if (filter.type === 'text') {
50
+ expect(typeof filter.value).toBe('string');
51
+ }
52
+ });
53
+
54
+ it('should narrow type correctly for numeric filter', () => {
55
+ const filter: KTDataTableColumnFilterInterface = {
56
+ column: 'id',
57
+ type: 'numeric',
58
+ value: 10,
59
+ };
60
+ if (filter.type === 'numeric') {
61
+ expect(typeof filter.value).toBe('number');
62
+ }
63
+ });
64
+
65
+ it('should narrow type correctly for dateRange filter', () => {
66
+ const filter: KTDataTableColumnFilterInterface = {
67
+ column: 'date',
68
+ type: 'dateRange',
69
+ value: { from: '2024-01-01', to: '2024-12-31' },
70
+ };
71
+ if (filter.type === 'dateRange') {
72
+ expect(filter.value).toHaveProperty('from');
73
+ expect(filter.value).toHaveProperty('to');
74
+ }
75
+ });
76
+ });
77
+
78
+ describe('Generic KTDataTableConfigInterface<T>', () => {
79
+ it('should work without type parameter (backward compatible)', () => {
80
+ const config: KTDataTableConfigInterface = {
81
+ pageSize: 10,
82
+ stateSave: false,
83
+ };
84
+ expect(config.pageSize).toBe(10);
85
+ });
86
+
87
+ it('should work with custom type parameter', () => {
88
+ interface MyData extends KTDataTableDataInterface {
89
+ id: number;
90
+ name: string;
91
+ email: string;
92
+ }
93
+
94
+ const config: KTDataTableConfigInterface<MyData> = {
95
+ pageSize: 20,
96
+ columns: {
97
+ name: {
98
+ title: 'Name',
99
+ render: (item, data) => {
100
+ return String(item);
101
+ },
102
+ },
103
+ },
104
+ sort: {
105
+ callback: (data, sortField, sortOrder) => {
106
+ return data;
107
+ },
108
+ },
109
+ search: {
110
+ callback: (data, search) => {
111
+ return data;
112
+ },
113
+ },
114
+ };
115
+ expect(config.pageSize).toBe(20);
116
+ });
117
+ });
@@ -0,0 +1,52 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { stripHtml } from '../datatable-utils';
3
+
4
+ describe('stripHtml', () => {
5
+ it('strips simple HTML tags', () => {
6
+ expect(stripHtml('<b>hello</b>')).toBe('hello');
7
+ });
8
+
9
+ it('strips nested HTML tags', () => {
10
+ expect(stripHtml('<div><span>text</span></div>')).toBe('text');
11
+ });
12
+
13
+ it('strips &nbsp; entities', () => {
14
+ expect(stripHtml('no&nbsp;spaces')).toBe('nospaces');
15
+ });
16
+
17
+ it('strips self-closing tags', () => {
18
+ expect(stripHtml('<img src="x"><p>text</p>')).toBe('text');
19
+ });
20
+
21
+ it('handles plain string without HTML', () => {
22
+ expect(stripHtml('just text')).toBe('just text');
23
+ });
24
+
25
+ it('handles empty string', () => {
26
+ expect(stripHtml('')).toBe('');
27
+ });
28
+
29
+ it('converts number to string', () => {
30
+ expect(stripHtml(123)).toBe('123');
31
+ });
32
+
33
+ it('converts boolean to string', () => {
34
+ expect(stripHtml(true)).toBe('true');
35
+ });
36
+
37
+ it('converts null to string', () => {
38
+ expect(stripHtml(null)).toBe('null');
39
+ });
40
+
41
+ it('converts undefined to string', () => {
42
+ expect(stripHtml(undefined)).toBe('undefined');
43
+ });
44
+
45
+ it('strips multiple tags in one string', () => {
46
+ expect(stripHtml('<b>bold</b> and <i>italic</i>')).toBe('bold and italic');
47
+ });
48
+
49
+ it('handles malformed HTML', () => {
50
+ expect(stripHtml('<div>unclosed')).toBe('unclosed');
51
+ });
52
+ });
@@ -30,23 +30,23 @@ describe('KTDataTable - Multi-row header column count', () => {
30
30
  const thead = document.createElement('thead');
31
31
  const row1 = document.createElement('tr');
32
32
  row1.innerHTML = `
33
- <th rowspan="2">Person</th>
33
+ <th rowspan="2" data-kt-datatable-column="person">Person</th>
34
34
  <th colspan="3">Backlog</th>
35
35
  <th colspan="3">Floater (2) 2025</th>
36
36
  <th colspan="3">Floater (1) 2025</th>
37
37
  <th colspan="3">CL2025</th>
38
38
  <th colspan="3">LWP</th>
39
- <th rowspan="2">Action</th>
39
+ <th rowspan="2" data-kt-datatable-column="action">Action</th>
40
40
  `;
41
41
  thead.appendChild(row1);
42
42
 
43
43
  const row2 = document.createElement('tr');
44
44
  row2.innerHTML = `
45
- <th>Assigned</th><th>Used</th><th>Balance</th>
46
- <th>Assigned</th><th>Used</th><th>Balance</th>
47
- <th>Assigned</th><th>Used</th><th>Balance</th>
48
- <th>Assigned</th><th>Used</th><th>Balance</th>
49
- <th>Assigned</th><th>Used</th><th>Balance</th>
45
+ <th data-kt-datatable-column="assigned1">Assigned</th><th data-kt-datatable-column="used1">Used</th><th data-kt-datatable-column="balance1">Balance</th>
46
+ <th data-kt-datatable-column="assigned2">Assigned</th><th data-kt-datatable-column="used2">Used</th><th data-kt-datatable-column="balance2">Balance</th>
47
+ <th data-kt-datatable-column="assigned3">Assigned</th><th data-kt-datatable-column="used3">Used</th><th data-kt-datatable-column="balance3">Balance</th>
48
+ <th data-kt-datatable-column="assigned4">Assigned</th><th data-kt-datatable-column="used4">Used</th><th data-kt-datatable-column="balance4">Balance</th>
49
+ <th data-kt-datatable-column="assigned5">Assigned</th><th data-kt-datatable-column="used5">Used</th><th data-kt-datatable-column="balance5">Balance</th>
50
50
  `;
51
51
  thead.appendChild(row2);
52
52
  tableElement.appendChild(thead);