@keenthemes/ktui 1.2.5 → 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 (196) hide show
  1. package/README.md +14 -5
  2. package/dist/ktui.js +1538 -786
  3. package/dist/ktui.min.js +1 -1
  4. package/dist/ktui.min.js.map +1 -1
  5. package/dist/styles.css +85 -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 +7 -0
  21. package/lib/cjs/components/datatable/datatable-layout-plugin.d.ts.map +1 -0
  22. package/lib/cjs/components/datatable/datatable-layout-plugin.js +338 -0
  23. package/lib/cjs/components/datatable/datatable-layout-plugin.js.map +1 -0
  24. package/lib/cjs/components/datatable/datatable-local-provider.d.ts +2 -2
  25. package/lib/cjs/components/datatable/datatable-local-provider.d.ts.map +1 -1
  26. package/lib/cjs/components/datatable/datatable-local-provider.js +85 -27
  27. package/lib/cjs/components/datatable/datatable-local-provider.js.map +1 -1
  28. package/lib/cjs/components/datatable/datatable-pagination-renderer.d.ts.map +1 -1
  29. package/lib/cjs/components/datatable/datatable-pagination-renderer.js +13 -13
  30. package/lib/cjs/components/datatable/datatable-pagination-renderer.js.map +1 -1
  31. package/lib/cjs/components/datatable/datatable-registry.d.ts +18 -0
  32. package/lib/cjs/components/datatable/datatable-registry.d.ts.map +1 -0
  33. package/lib/cjs/components/datatable/datatable-registry.js +66 -0
  34. package/lib/cjs/components/datatable/datatable-registry.js.map +1 -0
  35. package/lib/cjs/components/datatable/datatable-remote-provider.d.ts.map +1 -1
  36. package/lib/cjs/components/datatable/datatable-remote-provider.js +1 -2
  37. package/lib/cjs/components/datatable/datatable-remote-provider.js.map +1 -1
  38. package/lib/cjs/components/datatable/datatable-search-handler.d.ts +10 -0
  39. package/lib/cjs/components/datatable/datatable-search-handler.d.ts.map +1 -0
  40. package/lib/cjs/components/datatable/datatable-search-handler.js +65 -0
  41. package/lib/cjs/components/datatable/datatable-search-handler.js.map +1 -0
  42. package/lib/cjs/components/datatable/datatable-sort.d.ts +31 -4
  43. package/lib/cjs/components/datatable/datatable-sort.d.ts.map +1 -1
  44. package/lib/cjs/components/datatable/datatable-sort.js +86 -58
  45. package/lib/cjs/components/datatable/datatable-sort.js.map +1 -1
  46. package/lib/cjs/components/datatable/datatable-spinner.d.ts +30 -0
  47. package/lib/cjs/components/datatable/datatable-spinner.d.ts.map +1 -0
  48. package/lib/cjs/components/datatable/datatable-spinner.js +54 -0
  49. package/lib/cjs/components/datatable/datatable-spinner.js.map +1 -0
  50. package/lib/cjs/components/datatable/datatable-state-persistence.d.ts +19 -0
  51. package/lib/cjs/components/datatable/datatable-state-persistence.d.ts.map +1 -0
  52. package/lib/cjs/components/datatable/datatable-state-persistence.js +59 -0
  53. package/lib/cjs/components/datatable/datatable-state-persistence.js.map +1 -0
  54. package/lib/cjs/components/datatable/datatable-table-renderer.d.ts +2 -0
  55. package/lib/cjs/components/datatable/datatable-table-renderer.d.ts.map +1 -1
  56. package/lib/cjs/components/datatable/datatable-table-renderer.js +75 -16
  57. package/lib/cjs/components/datatable/datatable-table-renderer.js.map +1 -1
  58. package/lib/cjs/components/datatable/datatable-utils.d.ts +10 -0
  59. package/lib/cjs/components/datatable/datatable-utils.d.ts.map +1 -0
  60. package/lib/cjs/components/datatable/datatable-utils.js +15 -0
  61. package/lib/cjs/components/datatable/datatable-utils.js.map +1 -0
  62. package/lib/cjs/components/datatable/datatable.d.ts +35 -34
  63. package/lib/cjs/components/datatable/datatable.d.ts.map +1 -1
  64. package/lib/cjs/components/datatable/datatable.js +233 -497
  65. package/lib/cjs/components/datatable/datatable.js.map +1 -1
  66. package/lib/cjs/components/datatable/index.d.ts +1 -1
  67. package/lib/cjs/components/datatable/index.d.ts.map +1 -1
  68. package/lib/cjs/components/datatable/types.d.ts +127 -11
  69. package/lib/cjs/components/datatable/types.d.ts.map +1 -1
  70. package/lib/cjs/index.d.ts +1 -1
  71. package/lib/cjs/index.d.ts.map +1 -1
  72. package/lib/cjs/index.js +6 -0
  73. package/lib/cjs/index.js.map +1 -1
  74. package/lib/esm/components/datatable/datatable-checkbox.d.ts +37 -1
  75. package/lib/esm/components/datatable/datatable-checkbox.d.ts.map +1 -1
  76. package/lib/esm/components/datatable/datatable-checkbox.js +142 -155
  77. package/lib/esm/components/datatable/datatable-checkbox.js.map +1 -1
  78. package/lib/esm/components/datatable/datatable-column-utils.d.ts +30 -0
  79. package/lib/esm/components/datatable/datatable-column-utils.d.ts.map +1 -0
  80. package/lib/esm/components/datatable/datatable-column-utils.js +38 -0
  81. package/lib/esm/components/datatable/datatable-column-utils.js.map +1 -0
  82. package/lib/esm/components/datatable/datatable-contracts.d.ts +2 -4
  83. package/lib/esm/components/datatable/datatable-contracts.d.ts.map +1 -1
  84. package/lib/esm/components/datatable/datatable-defaults.d.ts +20 -0
  85. package/lib/esm/components/datatable/datatable-defaults.d.ts.map +1 -0
  86. package/lib/esm/components/datatable/datatable-defaults.js +190 -0
  87. package/lib/esm/components/datatable/datatable-defaults.js.map +1 -0
  88. package/lib/esm/components/datatable/datatable-layout-plugin.d.ts +7 -0
  89. package/lib/esm/components/datatable/datatable-layout-plugin.d.ts.map +1 -0
  90. package/lib/esm/components/datatable/datatable-layout-plugin.js +334 -0
  91. package/lib/esm/components/datatable/datatable-layout-plugin.js.map +1 -0
  92. package/lib/esm/components/datatable/datatable-local-provider.d.ts +2 -2
  93. package/lib/esm/components/datatable/datatable-local-provider.d.ts.map +1 -1
  94. package/lib/esm/components/datatable/datatable-local-provider.js +85 -27
  95. package/lib/esm/components/datatable/datatable-local-provider.js.map +1 -1
  96. package/lib/esm/components/datatable/datatable-pagination-renderer.d.ts.map +1 -1
  97. package/lib/esm/components/datatable/datatable-pagination-renderer.js +13 -13
  98. package/lib/esm/components/datatable/datatable-pagination-renderer.js.map +1 -1
  99. package/lib/esm/components/datatable/datatable-registry.d.ts +18 -0
  100. package/lib/esm/components/datatable/datatable-registry.d.ts.map +1 -0
  101. package/lib/esm/components/datatable/datatable-registry.js +63 -0
  102. package/lib/esm/components/datatable/datatable-registry.js.map +1 -0
  103. package/lib/esm/components/datatable/datatable-remote-provider.d.ts.map +1 -1
  104. package/lib/esm/components/datatable/datatable-remote-provider.js +1 -2
  105. package/lib/esm/components/datatable/datatable-remote-provider.js.map +1 -1
  106. package/lib/esm/components/datatable/datatable-search-handler.d.ts +10 -0
  107. package/lib/esm/components/datatable/datatable-search-handler.d.ts.map +1 -0
  108. package/lib/esm/components/datatable/datatable-search-handler.js +62 -0
  109. package/lib/esm/components/datatable/datatable-search-handler.js.map +1 -0
  110. package/lib/esm/components/datatable/datatable-sort.d.ts +31 -4
  111. package/lib/esm/components/datatable/datatable-sort.d.ts.map +1 -1
  112. package/lib/esm/components/datatable/datatable-sort.js +85 -57
  113. package/lib/esm/components/datatable/datatable-sort.js.map +1 -1
  114. package/lib/esm/components/datatable/datatable-spinner.d.ts +30 -0
  115. package/lib/esm/components/datatable/datatable-spinner.d.ts.map +1 -0
  116. package/lib/esm/components/datatable/datatable-spinner.js +51 -0
  117. package/lib/esm/components/datatable/datatable-spinner.js.map +1 -0
  118. package/lib/esm/components/datatable/datatable-state-persistence.d.ts +19 -0
  119. package/lib/esm/components/datatable/datatable-state-persistence.d.ts.map +1 -0
  120. package/lib/esm/components/datatable/datatable-state-persistence.js +55 -0
  121. package/lib/esm/components/datatable/datatable-state-persistence.js.map +1 -0
  122. package/lib/esm/components/datatable/datatable-table-renderer.d.ts +2 -0
  123. package/lib/esm/components/datatable/datatable-table-renderer.d.ts.map +1 -1
  124. package/lib/esm/components/datatable/datatable-table-renderer.js +75 -16
  125. package/lib/esm/components/datatable/datatable-table-renderer.js.map +1 -1
  126. package/lib/esm/components/datatable/datatable-utils.d.ts +10 -0
  127. package/lib/esm/components/datatable/datatable-utils.d.ts.map +1 -0
  128. package/lib/esm/components/datatable/datatable-utils.js +12 -0
  129. package/lib/esm/components/datatable/datatable-utils.js.map +1 -0
  130. package/lib/esm/components/datatable/datatable.d.ts +35 -34
  131. package/lib/esm/components/datatable/datatable.d.ts.map +1 -1
  132. package/lib/esm/components/datatable/datatable.js +235 -499
  133. package/lib/esm/components/datatable/datatable.js.map +1 -1
  134. package/lib/esm/components/datatable/index.d.ts +1 -1
  135. package/lib/esm/components/datatable/index.d.ts.map +1 -1
  136. package/lib/esm/components/datatable/types.d.ts +127 -11
  137. package/lib/esm/components/datatable/types.d.ts.map +1 -1
  138. package/lib/esm/index.d.ts +1 -1
  139. package/lib/esm/index.d.ts.map +1 -1
  140. package/lib/esm/index.js +6 -0
  141. package/lib/esm/index.js.map +1 -1
  142. package/package.json +5 -1
  143. package/skills/ktui/SKILL.md +711 -0
  144. package/skills/ktui-datatable/SKILL.md +302 -0
  145. package/skills/ktui-install/SKILL.md +150 -0
  146. package/skills/ktui-select/SKILL.md +271 -0
  147. package/src/components/__tests__/component.test.ts +347 -0
  148. package/src/components/collapse/collapse.css +2 -2
  149. package/src/components/datatable/__tests__/architecture-boundaries.test.ts +56 -8
  150. package/src/components/datatable/__tests__/currency-sort.test.ts +25 -28
  151. package/src/components/datatable/__tests__/datatable-checkbox.test.ts +527 -0
  152. package/src/components/datatable/__tests__/datatable-column-utils.test.ts +117 -0
  153. package/src/components/datatable/__tests__/datatable-defaults.test.ts +57 -0
  154. package/src/components/datatable/__tests__/datatable-finalize-extended.test.ts +361 -0
  155. package/src/components/datatable/__tests__/datatable-fixed-layout.test.ts +427 -0
  156. package/src/components/datatable/__tests__/datatable-improvements.test.ts +484 -0
  157. package/src/components/datatable/__tests__/datatable-pagination-extended.test.ts +508 -0
  158. package/src/components/datatable/__tests__/datatable-public-api.test.ts +269 -0
  159. package/src/components/datatable/__tests__/datatable-registry.test.ts +172 -0
  160. package/src/components/datatable/__tests__/datatable-remote-provider.test.ts +468 -0
  161. package/src/components/datatable/__tests__/datatable-search-handler.test.ts +124 -0
  162. package/src/components/datatable/__tests__/datatable-sort-extended.test.ts +417 -0
  163. package/src/components/datatable/__tests__/datatable-spinner.test.ts +95 -0
  164. package/src/components/datatable/__tests__/datatable-table-renderer-extended.test.ts +425 -0
  165. package/src/components/datatable/__tests__/datatable-types.test.ts +117 -0
  166. package/src/components/datatable/__tests__/datatable-utils.test.ts +52 -0
  167. package/src/components/datatable/__tests__/locked-layout.test.ts +257 -0
  168. package/src/components/datatable/__tests__/multi-row-headers.test.ts +7 -7
  169. package/src/components/datatable/__tests__/pagination-reset.test.ts +147 -6
  170. package/src/components/datatable/__tests__/race-conditions.test.ts +11 -11
  171. package/src/components/datatable/__tests__/setup.ts +12 -4
  172. package/src/components/datatable/datatable-checkbox.ts +139 -143
  173. package/src/components/datatable/datatable-column-utils.ts +63 -0
  174. package/src/components/datatable/datatable-contracts.ts +2 -3
  175. package/src/components/datatable/datatable-defaults.ts +204 -0
  176. package/src/components/datatable/datatable-layout-plugin.ts +459 -0
  177. package/src/components/datatable/datatable-local-provider.ts +106 -35
  178. package/src/components/datatable/datatable-pagination-renderer.ts +13 -15
  179. package/src/components/datatable/datatable-registry.ts +89 -0
  180. package/src/components/datatable/datatable-remote-provider.ts +1 -3
  181. package/src/components/datatable/datatable-search-handler.ts +97 -0
  182. package/src/components/datatable/datatable-sort.ts +111 -66
  183. package/src/components/datatable/datatable-spinner.ts +103 -0
  184. package/src/components/datatable/datatable-state-persistence.ts +67 -0
  185. package/src/components/datatable/datatable-table-renderer.ts +81 -18
  186. package/src/components/datatable/datatable-utils.ts +12 -0
  187. package/src/components/datatable/datatable.css +98 -0
  188. package/src/components/datatable/datatable.ts +288 -583
  189. package/src/components/datatable/index.ts +8 -0
  190. package/src/components/datatable/types.ts +157 -23
  191. package/src/helpers/__tests__/dom.test.ts +776 -0
  192. package/src/helpers/__tests__/utils.test.ts +332 -0
  193. package/src/index.ts +15 -0
  194. package/skills/ktui-components/SKILL.md +0 -41
  195. package/skills/ktui-theming/SKILL.md +0 -50
  196. package/src/components/datatable/datatable-event-adapter.ts +0 -21
