@keenthemes/ktui 1.2.3 → 1.2.5

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 (213) hide show
  1. package/dist/ktui.js +2244 -1061
  2. package/dist/ktui.min.js +1 -1
  3. package/dist/ktui.min.js.map +1 -1
  4. package/dist/styles.css +185 -40
  5. package/lib/cjs/components/context-menu/context-menu.d.ts +66 -0
  6. package/lib/cjs/components/context-menu/context-menu.d.ts.map +1 -0
  7. package/lib/cjs/components/context-menu/context-menu.js +423 -0
  8. package/lib/cjs/components/context-menu/context-menu.js.map +1 -0
  9. package/lib/cjs/components/context-menu/index.d.ts +7 -0
  10. package/lib/cjs/components/context-menu/index.d.ts.map +1 -0
  11. package/lib/cjs/components/context-menu/index.js +10 -0
  12. package/lib/cjs/components/context-menu/index.js.map +1 -0
  13. package/lib/cjs/components/context-menu/types.d.ts +30 -0
  14. package/lib/cjs/components/context-menu/types.d.ts.map +1 -0
  15. package/lib/cjs/components/context-menu/types.js +7 -0
  16. package/lib/cjs/components/context-menu/types.js.map +1 -0
  17. package/lib/cjs/components/datatable/datatable-checkbox.d.ts.map +1 -1
  18. package/lib/cjs/components/datatable/datatable-checkbox.js +34 -15
  19. package/lib/cjs/components/datatable/datatable-checkbox.js.map +1 -1
  20. package/lib/cjs/components/datatable/datatable-contracts.d.ts +66 -0
  21. package/lib/cjs/components/datatable/datatable-contracts.d.ts.map +1 -0
  22. package/lib/cjs/components/datatable/datatable-contracts.js +7 -0
  23. package/lib/cjs/components/datatable/datatable-contracts.js.map +1 -0
  24. package/lib/cjs/components/datatable/datatable-event-adapter.d.ts +7 -0
  25. package/lib/cjs/components/datatable/datatable-event-adapter.d.ts.map +1 -0
  26. package/lib/cjs/components/datatable/datatable-event-adapter.js +16 -0
  27. package/lib/cjs/components/datatable/datatable-event-adapter.js.map +1 -0
  28. package/lib/cjs/components/datatable/datatable-local-provider.d.ts +25 -0
  29. package/lib/cjs/components/datatable/datatable-local-provider.d.ts.map +1 -0
  30. package/lib/cjs/components/datatable/datatable-local-provider.js +190 -0
  31. package/lib/cjs/components/datatable/datatable-local-provider.js.map +1 -0
  32. package/lib/cjs/components/datatable/datatable-pagination-renderer.d.ts +15 -0
  33. package/lib/cjs/components/datatable/datatable-pagination-renderer.d.ts.map +1 -0
  34. package/lib/cjs/components/datatable/datatable-pagination-renderer.js +144 -0
  35. package/lib/cjs/components/datatable/datatable-pagination-renderer.js.map +1 -0
  36. package/lib/cjs/components/datatable/datatable-remote-provider.d.ts +25 -0
  37. package/lib/cjs/components/datatable/datatable-remote-provider.d.ts.map +1 -0
  38. package/lib/cjs/components/datatable/datatable-remote-provider.js +191 -0
  39. package/lib/cjs/components/datatable/datatable-remote-provider.js.map +1 -0
  40. package/lib/cjs/components/datatable/datatable-state-store.d.ts +21 -0
  41. package/lib/cjs/components/datatable/datatable-state-store.d.ts.map +1 -0
  42. package/lib/cjs/components/datatable/datatable-state-store.js +81 -0
  43. package/lib/cjs/components/datatable/datatable-state-store.js.map +1 -0
  44. package/lib/cjs/components/datatable/datatable-table-renderer.d.ts +16 -0
  45. package/lib/cjs/components/datatable/datatable-table-renderer.d.ts.map +1 -0
  46. package/lib/cjs/components/datatable/datatable-table-renderer.js +141 -0
  47. package/lib/cjs/components/datatable/datatable-table-renderer.js.map +1 -0
  48. package/lib/cjs/components/datatable/datatable.d.ts +9 -87
  49. package/lib/cjs/components/datatable/datatable.d.ts.map +1 -1
  50. package/lib/cjs/components/datatable/datatable.js +234 -740
  51. package/lib/cjs/components/datatable/datatable.js.map +1 -1
  52. package/lib/cjs/components/dropdown/dropdown.d.ts +2 -2
  53. package/lib/cjs/components/dropdown/dropdown.d.ts.map +1 -1
  54. package/lib/cjs/components/dropdown/dropdown.js +68 -31
  55. package/lib/cjs/components/dropdown/dropdown.js.map +1 -1
  56. package/lib/cjs/components/input-number/index.d.ts +7 -0
  57. package/lib/cjs/components/input-number/index.d.ts.map +1 -0
  58. package/lib/cjs/components/input-number/index.js +10 -0
  59. package/lib/cjs/components/input-number/index.js.map +1 -0
  60. package/lib/cjs/components/input-number/input-number.d.ts +40 -0
  61. package/lib/cjs/components/input-number/input-number.d.ts.map +1 -0
  62. package/lib/cjs/components/input-number/input-number.js +248 -0
  63. package/lib/cjs/components/input-number/input-number.js.map +1 -0
  64. package/lib/cjs/components/input-number/types.d.ts +30 -0
  65. package/lib/cjs/components/input-number/types.d.ts.map +1 -0
  66. package/lib/cjs/components/input-number/types.js +7 -0
  67. package/lib/cjs/components/input-number/types.js.map +1 -0
  68. package/lib/cjs/components/select/config.d.ts +1 -0
  69. package/lib/cjs/components/select/config.d.ts.map +1 -1
  70. package/lib/cjs/components/select/config.js +2 -1
  71. package/lib/cjs/components/select/config.js.map +1 -1
  72. package/lib/cjs/components/select/index.d.ts +1 -1
  73. package/lib/cjs/components/select/index.d.ts.map +1 -1
  74. package/lib/cjs/components/select/select.d.ts +8 -1
  75. package/lib/cjs/components/select/select.d.ts.map +1 -1
  76. package/lib/cjs/components/select/select.js +14 -1
  77. package/lib/cjs/components/select/select.js.map +1 -1
  78. package/lib/cjs/components/select/tags.d.ts.map +1 -1
  79. package/lib/cjs/components/select/tags.js +10 -0
  80. package/lib/cjs/components/select/tags.js.map +1 -1
  81. package/lib/cjs/index.d.ts +9 -1
  82. package/lib/cjs/index.d.ts.map +1 -1
  83. package/lib/cjs/index.js +11 -7
  84. package/lib/cjs/index.js.map +1 -1
  85. package/lib/cjs/init-all.d.ts +6 -0
  86. package/lib/cjs/init-all.d.ts.map +1 -0
  87. package/lib/cjs/init-all.js +17 -0
  88. package/lib/cjs/init-all.js.map +1 -0
  89. package/lib/cjs/legacy.d.ts +8 -0
  90. package/lib/cjs/legacy.d.ts.map +1 -0
  91. package/lib/cjs/legacy.js +26 -0
  92. package/lib/cjs/legacy.js.map +1 -0
  93. package/lib/esm/components/context-menu/context-menu.d.ts +66 -0
  94. package/lib/esm/components/context-menu/context-menu.d.ts.map +1 -0
  95. package/lib/esm/components/context-menu/context-menu.js +420 -0
  96. package/lib/esm/components/context-menu/context-menu.js.map +1 -0
  97. package/lib/esm/components/context-menu/index.d.ts +7 -0
  98. package/lib/esm/components/context-menu/index.d.ts.map +1 -0
  99. package/lib/esm/components/context-menu/index.js +6 -0
  100. package/lib/esm/components/context-menu/index.js.map +1 -0
  101. package/lib/esm/components/context-menu/types.d.ts +30 -0
  102. package/lib/esm/components/context-menu/types.d.ts.map +1 -0
  103. package/lib/esm/components/context-menu/types.js +6 -0
  104. package/lib/esm/components/context-menu/types.js.map +1 -0
  105. package/lib/esm/components/datatable/datatable-checkbox.d.ts.map +1 -1
  106. package/lib/esm/components/datatable/datatable-checkbox.js +34 -15
  107. package/lib/esm/components/datatable/datatable-checkbox.js.map +1 -1
  108. package/lib/esm/components/datatable/datatable-contracts.d.ts +66 -0
  109. package/lib/esm/components/datatable/datatable-contracts.d.ts.map +1 -0
  110. package/lib/esm/components/datatable/datatable-contracts.js +6 -0
  111. package/lib/esm/components/datatable/datatable-contracts.js.map +1 -0
  112. package/lib/esm/components/datatable/datatable-event-adapter.d.ts +7 -0
  113. package/lib/esm/components/datatable/datatable-event-adapter.d.ts.map +1 -0
  114. package/lib/esm/components/datatable/datatable-event-adapter.js +13 -0
  115. package/lib/esm/components/datatable/datatable-event-adapter.js.map +1 -0
  116. package/lib/esm/components/datatable/datatable-local-provider.d.ts +25 -0
  117. package/lib/esm/components/datatable/datatable-local-provider.d.ts.map +1 -0
  118. package/lib/esm/components/datatable/datatable-local-provider.js +187 -0
  119. package/lib/esm/components/datatable/datatable-local-provider.js.map +1 -0
  120. package/lib/esm/components/datatable/datatable-pagination-renderer.d.ts +15 -0
  121. package/lib/esm/components/datatable/datatable-pagination-renderer.d.ts.map +1 -0
  122. package/lib/esm/components/datatable/datatable-pagination-renderer.js +141 -0
  123. package/lib/esm/components/datatable/datatable-pagination-renderer.js.map +1 -0
  124. package/lib/esm/components/datatable/datatable-remote-provider.d.ts +25 -0
  125. package/lib/esm/components/datatable/datatable-remote-provider.d.ts.map +1 -0
  126. package/lib/esm/components/datatable/datatable-remote-provider.js +188 -0
  127. package/lib/esm/components/datatable/datatable-remote-provider.js.map +1 -0
  128. package/lib/esm/components/datatable/datatable-state-store.d.ts +21 -0
  129. package/lib/esm/components/datatable/datatable-state-store.d.ts.map +1 -0
  130. package/lib/esm/components/datatable/datatable-state-store.js +78 -0
  131. package/lib/esm/components/datatable/datatable-state-store.js.map +1 -0
  132. package/lib/esm/components/datatable/datatable-table-renderer.d.ts +16 -0
  133. package/lib/esm/components/datatable/datatable-table-renderer.d.ts.map +1 -0
  134. package/lib/esm/components/datatable/datatable-table-renderer.js +138 -0
  135. package/lib/esm/components/datatable/datatable-table-renderer.js.map +1 -0
  136. package/lib/esm/components/datatable/datatable.d.ts +9 -87
  137. package/lib/esm/components/datatable/datatable.d.ts.map +1 -1
  138. package/lib/esm/components/datatable/datatable.js +234 -740
  139. package/lib/esm/components/datatable/datatable.js.map +1 -1
  140. package/lib/esm/components/dropdown/dropdown.d.ts +2 -2
  141. package/lib/esm/components/dropdown/dropdown.d.ts.map +1 -1
  142. package/lib/esm/components/dropdown/dropdown.js +68 -31
  143. package/lib/esm/components/dropdown/dropdown.js.map +1 -1
  144. package/lib/esm/components/input-number/index.d.ts +7 -0
  145. package/lib/esm/components/input-number/index.d.ts.map +1 -0
  146. package/lib/esm/components/input-number/index.js +6 -0
  147. package/lib/esm/components/input-number/index.js.map +1 -0
  148. package/lib/esm/components/input-number/input-number.d.ts +40 -0
  149. package/lib/esm/components/input-number/input-number.d.ts.map +1 -0
  150. package/lib/esm/components/input-number/input-number.js +245 -0
  151. package/lib/esm/components/input-number/input-number.js.map +1 -0
  152. package/lib/esm/components/input-number/types.d.ts +30 -0
  153. package/lib/esm/components/input-number/types.d.ts.map +1 -0
  154. package/lib/esm/components/input-number/types.js +6 -0
  155. package/lib/esm/components/input-number/types.js.map +1 -0
  156. package/lib/esm/components/select/config.d.ts +1 -0
  157. package/lib/esm/components/select/config.d.ts.map +1 -1
  158. package/lib/esm/components/select/config.js +2 -1
  159. package/lib/esm/components/select/config.js.map +1 -1
  160. package/lib/esm/components/select/index.d.ts +1 -1
  161. package/lib/esm/components/select/index.d.ts.map +1 -1
  162. package/lib/esm/components/select/select.d.ts +8 -1
  163. package/lib/esm/components/select/select.d.ts.map +1 -1
  164. package/lib/esm/components/select/select.js +14 -1
  165. package/lib/esm/components/select/select.js.map +1 -1
  166. package/lib/esm/components/select/tags.d.ts.map +1 -1
  167. package/lib/esm/components/select/tags.js +11 -1
  168. package/lib/esm/components/select/tags.js.map +1 -1
  169. package/lib/esm/index.d.ts +9 -1
  170. package/lib/esm/index.d.ts.map +1 -1
  171. package/lib/esm/index.js +7 -5
  172. package/lib/esm/index.js.map +1 -1
  173. package/lib/esm/init-all.d.ts +6 -0
  174. package/lib/esm/init-all.d.ts.map +1 -0
  175. package/lib/esm/init-all.js +13 -0
  176. package/lib/esm/init-all.js.map +1 -0
  177. package/lib/esm/legacy.d.ts +8 -0
  178. package/lib/esm/legacy.d.ts.map +1 -0
  179. package/lib/esm/legacy.js +8 -0
  180. package/lib/esm/legacy.js.map +1 -0
  181. package/package.json +35 -11
  182. package/src/__tests__/entrypoints.test.ts +71 -0
  183. package/src/components/context-menu/__tests__/context-menu.test.ts +117 -0
  184. package/src/components/context-menu/context-menu.css +32 -0
  185. package/src/components/context-menu/context-menu.ts +529 -0
  186. package/src/components/context-menu/index.ts +10 -0
  187. package/src/components/context-menu/types.ts +32 -0
  188. package/src/components/datatable/__tests__/architecture-boundaries.test.ts +259 -0
  189. package/src/components/datatable/datatable-checkbox.ts +34 -23
  190. package/src/components/datatable/datatable-contracts.ts +96 -0
  191. package/src/components/datatable/datatable-event-adapter.ts +21 -0
  192. package/src/components/datatable/datatable-local-provider.ts +193 -0
  193. package/src/components/datatable/datatable-pagination-renderer.ts +225 -0
  194. package/src/components/datatable/datatable-remote-provider.ts +178 -0
  195. package/src/components/datatable/datatable-state-store.ts +94 -0
  196. package/src/components/datatable/datatable-table-renderer.ts +214 -0
  197. package/src/components/datatable/datatable.ts +250 -918
  198. package/src/components/dropdown/dropdown.ts +86 -58
  199. package/src/components/input/input-group.css +14 -1
  200. package/src/components/input-number/__tests__/input-number.test.ts +278 -0
  201. package/src/components/input-number/index.ts +11 -0
  202. package/src/components/input-number/input-number.ts +267 -0
  203. package/src/components/input-number/types.ts +32 -0
  204. package/src/components/select/__tests__/ux-behaviors.test.ts +72 -0
  205. package/src/components/select/config.ts +3 -1
  206. package/src/components/select/index.ts +1 -1
  207. package/src/components/select/select.css +23 -20
  208. package/src/components/select/select.ts +15 -1
  209. package/src/components/select/tags.ts +14 -1
  210. package/src/index.ts +18 -5
  211. package/src/init-all.ts +15 -0
  212. package/src/legacy.ts +9 -0
  213. package/styles.css +1 -0
