@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,484 @@
1
+ /**
2
+ * Tests for datatable improvements applied 2026-05-28:
3
+ * - Sort handler: AbortController cleanup, pre-stripped HTML cache
4
+ * - Local provider: filter pipeline (text, numeric, dateRange)
5
+ * - Checkbox handler: scoped to root element (not document.body)
6
+ * - Layout plugin: rAF throttle on resize/scroll
7
+ */
8
+
9
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
10
+ import { KTDataTableSortHandler } from '../datatable-sort';
11
+ import { KTDataTableLocalDataProvider } from '../datatable-local-provider';
12
+ import { KTDataTableCheckboxHandler } from '../datatable-checkbox';
13
+ import { KTDataTableConfigStateStore } from '../datatable-state-store';
14
+ import {
15
+ KTDataTableConfigInterface,
16
+ KTDataTableDataInterface,
17
+ } from '../types';
18
+ import { createStickyLayoutPlugin } from '../datatable-layout-plugin';
19
+ import { KTDataTable } from '../datatable';
20
+
21
+ // ── Helpers ──────────────────────────────────────────────────────────
22
+
23
+ function createConfig(
24
+ overrides: Partial<KTDataTableConfigInterface> = {},
25
+ ): KTDataTableConfigInterface {
26
+ return {
27
+ pageSize: 10,
28
+ pageSizes: [10, 20],
29
+ pageMore: true,
30
+ pageMoreLimit: 3,
31
+ info: '{start}-{end} of {total}',
32
+ infoEmpty: 'No records found',
33
+ pagination: {
34
+ number: { class: 'number', text: '{page}' },
35
+ previous: { class: 'previous', text: 'Previous' },
36
+ next: { class: 'next', text: 'Next' },
37
+ more: { class: 'more', text: '...' },
38
+ },
39
+ search: { delay: 0 },
40
+ sort: {},
41
+ attributes: {
42
+ table: '[data-kt-datatable-table="true"]',
43
+ info: '[data-kt-datatable-info="true"]',
44
+ size: '[data-kt-datatable-size="true"]',
45
+ pagination: '[data-kt-datatable-pagination="true"]',
46
+ spinner: '[data-kt-datatable-spinner="true"]',
47
+ check: '[data-kt-datatable-check="true"]',
48
+ checkbox: '[data-kt-datatable-row-check="true"]',
49
+ },
50
+ _state: {},
51
+ ...overrides,
52
+ } as KTDataTableConfigInterface;
53
+ }
54
+
55
+ /**
56
+ * Create a local provider with data in the DOM (so fetchSync can extract it).
57
+ * The thead has data-kt-datatable-column attributes matching the td values.
58
+ */
59
+ function createProviderWithDomData(
60
+ config: KTDataTableConfigInterface,
61
+ columns: string[],
62
+ rows: string[][],
63
+ ) {
64
+ const table = document.createElement('table');
65
+ const thead = table.createTHead();
66
+ const theadRow = document.createElement('tr');
67
+ for (const col of columns) {
68
+ const th = document.createElement('th');
69
+ th.setAttribute('data-kt-datatable-column', col);
70
+ theadRow.appendChild(th);
71
+ }
72
+ thead.appendChild(theadRow);
73
+
74
+ const tbody = table.createTBody();
75
+ for (const row of rows) {
76
+ const tr = document.createElement('tr');
77
+ for (const cell of row) {
78
+ const td = document.createElement('td');
79
+ td.textContent = cell;
80
+ tr.appendChild(td);
81
+ }
82
+ tbody.appendChild(tr);
83
+ }
84
+
85
+ const store = new KTDataTableConfigStateStore(config);
86
+
87
+ const provider = new KTDataTableLocalDataProvider({
88
+ config,
89
+ elements: () => ({
90
+ tableElement: table,
91
+ tbodyElement: tbody,
92
+ theadElement: thead,
93
+ }),
94
+ getLogicalColumnCount: () => columns.length,
95
+ storeOriginalClasses: vi.fn(),
96
+ stateStore: store,
97
+ });
98
+
99
+ return { provider, store };
100
+ }
101
+
102
+ // ── Sort Handler Tests ───────────────────────────────────────────────
103
+
104
+ describe('Sort handler improvements', () => {
105
+ let thead: HTMLTableSectionElement;
106
+
107
+ beforeEach(() => {
108
+ thead = document.createElement('thead');
109
+ const tr = document.createElement('tr');
110
+ const th1 = document.createElement('th');
111
+ th1.setAttribute('data-kt-datatable-column', 'name');
112
+ th1.innerHTML = '<span class="sort-icon"></span>';
113
+ const th2 = document.createElement('th');
114
+ th2.setAttribute('data-kt-datatable-column', 'price');
115
+ th2.innerHTML = '<span class="sort-icon"></span>';
116
+ tr.appendChild(th1);
117
+ tr.appendChild(th2);
118
+ thead.appendChild(tr);
119
+ });
120
+
121
+ it('uses AbortController to clean up sort listeners on re-init', () => {
122
+ const updateData = vi.fn();
123
+ const handler = new KTDataTableSortHandler<KTDataTableDataInterface>({
124
+ config: { sort: { classes: { base: 'sort-icon' } } } as never,
125
+ theadElement: thead,
126
+ getState: () => ({ sortField: '' as string | number, sortOrder: '' }),
127
+ setState: vi.fn(),
128
+ emit: vi.fn(),
129
+ updateData,
130
+ });
131
+
132
+ handler.initSort();
133
+
134
+ const th = thead.querySelector('th')!;
135
+ th.click();
136
+ expect(updateData).toHaveBeenCalledTimes(1);
137
+
138
+ // Re-init should abort previous listeners and attach new ones
139
+ handler.initSort();
140
+ updateData.mockClear();
141
+
142
+ th.click();
143
+ expect(updateData).toHaveBeenCalledTimes(1);
144
+
145
+ handler.dispose();
146
+ });
147
+
148
+ it('dispose() aborts listeners without destroying th elements', () => {
149
+ const handler = new KTDataTableSortHandler<KTDataTableDataInterface>({
150
+ config: { sort: { classes: { base: 'sort-icon' } } } as never,
151
+ theadElement: thead,
152
+ getState: () => ({ sortField: '' as string | number, sortOrder: '' }),
153
+ setState: vi.fn(),
154
+ emit: vi.fn(),
155
+ updateData: vi.fn(),
156
+ });
157
+
158
+ handler.initSort();
159
+ const thBefore = thead.querySelector('th')!;
160
+ thBefore.setAttribute('data-custom', 'preserved');
161
+
162
+ handler.dispose();
163
+
164
+ const thAfter = thead.querySelector('th')!;
165
+ expect(thAfter).toBe(thBefore);
166
+ expect(thAfter.getAttribute('data-custom')).toBe('preserved');
167
+ });
168
+
169
+ it('sortData pre-strips HTML for string comparison', () => {
170
+ const handler = new KTDataTableSortHandler<KTDataTableDataInterface>({
171
+ config: {} as never,
172
+ theadElement: thead,
173
+ getState: () => ({ sortField: '' as string | number, sortOrder: '' }),
174
+ setState: vi.fn(),
175
+ emit: vi.fn(),
176
+ updateData: vi.fn(),
177
+ });
178
+
179
+ const data = [
180
+ { name: '<b>Zoe</b>' },
181
+ { name: '<span class="x">Alice</span>' },
182
+ { name: '<i>Bob</i>' },
183
+ ];
184
+
185
+ const sorted = handler.sortData(data, 'name', 'asc');
186
+ const names = sorted.map((r) => (r as Record<string, unknown>)['name']);
187
+
188
+ expect(names).toEqual([
189
+ '<span class="x">Alice</span>',
190
+ '<i>Bob</i>',
191
+ '<b>Zoe</b>',
192
+ ]);
193
+ });
194
+
195
+ it('sortData pre-strips HTML for numeric comparison', () => {
196
+ const handler = new KTDataTableSortHandler<KTDataTableDataInterface>({
197
+ config: { columns: { price: { sortType: 'numeric' } } } as never,
198
+ theadElement: thead,
199
+ getState: () => ({ sortField: '' as string | number, sortOrder: '' }),
200
+ setState: vi.fn(),
201
+ emit: vi.fn(),
202
+ updateData: vi.fn(),
203
+ });
204
+
205
+ const data = [
206
+ { price: '<b>$123</b>' },
207
+ { price: '<span>$5</span>' },
208
+ { price: '<i>$20</i>' },
209
+ ];
210
+
211
+ const sorted = handler.sortData(data, 'price', 'asc');
212
+ const prices = sorted.map((r) => (r as Record<string, unknown>)['price']);
213
+
214
+ expect(prices).toEqual([
215
+ '<span>$5</span>',
216
+ '<i>$20</i>',
217
+ '<b>$123</b>',
218
+ ]);
219
+ });
220
+ });
221
+
222
+ // ── Local Provider Filter Tests ─────────────────────────────────────
223
+
224
+ describe('Local provider filter pipeline', () => {
225
+ const columns = ['id', 'name'];
226
+ const rows = [
227
+ ['1', 'Alice'],
228
+ ['2', 'Bob'],
229
+ ['3', 'Charlie'],
230
+ ];
231
+
232
+ it('applies text filter (case-insensitive)', () => {
233
+ const config = createConfig({ pageSize: 100 });
234
+ const { provider, store } = createProviderWithDomData(config, columns, rows);
235
+ store.setFilter({ column: 'name', type: 'text', value: 'bo' });
236
+
237
+ const result = provider.fetchSync();
238
+
239
+ expect(result.totalItems).toBe(1);
240
+ expect(result.data).toEqual([{ id: '2', name: 'Bob' }]);
241
+ });
242
+
243
+ it('applies numeric filter (exact match)', () => {
244
+ const config = createConfig({ pageSize: 100 });
245
+ const { provider, store } = createProviderWithDomData(config, columns, rows);
246
+ store.setFilter({ column: 'id', type: 'numeric', value: 2 });
247
+
248
+ const result = provider.fetchSync();
249
+
250
+ expect(result.totalItems).toBe(1);
251
+ expect(result.data).toEqual([{ id: '2', name: 'Bob' }]);
252
+ });
253
+
254
+ it('applies dateRange filter', () => {
255
+ const dateColumns = ['date', 'name'];
256
+ const dateRows = [
257
+ ['2024-01-01', 'Alice'],
258
+ ['2024-06-15', 'Bob'],
259
+ ['2025-01-01', 'Charlie'],
260
+ ];
261
+ const config = createConfig({ pageSize: 100 });
262
+ const { provider, store } = createProviderWithDomData(config, dateColumns, dateRows);
263
+ store.setFilter({
264
+ column: 'date',
265
+ type: 'dateRange',
266
+ value: { from: '2024-03-01', to: '2024-12-31' },
267
+ });
268
+
269
+ const result = provider.fetchSync();
270
+
271
+ expect(result.totalItems).toBe(1);
272
+ expect(result.data).toEqual([{ date: '2024-06-15', name: 'Bob' }]);
273
+ });
274
+
275
+ it('chains multiple filters (AND logic)', () => {
276
+ const multiColumns = ['score', 'name'];
277
+ const multiRows = [
278
+ ['10', 'Alice'],
279
+ ['20', 'Bob'],
280
+ ['20', 'Charlie'],
281
+ ['30', 'Alice'],
282
+ ];
283
+ const config = createConfig({ pageSize: 100 });
284
+ const { provider, store } = createProviderWithDomData(config, multiColumns, multiRows);
285
+ store.setFilter({ column: 'name', type: 'text', value: 'alice' });
286
+ store.setFilter({ column: 'score', type: 'numeric', value: 30 });
287
+
288
+ const result = provider.fetchSync();
289
+
290
+ expect(result.totalItems).toBe(1);
291
+ expect(result.data).toEqual([{ score: '30', name: 'Alice' }]);
292
+ });
293
+
294
+ it('empty text filter value matches all rows', () => {
295
+ const config = createConfig({ pageSize: 100 });
296
+ const { provider, store } = createProviderWithDomData(config, columns, rows);
297
+ store.setFilter({ column: 'name', type: 'text', value: '' });
298
+
299
+ const result = provider.fetchSync();
300
+
301
+ expect(result.totalItems).toBe(3);
302
+ });
303
+
304
+ it('filters and paginates correctly together', () => {
305
+ const paginateColumns = ['id', 'status'];
306
+ const paginateRows = [
307
+ ['1', 'yes'],
308
+ ['2', 'no'],
309
+ ['3', 'yes'],
310
+ ['4', 'yes'],
311
+ ['5', 'no'],
312
+ ];
313
+ const config = createConfig({ pageSize: 2 });
314
+ const { provider, store } = createProviderWithDomData(config, paginateColumns, paginateRows);
315
+ store.setFilter({ column: 'status', type: 'text', value: 'yes' });
316
+
317
+ const result = provider.fetchSync();
318
+
319
+ // 3 'yes' rows, page size 2 => page 1 shows 2, totalItems = 3
320
+ expect(result.totalItems).toBe(3);
321
+ expect(result.data).toHaveLength(2);
322
+ });
323
+
324
+ it('no filters returns all rows (backwards compatible)', () => {
325
+ const config = createConfig({ pageSize: 100 });
326
+ const { provider } = createProviderWithDomData(config, columns, rows);
327
+
328
+ const result = provider.fetchSync();
329
+
330
+ expect(result.totalItems).toBe(3);
331
+ expect(result.data).toHaveLength(3);
332
+ });
333
+ });
334
+
335
+ // ── Checkbox Handler Scope Tests ─────────────────────────────────────
336
+
337
+ describe('Checkbox handler event scope', () => {
338
+ it('delegates checkbox events to root element, not document.body', () => {
339
+ const root = document.createElement('div');
340
+ const headerCheck = document.createElement('input');
341
+ headerCheck.type = 'checkbox';
342
+ headerCheck.setAttribute('data-kt-datatable-check', 'true');
343
+ root.appendChild(headerCheck);
344
+
345
+ const rowCheck = document.createElement('input');
346
+ rowCheck.type = 'checkbox';
347
+ rowCheck.value = 'row-1';
348
+ rowCheck.setAttribute('data-kt-datatable-row-check', 'true');
349
+ const tr = document.createElement('tr');
350
+ tr.appendChild(rowCheck);
351
+ const tbody = document.createElement('tbody');
352
+ tbody.appendChild(tr);
353
+ root.appendChild(tbody);
354
+ document.body.appendChild(root);
355
+
356
+ const fireEvent = vi.fn();
357
+ const config = createConfig();
358
+
359
+ const handler = new KTDataTableCheckboxHandler(
360
+ root,
361
+ config,
362
+ fireEvent,
363
+ {
364
+ getState: () => ({ selectedRows: [] }),
365
+ setSelectedRows: vi.fn(),
366
+ },
367
+ );
368
+ handler.init();
369
+
370
+ rowCheck.checked = true;
371
+ rowCheck.dispatchEvent(new Event('input', { bubbles: true }));
372
+
373
+ expect(fireEvent).toHaveBeenCalledWith('changed');
374
+
375
+ // Verify that a checkbox OUTSIDE root does not trigger the handler
376
+ fireEvent.mockClear();
377
+ const outsideCheck = document.createElement('input');
378
+ outsideCheck.type = 'checkbox';
379
+ outsideCheck.value = 'outside';
380
+ outsideCheck.setAttribute('data-kt-datatable-row-check', 'true');
381
+ document.body.appendChild(outsideCheck);
382
+
383
+ outsideCheck.checked = true;
384
+ outsideCheck.dispatchEvent(new Event('input', { bubbles: true }));
385
+
386
+ expect(fireEvent).not.toHaveBeenCalled();
387
+
388
+ handler.dispose();
389
+ });
390
+ });
391
+
392
+ // ── update() → refreshCheckboxes() Alias Tests ──────────────────────
393
+
394
+ describe('update() alias', () => {
395
+ it('both methods exist on the prototype', () => {
396
+ expect(typeof KTDataTable.prototype.update).toBe('function');
397
+ expect(typeof KTDataTable.prototype.refreshCheckboxes).toBe('function');
398
+ });
399
+
400
+ it('update() delegates to refreshCheckboxes()', () => {
401
+ const spy = vi.spyOn(KTDataTable.prototype, 'refreshCheckboxes').mockImplementation(() => {});
402
+ const instance = Object.create(KTDataTable.prototype);
403
+ instance.update();
404
+ expect(spy).toHaveBeenCalledTimes(1);
405
+ spy.mockRestore();
406
+ });
407
+ });
408
+
409
+ // ── Layout Plugin Tests ─────────────────────────────────────────────
410
+
411
+ describe('Layout plugin', () => {
412
+ it('exports createStickyLayoutPlugin with expected hooks', () => {
413
+ const plugin = createStickyLayoutPlugin();
414
+ expect(plugin.afterDraw).toBeDefined();
415
+ expect(plugin.beforeDraw).toBeDefined();
416
+ expect(plugin.dispose).toBeDefined();
417
+ });
418
+
419
+ it('afterDraw attaches resize and scroll listeners', () => {
420
+ const addEventListenerSpy = vi.spyOn(window, 'addEventListener');
421
+ const plugin = createStickyLayoutPlugin();
422
+
423
+ const scrollContainer = document.createElement('div');
424
+ scrollContainer.className = 'kt-table-wrapper';
425
+ const root = document.createElement('div');
426
+ root.appendChild(scrollContainer);
427
+
428
+ const table = document.createElement('table');
429
+ const thead = table.createTHead();
430
+ const tbody = table.createTBody();
431
+ scrollContainer.appendChild(table);
432
+
433
+ const ctx = {
434
+ rootElement: root,
435
+ tableElement: table,
436
+ theadElement: thead,
437
+ tbodyElement: tbody,
438
+ config: { lockedLayout: { stickyHeader: true } },
439
+ } as never;
440
+
441
+ plugin.afterDraw!(ctx);
442
+
443
+ expect(addEventListenerSpy).toHaveBeenCalledWith(
444
+ 'resize',
445
+ expect.any(Function),
446
+ );
447
+
448
+ plugin.dispose!(ctx);
449
+ addEventListenerSpy.mockRestore();
450
+ });
451
+
452
+ it('dispose removes resize listener', () => {
453
+ const removeEventListenerSpy = vi.spyOn(window, 'removeEventListener');
454
+ const plugin = createStickyLayoutPlugin();
455
+
456
+ const scrollContainer = document.createElement('div');
457
+ scrollContainer.className = 'kt-table-wrapper';
458
+ const root = document.createElement('div');
459
+ root.appendChild(scrollContainer);
460
+
461
+ const table = document.createElement('table');
462
+ const thead = table.createTHead();
463
+ const tbody = table.createTBody();
464
+ scrollContainer.appendChild(table);
465
+
466
+ const ctx = {
467
+ rootElement: root,
468
+ tableElement: table,
469
+ theadElement: thead,
470
+ tbodyElement: tbody,
471
+ config: { lockedLayout: { stickyHeader: true } },
472
+ } as never;
473
+
474
+ plugin.afterDraw!(ctx);
475
+ plugin.dispose!(ctx);
476
+
477
+ expect(removeEventListenerSpy).toHaveBeenCalledWith(
478
+ 'resize',
479
+ expect.any(Function),
480
+ );
481
+
482
+ removeEventListenerSpy.mockRestore();
483
+ });
484
+ });