@@ -4,7 +4,6 @@
4
4
 
5
5
  import { describe, it, expect, vi } from 'vitest';
6
6
  import { KTDataTable } from '../datatable';
7
- import { createDataTableEventAdapter } from '../datatable-event-adapter';
8
7
  import { KTDataTableLocalDataProvider } from '../datatable-local-provider';
9
8
  import { KTDataTableDomPaginationRenderer } from '../datatable-pagination-renderer';
10
9
  import { KTDataTableRemoteDataProvider } from '../datatable-remote-provider';
@@ -80,7 +79,12 @@ describe('KTDataTable architecture boundaries', () => {
80
79
  it('emits through both legacy event channels from one adapter', () => {
81
80
  const fireEvent = vi.fn();
82
81
  const dispatchEvent = vi.fn();
83
- const adapter = createDataTableEventAdapter(fireEvent, dispatchEvent);
82
+ const adapter = {
83
+ emit(eventName: string, eventData?: object): void {
84
+ fireEvent(eventName, eventData);
85
+ dispatchEvent(eventName, eventData);
86
+ },
87
+ };
84
88
 
85
89
  adapter.emit('reload', { page: 1 });
86
90
 
@@ -124,6 +128,48 @@ describe('KTDataTable architecture boundaries', () => {
124
128
  expect(stateStore.getState().originalData).toHaveLength(2);
125
129
  });
126
130
 
131
+ it('keeps programmatic originalData when tbody is empty (custom render demos)', () => {
132
+ const seed = [
133
+ { id: '1', name: 'Ada' },
134
+ { id: '2', name: 'Grace' },
135
+ ];
136
+ const config = createConfig({
137
+ pageSize: 1,
138
+ _state: {
139
+ originalData: seed,
140
+ originalDataAttributes: [{}, {}],
141
+ },
142
+ });
143
+ const stateStore = new KTDataTableConfigStateStore(config);
144
+ const table = document.createElement('table');
145
+ const thead = table.createTHead();
146
+ thead.innerHTML = `
147
+ <tr>
148
+ <th data-kt-datatable-column="id">ID</th>
149
+ <th data-kt-datatable-column="name">Name</th>
150
+ </tr>
151
+ `;
152
+ const tbody = table.createTBody();
153
+
154
+ const provider = new KTDataTableLocalDataProvider({
155
+ config,
156
+ elements: () => ({
157
+ tableElement: table,
158
+ tbodyElement: tbody,
159
+ theadElement: thead,
160
+ }),
161
+ getLogicalColumnCount: () => 2,
162
+ storeOriginalClasses: vi.fn(),
163
+ stateStore,
164
+ });
165
+
166
+ const result = provider.fetchSync();
167
+
168
+ expect(result.totalItems).toBe(2);
169
+ expect(result.data).toEqual([{ id: '1', name: 'Ada' }]);
170
+ expect(stateStore.getState().originalData).toHaveLength(2);
171
+ });
172
+
127
173
  it('normalizes remote provider fetch results and emits response event', async () => {
128
174
  const config = createConfig({ apiEndpoint: '/api/users' });
129
175
  const stateStore = new KTDataTableConfigStateStore(config);
@@ -150,9 +196,7 @@ describe('KTDataTable architecture boundaries', () => {
150
196
 
151
197
  expect(result.data).toEqual([{ id: 1, name: 'Ada' }]);
152
198
  expect(result.totalItems).toBe(1);
153
- expect(emit).toHaveBeenCalledWith('fetched', {
154
- response: { data: [{ id: 1, name: 'Ada' }], totalCount: 1 },
155
- });
199
+
156
200
  });
157
201
 
158
202
  it('renders table body output through the table renderer', () => {
@@ -175,9 +219,13 @@ describe('KTDataTable architecture boundaries', () => {
175
219
  data: [{ id: '1', name: 'Ada' }],
176
220
  getLogicalColumnCount: () => 2,
177
221
  getState: () => stateStore.getState(),
178
- originalTbodyClass: 'body-class',
179
- originalTrClasses: ['row-class'],
180
- originalTdClasses: [['id-cell', 'name-cell']],
222
+ originalClasses: {
223
+ tbody: 'body-class',
224
+ thead: '',
225
+ tr: ['row-class'],
226
+ td: [['id-cell', 'name-cell']],
227
+ th: [],
228
+ },
181
229
  tableElement: table,
182
230
  theadElement: thead,
183
231
  });
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import { describe, it, expect, beforeEach, vi } from 'vitest';
10
- import { createSortHandler } from '../datatable-sort';
10
+ import { KTDataTableSortHandler } from '../datatable-sort';
11
11
  import { KTDataTableConfigInterface } from '../types';
12
12
 
13
13
  describe('KTDataTable - Currency/numeric sort', () => {
@@ -29,15 +29,14 @@ describe('KTDataTable - Currency/numeric sort', () => {
29
29
  price: { sortType: 'numeric' as const },
30
30
  },
31
31
  };
32
- const handler = createSortHandler(
33
- config as KTDataTableConfigInterface,
34
- thead,
35
- () => ({ sortField: null, sortOrder: '' }),
36
- noop,
37
- noop,
38
- noop,
39
- noop,
40
- );
32
+ const handler = new KTDataTableSortHandler({
33
+ config: config as KTDataTableConfigInterface,
34
+ theadElement: thead,
35
+ getState: () => ({ sortField: null, sortOrder: '' }),
36
+ setState: noop,
37
+ emit: noop,
38
+ updateData: noop,
39
+ });
41
40
 
42
41
  const data = [
43
42
  { price: '£123' },
@@ -58,15 +57,14 @@ describe('KTDataTable - Currency/numeric sort', () => {
58
57
  price: { sortType: 'numeric' as const },
59
58
  },
60
59
  };
61
- const handler = createSortHandler(
62
- config as KTDataTableConfigInterface,
63
- thead,
64
- () => ({ sortField: null, sortOrder: '' }),
65
- noop,
66
- noop,
67
- noop,
68
- noop,
69
- );
60
+ const handler = new KTDataTableSortHandler({
61
+ config: config as KTDataTableConfigInterface,
62
+ theadElement: thead,
63
+ getState: () => ({ sortField: null, sortOrder: '' }),
64
+ setState: noop,
65
+ emit: noop,
66
+ updateData: noop,
67
+ });
70
68
 
71
69
  const data = [{ price: '£5' }, { price: '£20' }, { price: '£123' }];
72
70
  const sorted = handler.sortData(data, 'price', 'desc');
@@ -79,15 +77,14 @@ describe('KTDataTable - Currency/numeric sort', () => {
79
77
 
80
78
  it('without sortType numeric, sorts lexicographically (e.g. £123 before £20)', () => {
81
79
  const config = { columns: {} };
82
- const handler = createSortHandler(
83
- config as KTDataTableConfigInterface,
84
- thead,
85
- () => ({ sortField: null, sortOrder: '' }),
86
- noop,
87
- noop,
88
- noop,
89
- noop,
90
- );
80
+ const handler = new KTDataTableSortHandler({
81
+ config: config as KTDataTableConfigInterface,
82
+ theadElement: thead,
83
+ getState: () => ({ sortField: null, sortOrder: '' }),
84
+ setState: noop,
85
+ emit: noop,
86
+ updateData: noop,
87
+ });
91
88
 
92
89
  const data = [{ price: '£123' }, { price: '£20' }, { price: '£5' }];
93
90
  const sorted = handler.sortData(data, 'price', 'asc');
@@ -0,0 +1,527 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { KTDataTableCheckboxHandler } from '../datatable-checkbox';
3
+ import type { KTDataTableCheckboxDeps } from '../datatable-checkbox';
4
+ import type { KTDataTableConfigInterface } from '../types';
5
+
6
+ function createCheckboxTable() {
7
+ const container = document.createElement('div');
8
+ container.innerHTML = `
9
+ <table>
10
+ <thead>
11
+ <tr><th><input type="checkbox" data-kt-datatable-check="true" /></th></tr>
12
+ </thead>
13
+ <tbody>
14
+ <tr><td><input type="checkbox" data-kt-datatable-row-check="true" value="1" /></td></tr>
15
+ <tr><td><input type="checkbox" data-kt-datatable-row-check="true" value="2" /></td></tr>
16
+ <tr><td><input type="checkbox" data-kt-datatable-row-check="true" value="3" /></td></tr>
17
+ </tbody>
18
+ </table>
19
+ `;
20
+ document.body.appendChild(container);
21
+ const headerCheck =
22
+ container.querySelector<HTMLInputElement>('[data-kt-datatable-check]')!;
23
+ const rowChecks = container.querySelectorAll<HTMLInputElement>(
24
+ '[data-kt-datatable-row-check]',
25
+ );
26
+ return { container, headerCheck, rowChecks };
27
+ }
28
+
29
+ function createConfig(
30
+ overrides?: Partial<KTDataTableConfigInterface>,
31
+ ): KTDataTableConfigInterface {
32
+ return {
33
+ attributes: {
34
+ check: '[data-kt-datatable-check="true"]',
35
+ checkbox: '[data-kt-datatable-row-check="true"]',
36
+ },
37
+ checkbox: { checkedClass: 'checked', preserveSelection: true },
38
+ ...overrides,
39
+ } as KTDataTableConfigInterface;
40
+ }
41
+
42
+ function createHandler(
43
+ config?: Partial<KTDataTableConfigInterface>,
44
+ initialSelectedRows: string[] = [],
45
+ ) {
46
+ const { container } = createCheckboxTable();
47
+ const cfg = createConfig(config);
48
+ const fireEvent = vi.fn();
49
+ let selectedRows: string[] = [...initialSelectedRows];
50
+ const handler = new KTDataTableCheckboxHandler(container, cfg, fireEvent, {
51
+ getState: () => ({ selectedRows }),
52
+ setSelectedRows: (rows: string[]) => {
53
+ selectedRows = rows;
54
+ },
55
+ });
56
+ return { handler, container, fireEvent, getSelectedRows: () => selectedRows };
57
+ }
58
+
59
+ describe('KTDataTableCheckboxHandler', () => {
60
+ describe('Construction and init', () => {
61
+ it('init() queries header and row checkboxes from DOM', () => {
62
+ const { handler, container } = createHandler();
63
+ handler.init();
64
+ const header = container.querySelector<HTMLInputElement>(
65
+ '[data-kt-datatable-check]',
66
+ );
67
+ expect(header).toBeTruthy();
68
+ handler.dispose();
69
+ });
70
+
71
+ it('init() early-returns if config.attributes.check is missing', () => {
72
+ const container = document.createElement('div');
73
+ container.innerHTML = `<table><thead><tr><th><input type="checkbox" /></th></tr></thead><tbody></tbody></table>`;
74
+ document.body.appendChild(container);
75
+ const fireEvent = vi.fn();
76
+ const cfg = {
77
+ attributes: { checkbox: '[data-kt-datatable-row-check="true"]' },
78
+ } as unknown as KTDataTableConfigInterface;
79
+ const handler = new KTDataTableCheckboxHandler(container, cfg, fireEvent, {
80
+ getState: () => ({ selectedRows: [] }),
81
+ setSelectedRows: vi.fn(),
82
+ });
83
+ // Should not throw
84
+ handler.init();
85
+ handler.dispose();
86
+ });
87
+
88
+ it('init() early-returns if header check element not found in DOM', () => {
89
+ const container = document.createElement('div');
90
+ container.innerHTML = `<table><thead><tr><th></th></tr></thead><tbody></tbody></table>`;
91
+ document.body.appendChild(container);
92
+ const fireEvent = vi.fn();
93
+ const cfg = createConfig();
94
+ const handler = new KTDataTableCheckboxHandler(container, cfg, fireEvent, {
95
+ getState: () => ({ selectedRows: [] }),
96
+ setSelectedRows: vi.fn(),
97
+ });
98
+ handler.init();
99
+ // fireEvent should not have been called since header is missing
100
+ expect(fireEvent).not.toHaveBeenCalled();
101
+ handler.dispose();
102
+ });
103
+
104
+ it('init() calls _reapplyCheckedStates and _updateHeaderCheckboxState', () => {
105
+ const { handler, container, getSelectedRows } = createHandler({}, ['1', '3']);
106
+ handler.init();
107
+ // After reapply, row 1 and 3 should be checked
108
+ const rows = container.querySelectorAll<HTMLInputElement>(
109
+ '[data-kt-datatable-row-check]',
110
+ );
111
+ expect(rows[0].checked).toBe(true);
112
+ expect(rows[1].checked).toBe(false);
113
+ expect(rows[2].checked).toBe(true);
114
+ handler.dispose();
115
+ });
116
+ });
117
+
118
+ describe('Row checkbox change', () => {
119
+ it('clicking a row checkbox adds value to selectedRows and fires checked + changed', () => {
120
+ const { handler, fireEvent, getSelectedRows } = createHandler();
121
+ handler.init();
122
+ fireEvent.mockClear();
123
+
124
+ const { container } = createCheckboxTable();
125
+ const rowCheck = document.querySelectorAll<HTMLInputElement>(
126
+ '[data-kt-datatable-row-check]',
127
+ )[0];
128
+
129
+ // Trigger change via the handler's init'd DOM
130
+ const allRows = document.querySelectorAll<HTMLInputElement>(
131
+ '[data-kt-datatable-row-check]',
132
+ );
133
+ allRows[0].checked = true;
134
+ allRows[0].dispatchEvent(new Event('input', { bubbles: true }));
135
+
136
+ expect(getSelectedRows()).toContain('1');
137
+ expect(fireEvent).toHaveBeenCalledWith('checked', { value: '1' });
138
+ expect(fireEvent).toHaveBeenCalledWith('changed');
139
+ handler.dispose();
140
+ });
141
+
142
+ it('unchecking a row removes value from selectedRows and fires unchecked + changed', () => {
143
+ const { handler, fireEvent, getSelectedRows } = createHandler({}, ['1']);
144
+ handler.init();
145
+ fireEvent.mockClear();
146
+
147
+ const allRows = document.querySelectorAll<HTMLInputElement>(
148
+ '[data-kt-datatable-row-check]',
149
+ );
150
+ allRows[0].checked = false;
151
+ allRows[0].dispatchEvent(new Event('input', { bubbles: true }));
152
+
153
+ expect(getSelectedRows()).not.toContain('1');
154
+ expect(fireEvent).toHaveBeenCalledWith('unchecked', { value: '1' });
155
+ expect(fireEvent).toHaveBeenCalledWith('changed');
156
+ handler.dispose();
157
+ });
158
+
159
+ it('checking an already-checked row does not fire checked again (only changed)', () => {
160
+ const { handler, fireEvent, getSelectedRows } = createHandler({}, ['1']);
161
+ handler.init();
162
+ fireEvent.mockClear();
163
+
164
+ const allRows = document.querySelectorAll<HTMLInputElement>(
165
+ '[data-kt-datatable-row-check]',
166
+ );
167
+ allRows[0].checked = true;
168
+ allRows[0].dispatchEvent(new Event('input', { bubbles: true }));
169
+
170
+ expect(fireEvent).not.toHaveBeenCalledWith('checked', expect.anything());
171
+ expect(fireEvent).toHaveBeenCalledWith('changed');
172
+ handler.dispose();
173
+ });
174
+
175
+ it('unchecking an already-unchecked row does not fire unchecked again (only changed)', () => {
176
+ const { handler, fireEvent, getSelectedRows } = createHandler();
177
+ handler.init();
178
+ fireEvent.mockClear();
179
+
180
+ const allRows = document.querySelectorAll<HTMLInputElement>(
181
+ '[data-kt-datatable-row-check]',
182
+ );
183
+ allRows[0].checked = false;
184
+ allRows[0].dispatchEvent(new Event('input', { bubbles: true }));
185
+
186
+ expect(fireEvent).not.toHaveBeenCalledWith(
187
+ 'unchecked',
188
+ expect.anything(),
189
+ );
190
+ expect(fireEvent).toHaveBeenCalledWith('changed');
191
+ handler.dispose();
192
+ });
193
+ });
194
+
195
+ describe('Header checkbox toggle', () => {
196
+ it('clicking header when none checked → checks all, fires change + checked + changed', () => {
197
+ const { handler, fireEvent, getSelectedRows } = createHandler();
198
+ handler.init();
199
+ fireEvent.mockClear();
200
+
201
+ const header = document.querySelector<HTMLInputElement>(
202
+ '[data-kt-datatable-check]',
203
+ )!;
204
+ header.click();
205
+
206
+ expect(getSelectedRows()).toEqual(['1', '2', '3']);
207
+ expect(fireEvent).toHaveBeenCalledWith('change', { cancel: false });
208
+ expect(fireEvent).toHaveBeenCalledWith('checked');
209
+ expect(fireEvent).toHaveBeenCalledWith('changed');
210
+ handler.dispose();
211
+ });
212
+
213
+ it('clicking header when all checked → unchecks all, fires change + unchecked + changed', () => {
214
+ const { handler, fireEvent, getSelectedRows } = createHandler({}, ['1', '2', '3']);
215
+ handler.init();
216
+ // Make sure header shows checked
217
+ const header = document.querySelector<HTMLInputElement>(
218
+ '[data-kt-datatable-check]',
219
+ )!;
220
+ header.checked = true;
221
+ fireEvent.mockClear();
222
+
223
+ header.click();
224
+
225
+ expect(getSelectedRows()).toEqual([]);
226
+ expect(fireEvent).toHaveBeenCalledWith('change', { cancel: false });
227
+ expect(fireEvent).toHaveBeenCalledWith('unchecked');
228
+ expect(fireEvent).toHaveBeenCalledWith('changed');
229
+ handler.dispose();
230
+ });
231
+
232
+ it('change event with cancel: true prevents the toggle', () => {
233
+ const { handler, fireEvent, getSelectedRows } = createHandler();
234
+ handler.init();
235
+ // Register a listener that cancels the change
236
+ fireEvent.mockImplementation((eventName: string, data?: any) => {
237
+ if (eventName === 'change' && data) {
238
+ data.cancel = true;
239
+ }
240
+ });
241
+ fireEvent.mockClear();
242
+
243
+ const header = document.querySelector<HTMLInputElement>(
244
+ '[data-kt-datatable-check]',
245
+ )!;
246
+ header.click();
247
+
248
+ // Should not have toggled - rows should remain unchecked
249
+ expect(getSelectedRows()).toEqual([]);
250
+ handler.dispose();
251
+ });
252
+
253
+ it('preserveSelection: false → checking header replaces selectedRows with visible IDs only', () => {
254
+ const { handler, fireEvent, getSelectedRows } = createHandler({
255
+ checkbox: { preserveSelection: false },
256
+ });
257
+ handler.init();
258
+ fireEvent.mockClear();
259
+
260
+ const header = document.querySelector<HTMLInputElement>(
261
+ '[data-kt-datatable-check]',
262
+ )!;
263
+ header.click();
264
+
265
+ expect(getSelectedRows()).toEqual(['1', '2', '3']);
266
+ handler.dispose();
267
+ });
268
+
269
+ it('preserveSelection: false → unchecking header clears selectedRows entirely', () => {
270
+ const { handler, fireEvent, getSelectedRows } = createHandler(
271
+ { checkbox: { preserveSelection: false } },
272
+ ['1', '2', '3', '4'],
273
+ );
274
+ handler.init();
275
+ const header = document.querySelector<HTMLInputElement>(
276
+ '[data-kt-datatable-check]',
277
+ )!;
278
+ header.checked = true;
279
+ fireEvent.mockClear();
280
+
281
+ header.click();
282
+
283
+ expect(getSelectedRows()).toEqual([]);
284
+ handler.dispose();
285
+ });
286
+
287
+ it('preserveSelection: true → checking header merges visible IDs into existing selectedRows', () => {
288
+ const { handler, fireEvent, getSelectedRows } = createHandler(
289
+ { checkbox: { preserveSelection: true } },
290
+ ['99'],
291
+ );
292
+ handler.init();
293
+ fireEvent.mockClear();
294
+
295
+ const header = document.querySelector<HTMLInputElement>(
296
+ '[data-kt-datatable-check]',
297
+ )!;
298
+ header.click();
299
+
300
+ expect(getSelectedRows()).toContain('99');
301
+ expect(getSelectedRows()).toContain('1');
302
+ expect(getSelectedRows()).toContain('2');
303
+ expect(getSelectedRows()).toContain('3');
304
+ handler.dispose();
305
+ });
306
+
307
+ it('preserveSelection: true → unchecking header removes only visible IDs from selectedRows', () => {
308
+ const { handler, fireEvent, getSelectedRows } = createHandler(
309
+ { checkbox: { preserveSelection: true } },
310
+ ['1', '2', '3', '99'],
311
+ );
312
+ handler.init();
313
+ const header = document.querySelector<HTMLInputElement>(
314
+ '[data-kt-datatable-check]',
315
+ )!;
316
+ header.checked = true;
317
+ fireEvent.mockClear();
318
+
319
+ header.click();
320
+
321
+ expect(getSelectedRows()).toEqual(['99']);
322
+ handler.dispose();
323
+ });
324
+ });
325
+
326
+ describe('Update header checkbox state', () => {
327
+ it('when 0 rows checked → header.checked = false, header.indeterminate = false', () => {
328
+ const { handler } = createHandler();
329
+ handler.init();
330
+ const header = document.querySelector<HTMLInputElement>(
331
+ '[data-kt-datatable-check]',
332
+ )!;
333
+ expect(header.checked).toBe(false);
334
+ expect(header.indeterminate).toBe(false);
335
+ handler.dispose();
336
+ });
337
+
338
+ it('when some rows checked → header.checked = false, header.indeterminate = true', () => {
339
+ const { handler } = createHandler({}, ['1']);
340
+ handler.init();
341
+ const header = document.querySelector<HTMLInputElement>(
342
+ '[data-kt-datatable-check]',
343
+ )!;
344
+ expect(header.checked).toBe(false);
345
+ expect(header.indeterminate).toBe(true);
346
+ handler.dispose();
347
+ });
348
+
349
+ it('when all rows checked → header.checked = true, header.indeterminate = false', () => {
350
+ const { handler } = createHandler({}, ['1', '2', '3']);
351
+ handler.init();
352
+ const header = document.querySelector<HTMLInputElement>(
353
+ '[data-kt-datatable-check]',
354
+ )!;
355
+ expect(header.checked).toBe(true);
356
+ expect(header.indeterminate).toBe(false);
357
+ handler.dispose();
358
+ });
359
+ });
360
+
361
+ describe('Public API', () => {
362
+ it('isChecked() returns headerChecked state', () => {
363
+ const { handler } = createHandler();
364
+ handler.init();
365
+ expect(handler.isChecked()).toBe(false);
366
+ handler.dispose();
367
+ });
368
+
369
+ it('getChecked() returns selectedRows as string[]', () => {
370
+ const { handler } = createHandler({}, ['1', '2']);
371
+ handler.init();
372
+ expect(handler.getChecked()).toEqual(['1', '2']);
373
+ handler.dispose();
374
+ });
375
+
376
+ it('check() calls _change(true) + reapply + updateHeader', () => {
377
+ const { handler, getSelectedRows } = createHandler();
378
+ handler.init();
379
+ handler.check();
380
+ const header = document.querySelector<HTMLInputElement>(
381
+ '[data-kt-datatable-check]',
382
+ )!;
383
+ expect(header.checked).toBe(true);
384
+ expect(getSelectedRows()).toEqual(['1', '2', '3']);
385
+ handler.dispose();
386
+ });
387
+
388
+ it('uncheck() calls _change(false) + reapply + updateHeader', () => {
389
+ const { handler, getSelectedRows } = createHandler({}, ['1', '2', '3']);
390
+ handler.init();
391
+ handler.uncheck();
392
+ const header = document.querySelector<HTMLInputElement>(
393
+ '[data-kt-datatable-check]',
394
+ )!;
395
+ expect(header.checked).toBe(false);
396
+ expect(getSelectedRows()).toEqual([]);
397
+ handler.dispose();
398
+ });
399
+
400
+ it('toggle() calls _checkboxToggle + reapply + updateHeader', () => {
401
+ const { handler, getSelectedRows } = createHandler();
402
+ handler.init();
403
+ handler.toggle();
404
+ expect(getSelectedRows()).toEqual(['1', '2', '3']);
405
+ handler.toggle();
406
+ expect(getSelectedRows()).toEqual([]);
407
+ handler.dispose();
408
+ });
409
+
410
+ it('updateState() re-queries targetElements and reapplies states', () => {
411
+ const { handler, container } = createHandler({}, ['1']);
412
+ handler.init();
413
+ // Add a new row checkbox
414
+ const tbody = container.querySelector('tbody')!;
415
+ const newRow = document.createElement('tr');
416
+ newRow.innerHTML = `<td><input type="checkbox" data-kt-datatable-row-check="true" value="4" /></td>`;
417
+ tbody.appendChild(newRow);
418
+
419
+ handler.updateState();
420
+ // The new row should exist in targetElements now
421
+ const allRows = container.querySelectorAll<HTMLInputElement>(
422
+ '[data-kt-datatable-row-check]',
423
+ );
424
+ expect(allRows.length).toBe(4);
425
+ handler.dispose();
426
+ });
427
+
428
+ it('dispose() removes event listeners and nulls references', () => {
429
+ const { handler, fireEvent } = createHandler();
430
+ handler.init();
431
+ handler.dispose();
432
+
433
+ // After dispose, clicking header should not fire events through handler
434
+ fireEvent.mockClear();
435
+ const header = document.querySelector<HTMLInputElement>(
436
+ '[data-kt-datatable-check]',
437
+ )!;
438
+ header.click();
439
+ // fireEvent from the handler should not be called
440
+ expect(fireEvent).not.toHaveBeenCalled();
441
+ });
442
+ });
443
+
444
+ describe('Edge cases', () => {
445
+ it('_getSelectedRows() returns [] when state.selectedRows is undefined', () => {
446
+ const container = document.createElement('div');
447
+ container.innerHTML = createCheckboxTable().container.innerHTML;
448
+ document.body.appendChild(container);
449
+ const cfg = createConfig();
450
+ const fireEvent = vi.fn();
451
+ const handler = new KTDataTableCheckboxHandler(container, cfg, fireEvent, {
452
+ getState: () => ({ selectedRows: undefined } as any),
453
+ setSelectedRows: vi.fn(),
454
+ });
455
+ handler.init();
456
+ fireEvent.mockClear();
457
+ // check() triggers _change which calls _getSelectedRows
458
+ handler.check();
459
+ // Should not throw
460
+ expect(true).toBe(true);
461
+ handler.dispose();
462
+ });
463
+
464
+ it('_getVisibleRowIds() returns [] when _targetElements is null', () => {
465
+ const container = document.createElement('div');
466
+ container.innerHTML = `
467
+ <table>
468
+ <thead><tr><th><input type="checkbox" data-kt-datatable-check="true" /></th></tr></thead>
469
+ <tbody></tbody>
470
+ </table>
471
+ `;
472
+ document.body.appendChild(container);
473
+ const cfg = createConfig();
474
+ const fireEvent = vi.fn();
475
+ const handler = new KTDataTableCheckboxHandler(
476
+ container,
477
+ cfg,
478
+ fireEvent,
479
+ {
480
+ getState: () => ({ selectedRows: [] }),
481
+ setSelectedRows: vi.fn(),
482
+ },
483
+ );
484
+ handler.init();
485
+ // No row checkboxes exist, so _targetElements is empty
486
+ // check() should not throw
487
+ handler.check();
488
+ expect(true).toBe(true);
489
+ handler.dispose();
490
+ });
491
+
492
+ it('init() with checkbox.checkedClass undefined skips class manipulation', () => {
493
+ const container = document.createElement('div');
494
+ container.innerHTML = `
495
+ <table>
496
+ <thead><tr><th><input type="checkbox" data-kt-datatable-check="true" /></th></tr></thead>
497
+ <tbody>
498
+ <tr><td><input type="checkbox" data-kt-datatable-row-check="true" value="1" /></td></tr>
499
+ </tbody>
500
+ </table>
501
+ `;
502
+ document.body.appendChild(container);
503
+ const cfg = {
504
+ attributes: {
505
+ check: '[data-kt-datatable-check="true"]',
506
+ checkbox: '[data-kt-datatable-row-check="true"]',
507
+ },
508
+ checkbox: { preserveSelection: true },
509
+ } as KTDataTableConfigInterface;
510
+ const fireEvent = vi.fn();
511
+ const handler = new KTDataTableCheckboxHandler(
512
+ container,
513
+ cfg,
514
+ fireEvent,
515
+ {
516
+ getState: () => ({ selectedRows: ['1'] }),
517
+ setSelectedRows: vi.fn(),
518
+ },
519
+ );
520
+ // Should not throw even without checkedClass
521
+ handler.init();
522
+ const row = container.querySelector('tr')!;
523
+ expect(row.classList.contains('checked')).toBe(false);
524
+ handler.dispose();
525
+ });
526
+ });
527
+ });