@@ -0,0 +1,259 @@
1
+ /**
2
+ * Tests for datatable architecture boundaries introduced during internal refactor.
3
+ */
4
+
5
+ import { describe, it, expect, vi } from 'vitest';
6
+ import { KTDataTable } from '../datatable';
7
+ import { createDataTableEventAdapter } from '../datatable-event-adapter';
8
+ import { KTDataTableLocalDataProvider } from '../datatable-local-provider';
9
+ import { KTDataTableDomPaginationRenderer } from '../datatable-pagination-renderer';
10
+ import { KTDataTableRemoteDataProvider } from '../datatable-remote-provider';
11
+ import { KTDataTableConfigStateStore } from '../datatable-state-store';
12
+ import { KTDataTableDomTableRenderer } from '../datatable-table-renderer';
13
+ import { KTDataTableConfigInterface } from '../types';
14
+ import { waitFor } from './setup';
15
+
16
+ function createConfig(
17
+ overrides: Partial<KTDataTableConfigInterface> = {},
18
+ ): KTDataTableConfigInterface {
19
+ return {
20
+ pageSize: 10,
21
+ pageSizes: [10, 20],
22
+ pageMore: true,
23
+ pageMoreLimit: 3,
24
+ info: '{start}-{end} of {total}',
25
+ infoEmpty: 'No records found',
26
+ requestMethod: 'GET',
27
+ requestHeaders: {},
28
+ pagination: {
29
+ number: { class: 'number', text: '{page}' },
30
+ previous: { class: 'previous', text: 'Previous' },
31
+ next: { class: 'next', text: 'Next' },
32
+ more: { class: 'more', text: '...' },
33
+ },
34
+ search: {
35
+ delay: 0,
36
+ callback: (data, search) =>
37
+ data.filter((item) => String(item.name).includes(search)),
38
+ },
39
+ sort: {
40
+ callback: (data) => data,
41
+ },
42
+ attributes: {
43
+ table: '[data-kt-datatable-table="true"]',
44
+ info: '[data-kt-datatable-info="true"]',
45
+ size: '[data-kt-datatable-size="true"]',
46
+ pagination: '[data-kt-datatable-pagination="true"]',
47
+ spinner: '[data-kt-datatable-spinner="true"]',
48
+ check: '[data-kt-datatable-check="true"]',
49
+ checkbox: '[data-kt-datatable-row-check="true"]',
50
+ },
51
+ _state: {},
52
+ ...overrides,
53
+ } as KTDataTableConfigInterface;
54
+ }
55
+
56
+ describe('KTDataTable architecture boundaries', () => {
57
+ it('routes state transitions through the state store', () => {
58
+ const config = createConfig();
59
+ const store = new KTDataTableConfigStateStore(config);
60
+
61
+ store.setPage(3);
62
+ store.setPageSize(20);
63
+ store.setSort('name', 'asc');
64
+ store.setSearch('Ada');
65
+ store.setFilter({ column: 'status', type: 'text', value: 'active' });
66
+
67
+ expect(store.getState()).toMatchObject({
68
+ page: 1,
69
+ pageSize: 20,
70
+ sortField: 'name',
71
+ sortOrder: 'asc',
72
+ search: 'Ada',
73
+ });
74
+ expect(store.getState().filters).toEqual([
75
+ { column: 'status', type: 'text', value: 'active' },
76
+ ]);
77
+ expect(config._state.page).toBe(1);
78
+ });
79
+
80
+ it('emits through both legacy event channels from one adapter', () => {
81
+ const fireEvent = vi.fn();
82
+ const dispatchEvent = vi.fn();
83
+ const adapter = createDataTableEventAdapter(fireEvent, dispatchEvent);
84
+
85
+ adapter.emit('reload', { page: 1 });
86
+
87
+ expect(fireEvent).toHaveBeenCalledWith('reload', { page: 1 });
88
+ expect(dispatchEvent).toHaveBeenCalledWith('reload', { page: 1 });
89
+ });
90
+
91
+ it('extracts and pages local table data through the local provider', () => {
92
+ const config = createConfig({ pageSize: 1 });
93
+ const stateStore = new KTDataTableConfigStateStore(config);
94
+ const table = document.createElement('table');
95
+ const thead = table.createTHead();
96
+ thead.innerHTML = `
97
+ <tr>
98
+ <th data-kt-datatable-column="id">ID</th>
99
+ <th data-kt-datatable-column="name">Name</th>
100
+ </tr>
101
+ `;
102
+ const tbody = table.createTBody();
103
+ tbody.innerHTML = `
104
+ <tr><td>1</td><td>Ada</td></tr>
105
+ <tr><td>2</td><td>Grace</td></tr>
106
+ `;
107
+
108
+ const provider = new KTDataTableLocalDataProvider({
109
+ config,
110
+ elements: () => ({
111
+ tableElement: table,
112
+ tbodyElement: tbody,
113
+ theadElement: thead,
114
+ }),
115
+ getLogicalColumnCount: () => 2,
116
+ storeOriginalClasses: vi.fn(),
117
+ stateStore,
118
+ });
119
+
120
+ const result = provider.fetchSync();
121
+
122
+ expect(result.totalItems).toBe(2);
123
+ expect(result.data).toEqual([{ id: '1', name: 'Ada' }]);
124
+ expect(stateStore.getState().originalData).toHaveLength(2);
125
+ });
126
+
127
+ it('normalizes remote provider fetch results and emits response event', async () => {
128
+ const config = createConfig({ apiEndpoint: '/api/users' });
129
+ const stateStore = new KTDataTableConfigStateStore(config);
130
+ const emit = vi.fn();
131
+ global.fetch = vi.fn().mockResolvedValue(
132
+ new Response(
133
+ JSON.stringify({
134
+ data: [{ id: 1, name: 'Ada' }],
135
+ totalCount: 1,
136
+ }),
137
+ ),
138
+ );
139
+
140
+ const provider = new KTDataTableRemoteDataProvider({
141
+ config,
142
+ createUrl: (pathOrUrl: string) =>
143
+ new URL(pathOrUrl, window.location.origin),
144
+ eventAdapter: { emit },
145
+ noticeOnTable: vi.fn(),
146
+ stateStore,
147
+ });
148
+
149
+ const result = await provider.fetch();
150
+
151
+ expect(result.data).toEqual([{ id: 1, name: 'Ada' }]);
152
+ expect(result.totalItems).toBe(1);
153
+ expect(emit).toHaveBeenCalledWith('fetched', {
154
+ response: { data: [{ id: 1, name: 'Ada' }], totalCount: 1 },
155
+ });
156
+ });
157
+
158
+ it('renders table body output through the table renderer', () => {
159
+ const config = createConfig();
160
+ const stateStore = new KTDataTableConfigStateStore(config);
161
+ stateStore.setOriginalData([{ id: '1', name: 'Ada' }], []);
162
+ const table = document.createElement('table');
163
+ const thead = table.createTHead();
164
+ thead.innerHTML = `
165
+ <tr>
166
+ <th data-kt-datatable-column="id">ID</th>
167
+ <th data-kt-datatable-column="name">Name</th>
168
+ </tr>
169
+ `;
170
+ table.createTBody();
171
+
172
+ new KTDataTableDomTableRenderer().render({
173
+ config,
174
+ context: {} as KTDataTable,
175
+ data: [{ id: '1', name: 'Ada' }],
176
+ getLogicalColumnCount: () => 2,
177
+ getState: () => stateStore.getState(),
178
+ originalTbodyClass: 'body-class',
179
+ originalTrClasses: ['row-class'],
180
+ originalTdClasses: [['id-cell', 'name-cell']],
181
+ tableElement: table,
182
+ theadElement: thead,
183
+ });
184
+
185
+ expect(table.tBodies[0].className).toBe('body-class');
186
+ expect(table.tBodies[0].querySelector('tr')?.className).toBe('row-class');
187
+ expect(table.tBodies[0].querySelectorAll('td')[1].textContent).toBe('Ada');
188
+ });
189
+
190
+ it('renders pagination controls through the pagination renderer', async () => {
191
+ const renderer = new KTDataTableDomPaginationRenderer();
192
+ const config = createConfig();
193
+ const sizeElement = document.createElement('select');
194
+ const infoElement = document.createElement('div');
195
+ const paginationElement = document.createElement('div');
196
+ const paginateData = vi.fn();
197
+
198
+ renderer.render({
199
+ config,
200
+ dataLength: 10,
201
+ infoElement,
202
+ paginateData,
203
+ paginationElement,
204
+ reloadPageSize: vi.fn(),
205
+ sizeElement,
206
+ state: {
207
+ page: 2,
208
+ pageSize: 10,
209
+ totalItems: 25,
210
+ totalPages: 3,
211
+ filters: [],
212
+ } as never,
213
+ });
214
+
215
+ await waitFor(120);
216
+
217
+ expect(infoElement.textContent).toBe('11-20 of 25');
218
+ expect(sizeElement.querySelectorAll('option')).toHaveLength(2);
219
+ expect(paginationElement.querySelectorAll('button')).toHaveLength(5);
220
+ paginationElement.querySelectorAll('button')[1].click();
221
+ expect(paginateData).toHaveBeenCalledWith(1);
222
+ });
223
+
224
+ it('keeps local and remote provider selection compatible through KTDataTable', async () => {
225
+ const element = document.createElement('div');
226
+ element.innerHTML = `
227
+ <table data-kt-datatable-table="true">
228
+ <thead><tr><th data-kt-datatable-column="name">Name</th></tr></thead>
229
+ <tbody><tr><td>Ada</td></tr></tbody>
230
+ </table>
231
+ <div data-kt-datatable-info="true"></div>
232
+ <select data-kt-datatable-size="true"></select>
233
+ <div data-kt-datatable-pagination="true"></div>
234
+ `;
235
+ document.body.appendChild(element);
236
+
237
+ const fetchSpy = vi
238
+ .fn()
239
+ .mockResolvedValue(
240
+ new Response(
241
+ JSON.stringify({ data: [{ name: 'Ada' }], totalCount: 1 }),
242
+ ),
243
+ );
244
+ global.fetch = fetchSpy;
245
+
246
+ new KTDataTable(element, { stateSave: false });
247
+ expect(fetchSpy).not.toHaveBeenCalled();
248
+
249
+ const remoteElement = element.cloneNode(true) as HTMLElement;
250
+ document.body.appendChild(remoteElement);
251
+ new KTDataTable(remoteElement, {
252
+ apiEndpoint: '/api/users',
253
+ stateSave: false,
254
+ });
255
+ await waitFor(0);
256
+
257
+ expect(fetchSpy).toHaveBeenCalledTimes(1);
258
+ });
259
+ });
@@ -8,8 +8,10 @@
8
8
  import {
9
9
  KTDataTableConfigInterface,
10
10
  KTDataTableCheckChangePayloadInterface,
11
+ KTDataTableStateInterface,
11
12
  } from './types';
12
13
  import KTEventHandler from '../../helpers/event-handler';
14
+ import { KTCallableType } from '../../types';
13
15
 
14
16
  export interface KTDataTableCheckboxAPI {
15
17
  init(): void;
@@ -29,25 +31,31 @@ export function createCheckboxHandler(
29
31
  ): KTDataTableCheckboxAPI {
30
32
  let headerChecked = false;
31
33
  let headerCheckElement: HTMLInputElement | null = null;
32
- let targetElements: NodeListOf<HTMLInputElement> = null;
34
+ let targetElements: NodeListOf<HTMLInputElement> | null = null;
33
35
 
34
36
  // Default: preserve selection across all pages
35
37
  const preserveSelection = config.checkbox?.preserveSelection !== false;
36
38
 
39
+ function ensureState(): KTDataTableStateInterface {
40
+ let state = config._state;
41
+ if (!state) {
42
+ state = {} as KTDataTableStateInterface;
43
+ config._state = state;
44
+ }
45
+ return state;
46
+ }
47
+
37
48
  // Helper: get selectedRows from state, always as string[]
38
49
  function getSelectedRows(): string[] {
39
- if (!config._state)
40
- config._state = {} as unknown as KTDataTableConfigInterface['_state'];
41
- if (!Array.isArray(config._state.selectedRows))
42
- config._state.selectedRows = [];
43
- return config._state.selectedRows.map(String);
50
+ const state = ensureState();
51
+ if (!Array.isArray(state.selectedRows)) state.selectedRows = [];
52
+ return state.selectedRows.map(String);
44
53
  }
45
54
 
46
55
  // Helper: set selectedRows in state
47
56
  function setSelectedRows(rows: string[]) {
48
- if (!config._state)
49
- config._state = {} as unknown as KTDataTableConfigInterface['_state'];
50
- config._state.selectedRows = Array.from(new Set(rows.map(String)));
57
+ const state = ensureState();
58
+ state.selectedRows = Array.from(new Set(rows.map(String)));
51
59
  }
52
60
 
53
61
  // Helper: get all visible row IDs (values)
@@ -64,14 +72,14 @@ export function createCheckboxHandler(
64
72
  };
65
73
 
66
74
  function init() {
67
- headerCheckElement = element.querySelector<HTMLInputElement>(
68
- config.attributes.check,
69
- );
75
+ const attrs = config.attributes;
76
+ if (!attrs?.check || !attrs.checkbox) {
77
+ return;
78
+ }
79
+ headerCheckElement = element.querySelector<HTMLInputElement>(attrs.check);
70
80
  if (!headerCheckElement) return;
71
81
  headerChecked = headerCheckElement.checked;
72
- targetElements = element.querySelectorAll(
73
- config.attributes.checkbox,
74
- ) as NodeListOf<HTMLInputElement>;
82
+ targetElements = element.querySelectorAll<HTMLInputElement>(attrs.checkbox);
75
83
  checkboxHandler();
76
84
  reapplyCheckedStates();
77
85
  updateHeaderCheckboxState();
@@ -79,14 +87,16 @@ export function createCheckboxHandler(
79
87
 
80
88
  function checkboxHandler() {
81
89
  if (!headerCheckElement) return;
90
+ const rowCheckboxSelector = config.attributes?.checkbox;
91
+ if (!rowCheckboxSelector) return;
82
92
  headerCheckElement.addEventListener('click', checkboxListener);
83
93
  KTEventHandler.on(
84
94
  document.body,
85
- config.attributes.checkbox,
95
+ rowCheckboxSelector,
86
96
  'input',
87
- (event: Event) => {
88
- handleRowCheckboxChange(event as InputEvent);
89
- },
97
+ ((event?: Event) => {
98
+ if (event) handleRowCheckboxChange(event);
99
+ }) as KTCallableType,
90
100
  );
91
101
  }
92
102
 
@@ -233,10 +243,11 @@ export function createCheckboxHandler(
233
243
  }
234
244
 
235
245
  function updateState() {
236
- // Called after redraw/pagination
237
- targetElements = element.querySelectorAll(
238
- config.attributes.checkbox,
239
- ) as NodeListOf<HTMLInputElement>;
246
+ const rowCheckSel = config.attributes?.checkbox;
247
+ if (!rowCheckSel) {
248
+ return;
249
+ }
250
+ targetElements = element.querySelectorAll<HTMLInputElement>(rowCheckSel);
240
251
  reapplyCheckedStates();
241
252
  updateHeaderCheckboxState();
242
253
  }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * KTUI - Free & Open-Source Tailwind UI Components by Keenthemes
3
+ * Copyright 2025 by Keenthemes Inc
4
+ */
5
+
6
+ import {
7
+ KTDataTableAttributeInterface,
8
+ KTDataTableColumnFilterInterface,
9
+ KTDataTableConfigInterface,
10
+ KTDataTableDataInterface,
11
+ KTDataTableInterface,
12
+ KTDataTableSortOrderInterface,
13
+ KTDataTableStateInterface,
14
+ } from './types';
15
+
16
+ export type KTDataTableEmit = (eventName: string, eventData?: object) => void;
17
+ export type KTDataTableCleanup = () => void;
18
+
19
+ export interface KTDataTableEventAdapter {
20
+ emit(eventName: string, eventData?: object): void;
21
+ }
22
+
23
+ export interface KTDataTableStateStore {
24
+ getState(): KTDataTableStateInterface;
25
+ replaceState(state: KTDataTableStateInterface): void;
26
+ patchState(state: Partial<KTDataTableStateInterface>): void;
27
+ setPage(page: number): void;
28
+ setPageSize(pageSize: number, page?: number): void;
29
+ setSort(
30
+ field: keyof KTDataTableDataInterface | number,
31
+ order: KTDataTableSortOrderInterface,
32
+ ): void;
33
+ setSearch(search: string | object): void;
34
+ setFilter(filter: KTDataTableColumnFilterInterface): void;
35
+ setOriginalData(
36
+ originalData: KTDataTableDataInterface[],
37
+ originalDataAttributes: KTDataTableAttributeInterface[],
38
+ ): void;
39
+ }
40
+
41
+ export interface KTDataTableProviderResult<T extends KTDataTableDataInterface> {
42
+ data: T[];
43
+ totalItems: number;
44
+ response?: unknown;
45
+ skipped?: boolean;
46
+ }
47
+
48
+ export interface KTDataTableDataProvider<T extends KTDataTableDataInterface> {
49
+ fetch(): Promise<KTDataTableProviderResult<T>>;
50
+ dispose?(): void;
51
+ }
52
+
53
+ export interface KTDataTableLocalProviderElements {
54
+ tableElement: HTMLTableElement;
55
+ tbodyElement: HTMLTableSectionElement;
56
+ theadElement: HTMLTableSectionElement;
57
+ }
58
+
59
+ export interface KTDataTableTableRendererInput<
60
+ T extends KTDataTableDataInterface,
61
+ > {
62
+ config: KTDataTableConfigInterface;
63
+ context: KTDataTableInterface;
64
+ data: T[];
65
+ getLogicalColumnCount: () => number;
66
+ getState: () => KTDataTableStateInterface;
67
+ originalTbodyClass: string;
68
+ originalTrClasses: string[];
69
+ originalTdClasses: string[][];
70
+ tableElement: HTMLTableElement;
71
+ theadElement: HTMLTableSectionElement;
72
+ }
73
+
74
+ export interface KTDataTableTableRenderer<T extends KTDataTableDataInterface> {
75
+ render(input: KTDataTableTableRendererInput<T>): HTMLTableSectionElement;
76
+ notice(
77
+ tableElement: HTMLTableElement,
78
+ getLogicalColumnCount: () => number,
79
+ message?: string,
80
+ ): void;
81
+ }
82
+
83
+ export interface KTDataTablePaginationRendererInput {
84
+ config: KTDataTableConfigInterface;
85
+ dataLength: number;
86
+ infoElement?: HTMLElement | null;
87
+ paginateData: (page: number) => void;
88
+ paginationElement?: HTMLElement | null;
89
+ reloadPageSize: (pageSize: number, page?: number) => void;
90
+ sizeElement?: HTMLSelectElement | null;
91
+ state: KTDataTableStateInterface;
92
+ }
93
+
94
+ export interface KTDataTablePaginationRenderer {
95
+ render(input: KTDataTablePaginationRendererInput): KTDataTableCleanup | void;
96
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * KTUI - Free & Open-Source Tailwind UI Components by Keenthemes
3
+ * Copyright 2025 by Keenthemes Inc
4
+ */
5
+
6
+ import {
7
+ KTDataTableEmit,
8
+ KTDataTableEventAdapter,
9
+ } from './datatable-contracts';
10
+
11
+ export function createDataTableEventAdapter(
12
+ fireEvent: KTDataTableEmit,
13
+ dispatchEvent: KTDataTableEmit,
14
+ ): KTDataTableEventAdapter {
15
+ return {
16
+ emit(eventName: string, eventData?: object): void {
17
+ fireEvent(eventName, eventData);
18
+ dispatchEvent(eventName, eventData);
19
+ },
20
+ };
21
+ }
@@ -0,0 +1,193 @@
1
+ /**
2
+ * KTUI - Free & Open-Source Tailwind UI Components by Keenthemes
3
+ * Copyright 2025 by Keenthemes Inc
4
+ */
5
+
6
+ import KTUtils from '../../helpers/utils';
7
+ import {
8
+ KTDataTableAttributeInterface,
9
+ KTDataTableConfigInterface,
10
+ KTDataTableDataInterface,
11
+ KTDataTableStateInterface,
12
+ } from './types';
13
+ import {
14
+ KTDataTableDataProvider,
15
+ KTDataTableLocalProviderElements,
16
+ KTDataTableProviderResult,
17
+ KTDataTableStateStore,
18
+ } from './datatable-contracts';
19
+
20
+ interface KTDataTableLocalProviderOptions<T extends KTDataTableDataInterface> {
21
+ config: KTDataTableConfigInterface;
22
+ elements: () => KTDataTableLocalProviderElements;
23
+ getLogicalColumnCount: () => number;
24
+ storeOriginalClasses: () => void;
25
+ stateStore: KTDataTableStateStore;
26
+ }
27
+
28
+ export class KTDataTableLocalDataProvider<
29
+ T extends KTDataTableDataInterface,
30
+ > implements KTDataTableDataProvider<T> {
31
+ constructor(private readonly options: KTDataTableLocalProviderOptions<T>) {}
32
+
33
+ public async fetch(): Promise<KTDataTableProviderResult<T>> {
34
+ return this.fetchSync();
35
+ }
36
+
37
+ public fetchSync(): KTDataTableProviderResult<T> {
38
+ const state = this.options.stateStore.getState();
39
+ let { originalData } = state;
40
+
41
+ if (
42
+ !this.options.elements().tableElement ||
43
+ originalData === undefined ||
44
+ this.tableConfigInvalidate() ||
45
+ this.localTableHeaderInvalidate() ||
46
+ this.localTableContentInvalidate()
47
+ ) {
48
+ const { originalData, originalDataAttributes } =
49
+ this.localExtractTableContent();
50
+
51
+ this.options.stateStore.setOriginalData(
52
+ originalData,
53
+ originalDataAttributes,
54
+ );
55
+ }
56
+
57
+ originalData = this.options.stateStore.getState().originalData;
58
+ let data = [...originalData] as T[];
59
+ let filteredData = data;
60
+
61
+ const { sortField, sortOrder, page, pageSize, search } =
62
+ this.options.stateStore.getState();
63
+
64
+ if (search) {
65
+ const searchTerm = typeof search === 'string' ? search : '';
66
+ const searchCallback = this.options.config.search?.callback;
67
+ if (searchCallback) {
68
+ filteredData = data = searchCallback.call(
69
+ this,
70
+ data,
71
+ searchTerm,
72
+ ) as T[];
73
+ }
74
+ }
75
+
76
+ const sortCallback = this.options.config.sort?.callback;
77
+ if (
78
+ sortField !== undefined &&
79
+ sortOrder !== undefined &&
80
+ sortOrder !== '' &&
81
+ typeof sortCallback === 'function'
82
+ ) {
83
+ data = sortCallback.call(this, data, sortField as string, sortOrder) as T[];
84
+ }
85
+
86
+ if (data?.length > 0) {
87
+ const startIndex = (page - 1) * pageSize;
88
+ const endIndex = startIndex + pageSize;
89
+ data = data.slice(startIndex, endIndex) as T[];
90
+ }
91
+
92
+ return {
93
+ data,
94
+ totalItems: filteredData.length,
95
+ };
96
+ }
97
+
98
+ private localTableContentInvalidate(): boolean {
99
+ const { tbodyElement } = this.options.elements();
100
+ const checksum: string = KTUtils.checksum(
101
+ JSON.stringify(tbodyElement.innerHTML),
102
+ );
103
+
104
+ if (this.options.stateStore.getState()._contentChecksum !== checksum) {
105
+ this.options.stateStore.patchState({ _contentChecksum: checksum });
106
+ return true;
107
+ }
108
+
109
+ return false;
110
+ }
111
+
112
+ private tableConfigInvalidate(): boolean {
113
+ const { _state, ...restConfig } = this.options.config;
114
+ const checksum: string = KTUtils.checksum(JSON.stringify(restConfig));
115
+
116
+ if ((_state?._configChecksum ?? '') !== checksum) {
117
+ this.options.stateStore.patchState({ _configChecksum: checksum });
118
+ return true;
119
+ }
120
+
121
+ return false;
122
+ }
123
+
124
+ private localExtractTableContent(): {
125
+ originalData: T[];
126
+ originalDataAttributes: KTDataTableAttributeInterface[];
127
+ } {
128
+ const originalData: T[] = [];
129
+ const originalDataAttributes: KTDataTableAttributeInterface[] = [];
130
+ const { tbodyElement, theadElement } = this.options.elements();
131
+
132
+ this.options.storeOriginalClasses();
133
+
134
+ const rows = tbodyElement.querySelectorAll<HTMLTableRowElement>('tr');
135
+ const allThs: NodeListOf<HTMLTableCellElement> = theadElement
136
+ ? theadElement.querySelectorAll('th')
137
+ : ([] as unknown as NodeListOf<HTMLTableCellElement>);
138
+
139
+ const ths: HTMLTableCellElement[] = Array.from(allThs).filter((th) =>
140
+ th.hasAttribute('data-kt-datatable-column'),
141
+ );
142
+ const columnsByIndex: HTMLTableCellElement[] =
143
+ ths.length > 0 && ths.length !== allThs.length ? Array.from(allThs) : ths;
144
+
145
+ rows.forEach((row: HTMLTableRowElement) => {
146
+ const dataRow: T = {} as T;
147
+ const dataRowAttribute: KTDataTableAttributeInterface =
148
+ {} as KTDataTableAttributeInterface;
149
+
150
+ row.querySelectorAll<HTMLTableCellElement>('td').forEach((td, index) => {
151
+ const colName = columnsByIndex[index]?.getAttribute(
152
+ 'data-kt-datatable-column',
153
+ );
154
+ if (colName) {
155
+ dataRow[colName as keyof T] = td.innerHTML?.trim() as T[keyof T];
156
+ } else {
157
+ dataRow[index as keyof T] = td.innerHTML?.trim() as T[keyof T];
158
+ }
159
+ });
160
+
161
+ if (Object.keys(dataRow).length > 0) {
162
+ originalData.push(dataRow);
163
+ originalDataAttributes.push(dataRowAttribute);
164
+ }
165
+ });
166
+
167
+ return { originalData, originalDataAttributes };
168
+ }
169
+
170
+ private localTableHeaderInvalidate(): boolean {
171
+ const { originalData } = this.options.stateStore.getState();
172
+ const { theadElement } = this.options.elements();
173
+
174
+ const totalColumns = originalData.length
175
+ ? Object.keys(originalData[0]).length
176
+ : 0;
177
+
178
+ const allThs: NodeListOf<HTMLTableCellElement> = theadElement
179
+ ? theadElement.querySelectorAll('th')
180
+ : ([] as unknown as NodeListOf<HTMLTableCellElement>);
181
+ const thsWithColumn = Array.from(allThs).filter((th) =>
182
+ th.hasAttribute('data-kt-datatable-column'),
183
+ );
184
+ const currentTableHeaders =
185
+ thsWithColumn.length > 0
186
+ ? thsWithColumn.length !== allThs.length
187
+ ? allThs.length
188
+ : thsWithColumn.length
189
+ : this.options.getLogicalColumnCount();
190
+
191
+ return currentTableHeaders !== totalColumns;
192
+ }
193
+ }