@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
@@ -11,10 +11,8 @@ import {
11
11
  KTDataTableSortOrderInterface,
12
12
  KTDataTableStateInterface,
13
13
  KTDataTableColumnFilterInterface,
14
- KTDataTableAttributeInterface,
15
14
  } from './types';
16
15
  import { KTOptionType } from '../../types';
17
- import KTUtils from '../../helpers/utils';
18
16
  import KTComponents from '../../index';
19
17
  import KTData from '../../helpers/data';
20
18
  import {
@@ -22,6 +20,19 @@ import {
22
20
  KTDataTableCheckboxAPI,
23
21
  } from './datatable-checkbox';
24
22
  import { createSortHandler, KTDataTableSortAPI } from './datatable-sort';
23
+ import {
24
+ KTDataTableCleanup,
25
+ KTDataTableEventAdapter,
26
+ KTDataTablePaginationRenderer,
27
+ KTDataTableStateStore,
28
+ KTDataTableTableRenderer,
29
+ } from './datatable-contracts';
30
+ import { createDataTableEventAdapter } from './datatable-event-adapter';
31
+ import { KTDataTableLocalDataProvider } from './datatable-local-provider';
32
+ import { KTDataTableRemoteDataProvider } from './datatable-remote-provider';
33
+ import { KTDataTableConfigStateStore } from './datatable-state-store';
34
+ import { KTDataTableDomPaginationRenderer } from './datatable-pagination-renderer';
35
+ import { KTDataTableDomTableRenderer } from './datatable-table-renderer';
25
36
 
26
37
  /**
27
38
  * Custom DataTable plugin class with server-side API, pagination, and sorting
@@ -63,28 +74,23 @@ export class KTDataTable<T extends KTDataTableDataInterface>
63
74
  private _originalTdClasses: string[][] = []; // Store original td classes as a 2D array [row][col]
64
75
  private _originalThClasses: string[] = []; // Store original th classes
65
76
 
66
- private _infoElement: HTMLElement;
67
- private _sizeElement: HTMLSelectElement;
68
- private _paginationElement: HTMLElement;
77
+ private _infoElement: HTMLElement | null = null;
78
+ private _sizeElement: HTMLSelectElement | null = null;
79
+ private _paginationElement: HTMLElement | null = null;
69
80
 
70
81
  private _checkbox: KTDataTableCheckboxAPI;
71
82
  private _sortHandler: KTDataTableSortAPI<T>;
83
+ private _eventAdapter: KTDataTableEventAdapter;
84
+ private _stateStore: KTDataTableStateStore;
85
+ private _localProvider: KTDataTableLocalDataProvider<T>;
86
+ private _remoteProvider: KTDataTableRemoteDataProvider<T>;
87
+ private _tableRenderer: KTDataTableTableRenderer<T>;
88
+ private _paginationRenderer: KTDataTablePaginationRenderer;
89
+ private _cleanupCallbacks: KTDataTableCleanup[] = [];
72
90
 
73
91
  private _data: T[] = [];
74
92
  private _isFetching: boolean = false;
75
93
 
76
- /**
77
- * AbortController for cancelling previous fetch requests
78
- * Used to prevent race conditions when multiple requests are triggered rapidly
79
- */
80
- private _abortController: AbortController | null = null;
81
-
82
- /**
83
- * Request ID counter for tracking request sequence
84
- * Used to detect and ignore stale responses from older requests
85
- */
86
- private _requestId: number = 0;
87
-
88
94
  constructor(element: HTMLElement, config?: KTDataTableConfigInterface) {
89
95
  super();
90
96
 
@@ -100,21 +106,29 @@ export class KTDataTable<T extends KTDataTableDataInterface>
100
106
  this._defaultConfig = this._initDefaultConfig(config);
101
107
 
102
108
  this._init(element);
109
+ if (!this._element) {
110
+ return;
111
+ }
103
112
  this._buildConfig();
113
+ this._stateStore = new KTDataTableConfigStateStore(this._config);
114
+ this._eventAdapter = createDataTableEventAdapter(
115
+ this._fireEvent.bind(this),
116
+ this._dispatchEvent.bind(this),
117
+ );
104
118
 
105
119
  // Store the instance directly on the element
106
120
  KTDataTable.asElementWithInstance(element).instance = this;
107
121
 
108
122
  this._initElements();
123
+ this._tableRenderer = new KTDataTableDomTableRenderer<T>();
124
+ this._paginationRenderer = new KTDataTableDomPaginationRenderer();
125
+ this._initDataProviders();
109
126
 
110
127
  // Initialize checkbox handler
111
128
  this._checkbox = createCheckboxHandler(
112
129
  this._element,
113
130
  this._config,
114
- (eventName: string, eventData?: object) => {
115
- this._fireEvent(eventName, eventData);
116
- this._dispatchEvent(eventName, eventData);
117
- },
131
+ this._emit.bind(this),
118
132
  );
119
133
 
120
134
  // Initialize sort handler
@@ -126,8 +140,7 @@ export class KTDataTable<T extends KTDataTableDataInterface>
126
140
  sortOrder: this.getState().sortOrder,
127
141
  }),
128
142
  (field, order) => {
129
- this._config._state.sortField = field as never;
130
- this._config._state.sortOrder = order;
143
+ this._stateStore.setSort(field as never, order);
131
144
  },
132
145
  this._fireEvent.bind(this),
133
146
  this._dispatchEvent.bind(this),
@@ -146,8 +159,33 @@ export class KTDataTable<T extends KTDataTableDataInterface>
146
159
 
147
160
  this._updateData();
148
161
 
149
- this._fireEvent('init');
150
- this._dispatchEvent('init');
162
+ this._emit('init');
163
+ }
164
+
165
+ private _emit(eventName: string, eventData?: object): void {
166
+ this._eventAdapter.emit(eventName, eventData);
167
+ }
168
+
169
+ private _initDataProviders(): void {
170
+ this._localProvider = new KTDataTableLocalDataProvider<T>({
171
+ config: this._config,
172
+ elements: () => ({
173
+ tableElement: this._tableElement,
174
+ tbodyElement: this._tbodyElement,
175
+ theadElement: this._theadElement,
176
+ }),
177
+ getLogicalColumnCount: this._getLogicalColumnCount.bind(this),
178
+ storeOriginalClasses: this._storeOriginalClasses.bind(this),
179
+ stateStore: this._stateStore,
180
+ });
181
+
182
+ this._remoteProvider = new KTDataTableRemoteDataProvider<T>({
183
+ config: this._config,
184
+ createUrl: this._createUrl.bind(this),
185
+ eventAdapter: this._eventAdapter,
186
+ noticeOnTable: this._noticeOnTable.bind(this),
187
+ stateStore: this._stateStore,
188
+ });
151
189
  }
152
190
 
153
191
  /**
@@ -390,43 +428,35 @@ export class KTDataTable<T extends KTDataTableDataInterface>
390
428
  * @returns {void}
391
429
  */
392
430
  private _initElements(): void {
393
- /**
394
- * Data table element
395
- */
396
- this._tableElement = this._element.querySelector<HTMLTableElement>(
397
- this._config.attributes.table,
398
- )!;
399
- /**
400
- * Table body element
401
- */
431
+ const root = this._element;
432
+ const attrs = this._config.attributes;
433
+ if (!root || !attrs?.table) {
434
+ throw new Error('KTDataTable: root element and table selector are required');
435
+ }
436
+
437
+ const tableEl = root.querySelector<HTMLTableElement>(attrs.table);
438
+ if (!tableEl) {
439
+ throw new Error(`KTDataTable: table element not found (${attrs.table})`);
440
+ }
441
+ this._tableElement = tableEl;
442
+
402
443
  this._tbodyElement =
403
444
  this._tableElement.tBodies[0] || this._tableElement.createTBody();
404
- /**
405
- * Table head element
406
- */
407
- this._theadElement = this._tableElement.tHead!;
408
445
 
409
- // Store original classes
446
+ this._theadElement =
447
+ this._tableElement.tHead ?? this._tableElement.createTHead();
448
+
410
449
  this._storeOriginalClasses();
411
450
 
412
- /**
413
- * Pagination info element
414
- */
415
- this._infoElement = this._element.querySelector<HTMLElement>(
416
- this._config.attributes.info,
417
- )!;
418
- /**
419
- * Page size dropdown element
420
- */
421
- this._sizeElement = this._element.querySelector<HTMLSelectElement>(
422
- this._config.attributes.size,
423
- )!;
424
- /**
425
- * Pagination element
426
- */
427
- this._paginationElement = this._element.querySelector<HTMLElement>(
428
- this._config.attributes.pagination,
429
- )!;
451
+ this._infoElement = attrs.info
452
+ ? root.querySelector<HTMLElement>(attrs.info)
453
+ : null;
454
+ this._sizeElement = attrs.size
455
+ ? root.querySelector<HTMLSelectElement>(attrs.size)
456
+ : null;
457
+ this._paginationElement = attrs.pagination
458
+ ? root.querySelector<HTMLElement>(attrs.pagination)
459
+ : null;
430
460
  }
431
461
 
432
462
  /**
@@ -480,14 +510,20 @@ export class KTDataTable<T extends KTDataTableDataInterface>
480
510
  try {
481
511
  this._showSpinner(); // Show spinner before fetching data
482
512
 
483
- // Fetch data and finalize - properly await to ensure finally block runs after completion
484
- if (typeof this._config.apiEndpoint === 'undefined') {
485
- await this._fetchDataFromLocal();
486
- await this._finalize();
487
- } else {
488
- await this._fetchDataFromServer();
489
- await this._finalize();
513
+ this._emit('fetch');
514
+ const result =
515
+ typeof this._config.apiEndpoint === 'undefined'
516
+ ? this._localProvider.fetchSync()
517
+ : await this._remoteProvider.fetch();
518
+
519
+ if (!result.skipped) {
520
+ this._data = result.data;
521
+ this._stateStore.patchState({ totalItems: result.totalItems });
522
+ await this._draw();
523
+ this._emit('fetched');
490
524
  }
525
+
526
+ await this._finalize();
491
527
  } finally {
492
528
  // Finally block now correctly executes after promises resolve, not immediately
493
529
  this._isFetching = false;
@@ -499,7 +535,7 @@ export class KTDataTable<T extends KTDataTableDataInterface>
499
535
  * @returns {void}
500
536
  */
501
537
  private _finalize(): void {
502
- this._element.classList.add('datatable-initialized');
538
+ this._element?.classList.add('datatable-initialized');
503
539
 
504
540
  // Initialize checkbox logic
505
541
  this._checkbox.init();
@@ -559,7 +595,7 @@ export class KTDataTable<T extends KTDataTableDataInterface>
559
595
  // Create a new debounced search function
560
596
  const debouncedSearch = this._debounce(() => {
561
597
  this.search(searchElement.value);
562
- }, this._config.search.delay);
598
+ }, this._config.search?.delay ?? 500);
563
599
 
564
600
  // Store the new debounced function as a property of the element
565
601
  searchWithDebounce._debouncedSearch = debouncedSearch;
@@ -569,188 +605,6 @@ export class KTDataTable<T extends KTDataTableDataInterface>
569
605
  }
570
606
  }
571
607
 
572
- /**
573
- * Fetch data from the DOM
574
- * Fetch data from the table element and save it to the `originalData` state property.
575
- * This method is used when the data is not fetched from the server via an API endpoint.
576
- */
577
- private async _fetchDataFromLocal(): Promise<void> {
578
- this._fireEvent('fetch');
579
- this._dispatchEvent('fetch');
580
-
581
- const { sortField, sortOrder, page, pageSize, search } = this.getState();
582
- let { originalData } = this.getState();
583
-
584
- // If the table element or the original data is not defined, bail
585
- if (
586
- !this._tableElement ||
587
- originalData === undefined ||
588
- this._tableConfigInvalidate() ||
589
- this._localTableHeaderInvalidate() ||
590
- this._localTableContentInvalidate()
591
- ) {
592
- this._deleteState();
593
-
594
- const { originalData, originalDataAttributes } =
595
- this._localExtractTableContent();
596
-
597
- this._config._state.originalData = originalData;
598
- this._config._state.originalDataAttributes = originalDataAttributes;
599
- }
600
-
601
- // Update the original data variable
602
- originalData = this.getState().originalData;
603
-
604
- // Clone the original data
605
- let _temp = (this._data = [...originalData] as T[]);
606
-
607
- if (search) {
608
- const searchTerm = typeof search === 'string' ? search : '';
609
- _temp = this._data = this._config.search.callback.call(
610
- this,
611
- this._data,
612
- searchTerm,
613
- ) as T[];
614
- }
615
-
616
- // If sorting is defined, sort the data
617
- if (
618
- sortField !== undefined &&
619
- sortOrder !== undefined &&
620
- sortOrder !== ''
621
- ) {
622
- if (typeof this._config.sort.callback === 'function') {
623
- this._data = this._config.sort.callback.call(
624
- this,
625
- this._data,
626
- sortField as string,
627
- sortOrder,
628
- ) as T[];
629
- }
630
- }
631
-
632
- // If there is data, slice it to the current page size
633
- if (this._data?.length > 0) {
634
- // Calculate the start and end indices for the current page
635
- const startIndex = (page - 1) * pageSize;
636
- const endIndex = startIndex + pageSize;
637
-
638
- this._data = this._data.slice(startIndex, endIndex) as T[];
639
- }
640
-
641
- // Determine number of total rows
642
- this._config._state.totalItems = _temp.length;
643
-
644
- // Draw the data
645
- await this._draw();
646
- this._fireEvent('fetched');
647
- this._dispatchEvent('fetched');
648
- }
649
-
650
- /**
651
- * Checks if the table content has been invalidated by comparing the current checksum of the table body
652
- * with the stored checksum in the state. If the checksums are different, the state is updated with the
653
- * new checksum and `true` is returned. Otherwise, `false` is returned.
654
- *
655
- * @returns {boolean} `true` if the table content has been invalidated, `false` otherwise.
656
- */
657
- private _localTableContentInvalidate(): boolean {
658
- const checksum: string = KTUtils.checksum(
659
- JSON.stringify(this._tbodyElement.innerHTML),
660
- );
661
- if (this.getState()._contentChecksum !== checksum) {
662
- this._config._state._contentChecksum = checksum;
663
- return true;
664
- }
665
- return false;
666
- }
667
-
668
- private _tableConfigInvalidate(): boolean {
669
- // Remove _data and _state from config
670
- const { _state, ...restConfig } = this._config;
671
- const checksum: string = KTUtils.checksum(JSON.stringify(restConfig));
672
- if (_state._configChecksum !== checksum) {
673
- this._config._state._configChecksum = checksum;
674
- return true;
675
- }
676
- return false;
677
- }
678
-
679
- /**
680
- * Extract the table content and returns it as an object containing an array of original data and an array of original data attributes.
681
- *
682
- * @returns {{originalData: T[], originalDataAttributes: KTDataTableAttributeInterface[]}} - An object containing an array of original data and an array of original data attributes.
683
- */
684
- private _localExtractTableContent(): {
685
- originalData: T[];
686
- originalDataAttributes: KTDataTableAttributeInterface[];
687
- } {
688
- const originalData: T[] = [];
689
- const originalDataAttributes: KTDataTableAttributeInterface[] = [];
690
-
691
- this._storeOriginalClasses();
692
-
693
- const rows = this._tbodyElement.querySelectorAll<HTMLTableRowElement>('tr');
694
-
695
- // Filter th elements to only include those with data-kt-datatable-column attribute
696
- const allThs: NodeListOf<HTMLTableCellElement> = this._theadElement
697
- ? this._theadElement.querySelectorAll('th')
698
- : ([] as unknown as NodeListOf<HTMLTableCellElement>);
699
-
700
- const ths: HTMLTableCellElement[] = Array.from(allThs).filter((th) =>
701
- th.hasAttribute('data-kt-datatable-column'),
702
- );
703
-
704
- rows.forEach((row: HTMLTableRowElement) => {
705
- const dataRow: T = {} as T;
706
- const dataRowAttribute: KTDataTableAttributeInterface =
707
- {} as KTDataTableAttributeInterface;
708
-
709
- row.querySelectorAll<HTMLTableCellElement>('td').forEach((td, index) => {
710
- const colName = ths[index]?.getAttribute('data-kt-datatable-column');
711
- if (colName) {
712
- dataRow[colName as keyof T] = td.innerHTML?.trim() as T[keyof T];
713
- } else {
714
- // Store the original HTML for fallback
715
- dataRow[index as keyof T] = td.innerHTML?.trim() as T[keyof T];
716
- }
717
- });
718
-
719
- if (Object.keys(dataRow).length > 0) {
720
- originalData.push(dataRow);
721
- originalDataAttributes.push(dataRowAttribute);
722
- }
723
- });
724
-
725
- return { originalData, originalDataAttributes };
726
- }
727
-
728
- /**
729
- * Check if the table header is invalidated
730
- * @returns {boolean} - Returns true if the table header is invalidated, false otherwise
731
- */
732
- private _localTableHeaderInvalidate(): boolean {
733
- const { originalData } = this.getState();
734
-
735
- const totalColumns = originalData.length
736
- ? Object.keys(originalData[0]).length
737
- : 0;
738
-
739
- // Count th elements with data-kt-datatable-column; when none (e.g. multi-row headers), use logical column count so we don't falsely invalidate
740
- const allThs: NodeListOf<HTMLTableCellElement> = this._theadElement
741
- ? this._theadElement.querySelectorAll('th')
742
- : ([] as unknown as NodeListOf<HTMLTableCellElement>);
743
- const thsWithColumn = Array.from(allThs).filter((th) =>
744
- th.hasAttribute('data-kt-datatable-column'),
745
- );
746
- const currentTableHeaders =
747
- thsWithColumn.length > 0
748
- ? thsWithColumn.length
749
- : this._getLogicalColumnCount();
750
-
751
- return currentTableHeaders !== totalColumns;
752
- }
753
-
754
608
  /**
755
609
  * Returns the logical data column count (number of data columns), used for multi-row headers
756
610
  * where querySelectorAll('th') would overcount. Prefers state.originalData, then first tbody row td count.
@@ -771,184 +625,6 @@ export class KTDataTable<T extends KTDataTableDataInterface>
771
625
  return 0;
772
626
  }
773
627
 
774
- /**
775
- * Fetch data from the server
776
- */
777
- private async _fetchDataFromServer(): Promise<void> {
778
- // Increment request ID to track this specific request
779
- const currentRequestId = ++this._requestId;
780
-
781
- this._fireEvent('fetch');
782
- this._dispatchEvent('fetch');
783
-
784
- const queryParams = this._getQueryParamsForFetchRequest();
785
-
786
- let response: Response;
787
- try {
788
- response = await this._performFetchRequest(queryParams);
789
- } catch (error) {
790
- // Silently ignore AbortError - request was cancelled
791
- if ((error as Error).name === 'AbortError') {
792
- return;
793
- }
794
- throw error;
795
- }
796
-
797
- // Check if this response is stale (a newer request has been initiated)
798
- if (currentRequestId !== this._requestId) {
799
- // Ignore stale response - a more recent request is in progress or has completed
800
- return;
801
- }
802
-
803
- let responseData = null;
804
-
805
- try {
806
- responseData = await response.json();
807
- } catch (error) {
808
- // Fire event with complete error context for application handling
809
- this._fireEvent('parseError', {
810
- response,
811
- error: String(error),
812
- status: response.status,
813
- statusText: response.statusText,
814
- });
815
- this._dispatchEvent('parseError', {
816
- response,
817
- error: String(error),
818
- status: response.status,
819
- statusText: response.statusText,
820
- });
821
- return;
822
- }
823
-
824
- // Double-check request ID after JSON parsing (additional safety)
825
- if (currentRequestId !== this._requestId) {
826
- return;
827
- }
828
-
829
- this._fireEvent('fetched', { response: responseData });
830
- this._dispatchEvent('fetched', { response: responseData });
831
-
832
- // Use the mapResponse function to transform the data if provided
833
- if (typeof this._config.mapResponse === 'function') {
834
- responseData = this._config.mapResponse.call(this, responseData);
835
- }
836
-
837
- this._data = responseData.data;
838
-
839
- this._config._state.totalItems = responseData.totalCount;
840
-
841
- await this._draw();
842
- this._fireEvent('fetched');
843
- this._dispatchEvent('fetched');
844
- }
845
-
846
- /**
847
- * Get the query params for a fetch request
848
- * @returns The query params for the fetch request
849
- */
850
- private _getQueryParamsForFetchRequest(): URLSearchParams {
851
- // Get the current state of the datatable
852
- const { page, pageSize, sortField, sortOrder, filters, search } =
853
- this.getState();
854
-
855
- // Create a new URLSearchParams object to store the query params
856
- let queryParams = new URLSearchParams();
857
-
858
- // Add the current page number and page size to the query params
859
- queryParams.set('page', String(page));
860
- queryParams.set('size', String(pageSize));
861
-
862
- // If there is a sort order and field set, add them to the query params
863
- if (sortOrder !== undefined) {
864
- queryParams.set('sortOrder', String(sortOrder));
865
- }
866
-
867
- if (sortField !== undefined) {
868
- queryParams.set('sortField', String(sortField));
869
- }
870
-
871
- // If there are any filters set, add them to the query params
872
- if (Array.isArray(filters) && filters.length) {
873
- queryParams.set(
874
- 'filters',
875
- JSON.stringify(
876
- filters.map((filter: KTDataTableColumnFilterInterface) => ({
877
- // Map the filter object to a simpler object with just the necessary properties
878
- column: filter.column,
879
- type: filter.type,
880
- value: filter.value,
881
- })),
882
- ),
883
- );
884
- }
885
-
886
- if (search) {
887
- queryParams.set(
888
- 'search',
889
- typeof search === 'object' ? JSON.stringify(search) : search,
890
- );
891
- }
892
-
893
- // If a mapRequest function is provided, call it with the query params object
894
- if (typeof this._config.mapRequest === 'function') {
895
- queryParams = this._config.mapRequest.call(this, queryParams);
896
- }
897
-
898
- // Return the query params object
899
- return queryParams;
900
- }
901
-
902
- private async _performFetchRequest(
903
- queryParams: URLSearchParams,
904
- ): Promise<Response> {
905
- const requestMethod: RequestInit['method'] = this._config.requestMethod;
906
- let requestBody: RequestInit['body'] | undefined = undefined;
907
-
908
- // Cancel previous request to prevent race conditions
909
- if (this._abortController) {
910
- this._abortController.abort();
911
- }
912
-
913
- // Create new AbortController for this request
914
- this._abortController = new AbortController();
915
-
916
- // If the request method is POST, send the query params as the request body
917
- if (requestMethod === 'POST') {
918
- requestBody = queryParams;
919
- } else if (requestMethod === 'GET') {
920
- // If the request method is GET, append the query params to the API endpoint
921
- const apiEndpointWithQueryParams = this._createUrl(
922
- this._config.apiEndpoint,
923
- );
924
- apiEndpointWithQueryParams.search = queryParams.toString();
925
- this._config.apiEndpoint = apiEndpointWithQueryParams.toString();
926
- }
927
-
928
- return fetch(this._config.apiEndpoint, {
929
- method: requestMethod,
930
- body: requestBody,
931
- headers: this._config.requestHeaders,
932
- ...(this._config.requestCredentials && {
933
- credentials: this._config.requestCredentials,
934
- }),
935
- // Add abort signal if available
936
- ...(this._abortController && { signal: this._abortController.signal }),
937
- }).catch((error) => {
938
- // Silently ignore AbortError - this is expected when requests are cancelled
939
- if (error.name === 'AbortError') {
940
- return Promise.reject(error);
941
- }
942
-
943
- // Trigger an error event for non-abort errors
944
- this._fireEvent('error', { error });
945
- this._dispatchEvent('error', { error });
946
-
947
- this._noticeOnTable('Error performing fetch request: ' + String(error));
948
- throw error;
949
- });
950
- }
951
-
952
628
  /**
953
629
  * Creates a complete URL from a relative path or a full URL.
954
630
  *
@@ -966,7 +642,9 @@ export class KTDataTable<T extends KTDataTableDataInterface>
966
642
 
967
643
  private _createUrl(
968
644
  pathOrUrl: string,
969
- baseUrl: string | null = window.location.origin,
645
+ baseUrl: string | null = typeof window !== 'undefined'
646
+ ? window.location.origin
647
+ : null,
970
648
  ): URL {
971
649
  // Regular expression to check if the input is a full URL
972
650
  const isFullUrl = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(pathOrUrl);
@@ -980,7 +658,39 @@ export class KTDataTable<T extends KTDataTableDataInterface>
980
658
  ? pathOrUrl
981
659
  : `/${pathOrUrl}`;
982
660
 
983
- return new URL(normalizedPath, baseUrl);
661
+ // Opaque origins (e.g. srcdoc iframes) serialize as the string "null", which is not a valid URL base.
662
+ const bases: string[] = [];
663
+ if (baseUrl && baseUrl !== 'null') {
664
+ bases.push(baseUrl);
665
+ }
666
+ if (typeof window !== 'undefined') {
667
+ const href = window.location.href;
668
+ if (href && !bases.includes(href)) {
669
+ bases.push(href);
670
+ }
671
+ try {
672
+ if (window.parent !== window && window.parent.location?.href) {
673
+ const parentHref = window.parent.location.href;
674
+ if (parentHref && !bases.includes(parentHref)) {
675
+ bases.push(parentHref);
676
+ }
677
+ }
678
+ } catch {
679
+ // parent is cross-origin
680
+ }
681
+ }
682
+
683
+ for (const base of bases) {
684
+ try {
685
+ return new URL(normalizedPath, base);
686
+ } catch {
687
+ // try next base
688
+ }
689
+ }
690
+
691
+ throw new Error(
692
+ `KTDataTable: cannot resolve relative apiEndpoint "${pathOrUrl}" (no valid base URL; use an absolute apiEndpoint).`,
693
+ );
984
694
  }
985
695
 
986
696
  /**
@@ -988,11 +698,12 @@ export class KTDataTable<T extends KTDataTableDataInterface>
988
698
  * @returns {Promise<void>} A promise that resolves when the table and pagination controls are updated
989
699
  */
990
700
  private async _draw(): Promise<void> {
991
- this._config._state.totalPages =
992
- Math.ceil(this.getState().totalItems / this.getState().pageSize) || 0;
701
+ this._stateStore.patchState({
702
+ totalPages:
703
+ Math.ceil(this.getState().totalItems / this.getState().pageSize) || 0,
704
+ });
993
705
 
994
- this._fireEvent('draw');
995
- this._dispatchEvent('draw');
706
+ this._emit('draw');
996
707
 
997
708
  this._dispose();
998
709
 
@@ -1001,12 +712,11 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1001
712
  this._updateTable();
1002
713
  }
1003
714
 
1004
- if (this._infoElement && this._paginationElement) {
715
+ if (this._infoElement || this._sizeElement || this._paginationElement) {
1005
716
  this._updatePagination();
1006
717
  }
1007
718
 
1008
- this._fireEvent('drew');
1009
- this._dispatchEvent('drew');
719
+ this._emit('drew');
1010
720
 
1011
721
  // Spinner is hidden in _finalize() to ensure it stays visible until the entire request completes
1012
722
  // Removed duplicate _hideSpinner() call here to prevent premature hiding
@@ -1021,143 +731,18 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1021
731
  * @returns {HTMLTableSectionElement} The new table body element
1022
732
  */
1023
733
  private _updateTable(): HTMLTableSectionElement {
1024
- // Clear the existing table contents using a more efficient method
1025
- while (this._tableElement.tBodies.length) {
1026
- this._tableElement.removeChild(this._tableElement.tBodies[0]);
1027
- }
1028
-
1029
- // Create the table body with the new data
1030
- const tbodyElement =
1031
- this._tableElement.createTBody() as HTMLTableSectionElement;
1032
-
1033
- // Apply the original class to the new tbody element
1034
- if (this._originalTbodyClass) {
1035
- tbodyElement.className = this._originalTbodyClass;
1036
- }
1037
-
1038
- this._updateTableContent(tbodyElement);
1039
-
1040
- return tbodyElement;
1041
- }
1042
-
1043
- /**
1044
- * Update the table content
1045
- * @param tbodyElement The table body element
1046
- * @returns {HTMLTableSectionElement} The updated table body element
1047
- */
1048
- private _updateTableContent(
1049
- tbodyElement: HTMLTableSectionElement,
1050
- ): HTMLTableSectionElement {
1051
- const fragment = document.createDocumentFragment();
1052
-
1053
- tbodyElement.textContent = ''; // Clear the tbody element
1054
-
1055
- if (this._data.length === 0) {
1056
- this._noticeOnTable(this._config.infoEmpty || '');
1057
- return tbodyElement;
1058
- }
1059
-
1060
- // Filter th elements to only include those with data-kt-datatable-column attribute
1061
- // This prevents creating blank td elements for merged header cells (colspan/rowspan)
1062
- const allThs: NodeListOf<HTMLTableCellElement> = this._theadElement
1063
- ? this._theadElement.querySelectorAll('th')
1064
- : ([] as unknown as NodeListOf<HTMLTableCellElement>);
1065
-
1066
- const ths: HTMLTableCellElement[] = Array.from(allThs).filter((th) =>
1067
- th.hasAttribute('data-kt-datatable-column'),
1068
- );
1069
- // When no th has data-kt-datatable-column (e.g. multi-row headers), use logical column count from tbody so we don't overcount thead cells
1070
- const columnsToRender: HTMLTableCellElement[] = ths.length > 0 ? ths : [];
1071
- const logicalColumnCount =
1072
- ths.length > 0 ? ths.length : this._getLogicalColumnCount();
1073
-
1074
- this._data.forEach((item: T, rowIndex: number) => {
1075
- const row = document.createElement('tr');
1076
-
1077
- // Apply original tr class if available
1078
- if (this._originalTrClasses && this._originalTrClasses[rowIndex]) {
1079
- row.className = this._originalTrClasses[rowIndex];
1080
- }
1081
-
1082
- if (!this._config.columns) {
1083
- const dataRowAttributes = this.getState().originalDataAttributes
1084
- ? this.getState().originalDataAttributes[rowIndex]
1085
- : null;
1086
-
1087
- for (let colIndex = 0; colIndex < logicalColumnCount; colIndex++) {
1088
- const th = columnsToRender[colIndex];
1089
- const colName = th?.getAttribute('data-kt-datatable-column');
1090
- const td = document.createElement('td');
1091
- let value: KTOptionType | '';
1092
- if (colName && Object.prototype.hasOwnProperty.call(item, colName)) {
1093
- value = item[colName as keyof T];
1094
- } else if (Object.prototype.hasOwnProperty.call(item, colIndex)) {
1095
- value = item[colIndex as keyof T];
1096
- } else {
1097
- value = '';
1098
- }
1099
- td.innerHTML = value as string;
1100
-
1101
- // Apply original td class if available
1102
- if (
1103
- this._originalTdClasses &&
1104
- this._originalTdClasses[rowIndex] &&
1105
- this._originalTdClasses[rowIndex][colIndex]
1106
- ) {
1107
- td.className = this._originalTdClasses[rowIndex][colIndex];
1108
- }
1109
-
1110
- if (dataRowAttributes && dataRowAttributes[colIndex]) {
1111
- for (const attr in dataRowAttributes[colIndex]) {
1112
- td.setAttribute(attr, dataRowAttributes[colIndex][attr]);
1113
- }
1114
- }
1115
-
1116
- row.appendChild(td);
1117
- }
1118
- } else {
1119
- Object.keys(this._config.columns).forEach(
1120
- (key: keyof T, colIndex: number) => {
1121
- const td = document.createElement('td');
1122
- const columnDef = this._config.columns[key as string];
1123
-
1124
- // Apply original td class if available
1125
- if (
1126
- this._originalTdClasses &&
1127
- this._originalTdClasses[rowIndex] &&
1128
- this._originalTdClasses[rowIndex][colIndex]
1129
- ) {
1130
- td.className = this._originalTdClasses[rowIndex][colIndex];
1131
- }
1132
-
1133
- if (typeof columnDef.render === 'function') {
1134
- const result = columnDef.render.call(this, item[key], item, this);
1135
- if (
1136
- result instanceof HTMLElement ||
1137
- result instanceof DocumentFragment
1138
- ) {
1139
- td.appendChild(result);
1140
- } else if (typeof result === 'string') {
1141
- td.innerHTML = result as string;
1142
- }
1143
- } else {
1144
- td.textContent = item[key] as string;
1145
- }
1146
-
1147
- if (typeof columnDef.createdCell === 'function') {
1148
- columnDef.createdCell.call(this, td, item[key], item, row);
1149
- }
1150
-
1151
- row.appendChild(td);
1152
- },
1153
- );
1154
- }
1155
-
1156
- fragment.appendChild(row);
734
+ return this._tableRenderer.render({
735
+ config: this._config,
736
+ context: this,
737
+ data: this._data,
738
+ getLogicalColumnCount: this._getLogicalColumnCount.bind(this),
739
+ getState: this.getState.bind(this),
740
+ originalTbodyClass: this._originalTbodyClass,
741
+ originalTrClasses: this._originalTrClasses,
742
+ originalTdClasses: this._originalTdClasses,
743
+ tableElement: this._tableElement,
744
+ theadElement: this._theadElement,
1157
745
  });
1158
-
1159
- tbodyElement.appendChild(fragment);
1160
- return tbodyElement;
1161
746
  }
1162
747
 
1163
748
  /**
@@ -1166,80 +751,28 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1166
751
  * @returns {void}
1167
752
  */
1168
753
  private _noticeOnTable(message: string = ''): void {
1169
- const row = this._tableElement.tBodies[0].insertRow();
1170
- const cell = row.insertCell();
1171
- const logicalCount = this._getLogicalColumnCount();
1172
- // Use logical column count so multi-row headers don't overcount; fallback to 1 when 0 so message still displays
1173
- cell.colSpan = logicalCount > 0 ? logicalCount : 1;
1174
- cell.innerHTML = message;
754
+ this._tableRenderer.notice(
755
+ this._tableElement,
756
+ this._getLogicalColumnCount.bind(this),
757
+ message,
758
+ );
1175
759
  }
1176
760
 
1177
761
  private _updatePagination(): void {
1178
- this._removeChildElements(this._sizeElement);
1179
- this._createPageSizeControls(this._sizeElement);
1180
-
1181
- this._removeChildElements(this._paginationElement);
1182
- this._createPaginationControls(this._infoElement, this._paginationElement);
1183
- }
1184
-
1185
- /**
1186
- * Removes all child elements from the given container element.
1187
- * @param container The container element to remove the child elements from.
1188
- */
1189
- private _removeChildElements(container: HTMLElement): void {
1190
- if (!container) {
1191
- return;
1192
- }
1193
-
1194
- // Loop through all child elements of the container and remove them one by one
1195
- while (container.firstChild) {
1196
- // Remove the first child element (which is the first element in the list of child elements)
1197
- container.removeChild(container.firstChild);
1198
- }
1199
- }
762
+ const cleanup = this._paginationRenderer.render({
763
+ config: this._config,
764
+ dataLength: this._data.length,
765
+ infoElement: this._infoElement,
766
+ paginateData: this._paginateData.bind(this),
767
+ paginationElement: this._paginationElement,
768
+ reloadPageSize: this._reloadPageSize.bind(this),
769
+ sizeElement: this._sizeElement,
770
+ state: this.getState(),
771
+ });
1200
772
 
1201
- /**
1202
- * Creates a container element for the items per page selector.
1203
- * @param _sizeElement The element to create the page size controls in.
1204
- * @returns The container element.
1205
- */
1206
- private _createPageSizeControls(
1207
- _sizeElement: HTMLSelectElement,
1208
- ): HTMLSelectElement {
1209
- // If no element is provided, return early
1210
- if (!_sizeElement) {
1211
- return _sizeElement;
773
+ if (typeof cleanup === 'function') {
774
+ this._cleanupCallbacks.push(cleanup);
1212
775
  }
1213
-
1214
- // Wait for the element to be attached to the DOM
1215
- setTimeout(() => {
1216
- // Create <option> elements for each page size option
1217
- const options = this._config.pageSizes.map((size: number) => {
1218
- const option = document.createElement('option') as HTMLOptionElement;
1219
- option.value = String(size);
1220
- option.text = String(size);
1221
- option.selected = this.getState().pageSize === size;
1222
- return option;
1223
- });
1224
-
1225
- // Add the <option> elements to the provided element
1226
- _sizeElement.append(...options);
1227
- }, 100);
1228
-
1229
- // Create an event listener for the "change" event on the element
1230
- const _pageSizeControlsEvent = (event: Event) => {
1231
- // When the element changes, reload the page with the new page size and page number 1
1232
- this._reloadPageSize(
1233
- Number((event.target as HTMLSelectElement).value),
1234
- 1,
1235
- );
1236
- };
1237
-
1238
- // Bind the event listener to the component instance
1239
- _sizeElement.onchange = _pageSizeControlsEvent.bind(this);
1240
-
1241
- // Return the element
1242
- return _sizeElement;
1243
776
  }
1244
777
 
1245
778
  /**
@@ -1249,191 +782,12 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1249
782
  */
1250
783
  private _reloadPageSize(pageSize: number, page: number = 1): void {
1251
784
  // Update the page size and page number in the state
1252
- this._config._state.pageSize = pageSize;
1253
- this._config._state.page = page;
785
+ this._stateStore.setPageSize(pageSize, page);
1254
786
 
1255
787
  // Update the data with the new page size and page number
1256
788
  this._updateData();
1257
789
  }
1258
790
 
1259
- /**
1260
- * Creates the pagination controls for the component.
1261
- * @param _infoElement The element to set the info text in.
1262
- * @param _paginationElement The element to create the pagination controls in.
1263
- * @return {HTMLElement} The element containing the pagination controls.
1264
- */
1265
- private _createPaginationControls(
1266
- _infoElement: HTMLElement,
1267
- _paginationElement: HTMLElement,
1268
- ): HTMLElement {
1269
- if (!_infoElement || !_paginationElement || this._data.length === 0) {
1270
- return null;
1271
- }
1272
-
1273
- this._setPaginationInfoText(_infoElement);
1274
- const paginationContainer =
1275
- this._createPaginationContainer(_paginationElement);
1276
-
1277
- if (paginationContainer) {
1278
- this._createPaginationButtons(paginationContainer);
1279
- }
1280
-
1281
- return paginationContainer;
1282
- }
1283
-
1284
- /**
1285
- * Sets the info text for the pagination controls.
1286
- * @param _infoElement The element to set the info text in.
1287
- */
1288
- private _setPaginationInfoText(_infoElement: HTMLElement): void {
1289
- _infoElement.textContent = this._config.info
1290
- .replace(
1291
- '{start}',
1292
- (this.getState().page - 1) * this.getState().pageSize + 1 + '',
1293
- )
1294
- .replace(
1295
- '{end}',
1296
- Math.min(
1297
- this.getState().page * this.getState().pageSize,
1298
- this.getState().totalItems,
1299
- ) + '',
1300
- )
1301
- .replace('{total}', this.getState().totalItems + '');
1302
- }
1303
-
1304
- /**
1305
- * Creates the container element for the pagination controls.
1306
- * @param _paginationElement The element to create the pagination controls in.
1307
- * @return {HTMLElement} The container element.
1308
- */
1309
- private _createPaginationContainer(
1310
- _paginationElement: HTMLElement,
1311
- ): HTMLElement {
1312
- // No longer create a wrapping div. Just return the pagination element itself.
1313
- return _paginationElement;
1314
- }
1315
-
1316
- /**
1317
- * Creates the pagination buttons for the component.
1318
- * @param paginationContainer The container element for the pagination controls.
1319
- */
1320
- private _createPaginationButtons(paginationContainer: HTMLElement): void {
1321
- const { page: currentPage, totalPages } = this.getState();
1322
- const { previous, next, number, more } = this._config.pagination;
1323
-
1324
- // Helper function to create a button
1325
- const createButton = (
1326
- text: string,
1327
- className: string,
1328
- disabled: boolean,
1329
- handleClick: () => void,
1330
- ): HTMLButtonElement => {
1331
- const button = document.createElement('button') as HTMLButtonElement;
1332
- button.className = className;
1333
- button.innerHTML = text;
1334
- button.disabled = disabled;
1335
- button.onclick = handleClick;
1336
- return button;
1337
- };
1338
-
1339
- // Add Previous Button
1340
- paginationContainer.appendChild(
1341
- createButton(
1342
- previous.text,
1343
- `${previous.class}${currentPage === 1 ? ' disabled' : ''}`,
1344
- currentPage === 1,
1345
- () => this._paginateData(currentPage - 1),
1346
- ),
1347
- );
1348
-
1349
- // Calculate range of pages
1350
- const pageMoreEnabled = this._config.pageMore;
1351
-
1352
- if (pageMoreEnabled) {
1353
- const maxButtons = this._config.pageMoreLimit;
1354
- const range = this._calculatePageRange(
1355
- currentPage,
1356
- totalPages,
1357
- maxButtons,
1358
- );
1359
-
1360
- // Add start ellipsis
1361
- if (range.start > 1) {
1362
- paginationContainer.appendChild(
1363
- createButton(more.text, more.class, false, () =>
1364
- this._paginateData(Math.max(1, range.start - 1)),
1365
- ),
1366
- );
1367
- }
1368
-
1369
- // Add page buttons
1370
- for (let i = range.start; i <= range.end; i++) {
1371
- paginationContainer.appendChild(
1372
- createButton(
1373
- number.text.replace('{page}', i.toString()),
1374
- `${number.class}${currentPage === i ? ' active disabled' : ''}`,
1375
- currentPage === i,
1376
- () => this._paginateData(i),
1377
- ),
1378
- );
1379
- }
1380
-
1381
- // Add end ellipsis
1382
- if (pageMoreEnabled && range.end < totalPages) {
1383
- paginationContainer.appendChild(
1384
- createButton(more.text, more.class, false, () =>
1385
- this._paginateData(Math.min(totalPages, range.end + 1)),
1386
- ),
1387
- );
1388
- }
1389
- } else {
1390
- // Add page buttons
1391
- for (let i = 1; i <= totalPages; i++) {
1392
- paginationContainer.appendChild(
1393
- createButton(
1394
- number.text.replace('{page}', i.toString()),
1395
- `${number.class}${currentPage === i ? ' active disabled' : ''}`,
1396
- currentPage === i,
1397
- () => this._paginateData(i),
1398
- ),
1399
- );
1400
- }
1401
- }
1402
-
1403
- // Add Next Button
1404
- paginationContainer.appendChild(
1405
- createButton(
1406
- next.text,
1407
- `${next.class}${currentPage === totalPages ? ' disabled' : ''}`,
1408
- currentPage === totalPages,
1409
- () => this._paginateData(currentPage + 1),
1410
- ),
1411
- );
1412
- }
1413
-
1414
- // New helper method to calculate page range
1415
- private _calculatePageRange(
1416
- currentPage: number,
1417
- totalPages: number,
1418
- maxButtons: number,
1419
- ): { start: number; end: number } {
1420
- let startPage: number, endPage: number;
1421
- const halfMaxButtons = Math.floor(maxButtons / 2);
1422
-
1423
- if (totalPages <= maxButtons) {
1424
- startPage = 1;
1425
- endPage = totalPages;
1426
- } else {
1427
- startPage = Math.max(currentPage - halfMaxButtons, 1);
1428
- endPage = Math.min(startPage + maxButtons - 1, totalPages);
1429
- if (endPage - startPage < maxButtons - 1) {
1430
- startPage = Math.max(endPage - maxButtons + 1, 1);
1431
- }
1432
- }
1433
-
1434
- return { start: startPage, end: endPage };
1435
- }
1436
-
1437
791
  /**
1438
792
  * Method for handling pagination
1439
793
  * @param page - The page number to navigate to
@@ -1443,49 +797,59 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1443
797
  return;
1444
798
  }
1445
799
 
1446
- this._fireEvent('pagination', { page: page });
1447
- this._dispatchEvent('pagination', { page: page });
800
+ this._emit('pagination', { page: page });
1448
801
 
1449
802
  if (page >= 1 && page <= this.getState().totalPages) {
1450
- this._config._state.page = page;
803
+ this._stateStore.setPage(page);
1451
804
  this._updateData();
1452
805
  }
1453
806
  }
1454
807
 
1455
808
  // Method to show the loading spinner
1456
809
  private _showSpinner(): void {
1457
- const spinner =
1458
- this._element.querySelector<HTMLElement>(
1459
- this._config.attributes.spinner,
1460
- ) || this._createSpinner();
810
+ const root = this._element;
811
+ const spinnerSel = this._config.attributes?.spinner;
812
+ const fromDom =
813
+ root && spinnerSel
814
+ ? root.querySelector<HTMLElement>(spinnerSel)
815
+ : null;
816
+ const spinner = fromDom ?? this._createSpinner();
1461
817
  if (spinner) {
1462
818
  spinner.style.display = 'block';
1463
819
  }
1464
- this._element.classList.add(this._config.loadingClass);
820
+ root?.classList.add(this._config.loadingClass ?? 'loading');
1465
821
  }
1466
822
 
1467
823
  // Method to hide the loading spinner
1468
824
  private _hideSpinner(): void {
1469
- const spinner = this._element.querySelector<HTMLElement>(
1470
- this._config.attributes.spinner,
1471
- );
825
+ const root = this._element;
826
+ const spinnerSel = this._config.attributes?.spinner;
827
+ const spinner =
828
+ root && spinnerSel
829
+ ? root.querySelector<HTMLElement>(spinnerSel)
830
+ : null;
1472
831
  if (spinner) {
1473
832
  spinner.style.display = 'none';
1474
833
  }
1475
- this._element.classList.remove(this._config.loadingClass);
834
+ root?.classList.remove(this._config.loadingClass ?? 'loading');
1476
835
  }
1477
836
 
1478
837
  // Method to create a spinner element if it doesn't exist
1479
- private _createSpinner(): HTMLElement {
1480
- if (typeof this._config.loading === 'undefined') {
838
+ private _createSpinner(): HTMLElement | null {
839
+ const loading = this._config.loading;
840
+ if (!loading) {
1481
841
  return null;
1482
842
  }
1483
843
 
1484
844
  const template = document.createElement('template');
1485
- template.innerHTML = this._config.loading.template
845
+ template.innerHTML = loading.template
1486
846
  .trim()
1487
- .replace('{content}', this._config.loading.content);
1488
- const spinner = template.content.firstChild as HTMLElement;
847
+ .replace('{content}', loading.content);
848
+ const first = template.content.firstChild;
849
+ if (!first || !(first instanceof HTMLElement)) {
850
+ return null;
851
+ }
852
+ const spinner = first;
1489
853
  spinner.setAttribute('data-kt-datatable-spinner', 'true');
1490
854
 
1491
855
  this._tableElement.appendChild(spinner);
@@ -1498,8 +862,7 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1498
862
  * @returns {void}
1499
863
  */
1500
864
  private _saveState(): void {
1501
- this._fireEvent('stateSave');
1502
- this._dispatchEvent('stateSave');
865
+ this._emit('stateSave');
1503
866
 
1504
867
  const ns: string = this._tableNamespace();
1505
868
 
@@ -1521,7 +884,7 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1521
884
 
1522
885
  try {
1523
886
  const state = JSON.parse(stateString) as KTDataTableStateInterface;
1524
- if (state) this._config._state = state;
887
+ if (state) this._stateStore.replaceState(state);
1525
888
  return state;
1526
889
  } catch {}
1527
890
 
@@ -1555,18 +918,15 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1555
918
  }
1556
919
 
1557
920
  private _tableId(): string {
1558
- let id: string = null;
1559
- // If the table element has an ID, use that
1560
- if (this._tableElement?.getAttribute('id')) {
1561
- id = this._tableElement.getAttribute('id') as string;
921
+ const tableIdAttr = this._tableElement?.getAttribute('id');
922
+ if (tableIdAttr) {
923
+ return tableIdAttr;
1562
924
  }
1563
-
1564
- // If the component element has an ID, use that
1565
- if (this._element?.getAttribute('id')) {
1566
- id = this._element.getAttribute('id') as string;
925
+ const rootIdAttr = this._element?.getAttribute('id');
926
+ if (rootIdAttr) {
927
+ return rootIdAttr;
1567
928
  }
1568
-
1569
- return id;
929
+ return '';
1570
930
  }
1571
931
 
1572
932
  /**
@@ -1574,6 +934,14 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1574
934
  * This method is called before re-rendering or when disposing the component.
1575
935
  */
1576
936
  private _dispose() {
937
+ const root = this._element;
938
+ if (!root) {
939
+ return;
940
+ }
941
+
942
+ this._cleanupCallbacks.forEach((cleanup) => cleanup());
943
+ this._cleanupCallbacks = [];
944
+
1577
945
  // --- 1. Remove search input event listener (debounced) ---
1578
946
  const tableId: string = this._tableId();
1579
947
  const searchElement: HTMLInputElement | null =
@@ -1583,14 +951,13 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1583
951
  if (searchElement) {
1584
952
  const searchWithDebounce =
1585
953
  KTDataTable.asSearchElementWithDebounce(searchElement);
1586
- if (!searchWithDebounce._debouncedSearch) {
1587
- return;
954
+ if (searchWithDebounce._debouncedSearch) {
955
+ searchElement.removeEventListener(
956
+ 'keyup',
957
+ searchWithDebounce._debouncedSearch,
958
+ );
959
+ delete searchWithDebounce._debouncedSearch;
1588
960
  }
1589
- searchElement.removeEventListener(
1590
- 'keyup',
1591
- searchWithDebounce._debouncedSearch,
1592
- );
1593
- delete searchWithDebounce._debouncedSearch;
1594
961
  }
1595
962
 
1596
963
  // --- 2. Remove page size dropdown event listener ---
@@ -1614,12 +981,12 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1614
981
  if (this._checkbox && typeof checkboxWithDispose.dispose === 'function') {
1615
982
  checkboxWithDispose.dispose();
1616
983
  } else {
1617
- // Remove header checkbox event listener if possible
1618
- const headerCheckElement = this._element.querySelector<HTMLInputElement>(
1619
- this._config.attributes.check,
1620
- );
1621
- if (headerCheckElement) {
1622
- headerCheckElement.replaceWith(headerCheckElement.cloneNode(true));
984
+ const checkSel = this._config.attributes?.check;
985
+ if (checkSel) {
986
+ const headerCheckElement = root.querySelector<HTMLInputElement>(checkSel);
987
+ if (headerCheckElement) {
988
+ headerCheckElement.replaceWith(headerCheckElement.cloneNode(true));
989
+ }
1623
990
  }
1624
991
  }
1625
992
  // KTDataTableSortAPI does not have a dispose method, but we can remove th click listeners by replacing them
@@ -1631,22 +998,23 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1631
998
  }
1632
999
 
1633
1000
  // --- 5. Remove spinner DOM node if it exists ---
1634
- const spinner = this._element.querySelector<HTMLElement>(
1635
- this._config.attributes.spinner,
1636
- );
1637
- if (spinner && spinner.parentNode) {
1638
- spinner.parentNode.removeChild(spinner);
1001
+ const spinnerSel = this._config.attributes?.spinner;
1002
+ if (spinnerSel) {
1003
+ const spinner = root.querySelector<HTMLElement>(spinnerSel);
1004
+ if (spinner?.parentNode) {
1005
+ spinner.parentNode.removeChild(spinner);
1006
+ }
1639
1007
  }
1640
- this._element.classList.remove(this._config.loadingClass);
1008
+ root.classList.remove(this._config.loadingClass ?? 'loading');
1641
1009
 
1642
1010
  // --- 6. Remove instance reference from the DOM element ---
1643
- const elementWithInstance = KTDataTable.asElementWithInstance(
1644
- this._element,
1645
- );
1011
+ const elementWithInstance = KTDataTable.asElementWithInstance(root);
1646
1012
  if (elementWithInstance.instance) {
1647
1013
  delete elementWithInstance.instance;
1648
1014
  }
1649
1015
 
1016
+ KTData.remove(root, this._name);
1017
+
1650
1018
  // --- 7. (Optional) Clear localStorage state ---
1651
1019
  // Uncomment the following line if you want to clear state on dispose:
1652
1020
  // this._deleteState();
@@ -1672,31 +1040,7 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1672
1040
  * @returns {KTDataTableStateInterface} The current state of the table.
1673
1041
  */
1674
1042
  public getState(): KTDataTableStateInterface {
1675
- return {
1676
- /**
1677
- * The current page number.
1678
- */
1679
- page: 1,
1680
- /**
1681
- * The field that the data is sorted by.
1682
- */
1683
- sortField: null,
1684
- /**
1685
- * The sort order (ascending or descending).
1686
- */
1687
- sortOrder: '',
1688
- /**
1689
- * The number of rows to display per page.
1690
- */
1691
- pageSize: this._config.pageSize,
1692
-
1693
- filters: [],
1694
-
1695
- /**
1696
- * Any additional state that may have been stored in the config.
1697
- */
1698
- ...this._config._state,
1699
- };
1043
+ return this._stateStore.getState();
1700
1044
  }
1701
1045
 
1702
1046
  /**
@@ -1712,10 +1056,8 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1712
1056
  field,
1713
1057
  );
1714
1058
  this._sortHandler.setSortIcon(field as keyof T, sortOrder);
1715
- this._config._state.sortField = field as never;
1716
- this._config._state.sortOrder = sortOrder;
1717
- this._fireEvent('sort', { field, order: sortOrder });
1718
- this._dispatchEvent('sort', { field, order: sortOrder });
1059
+ this._stateStore.setSort(field as never, sortOrder);
1060
+ this._emit('sort', { field, order: sortOrder });
1719
1061
  this._updateData();
1720
1062
  }
1721
1063
 
@@ -1753,16 +1095,14 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1753
1095
  * Triggers the 'reload' event and the 'kt.datatable.reload' custom event.
1754
1096
  */
1755
1097
  public reload(): void {
1756
- this._fireEvent('reload');
1757
- this._dispatchEvent('reload');
1098
+ this._emit('reload');
1758
1099
 
1759
1100
  // Fetch the data from the server using the current sort and filter settings
1760
1101
  this._updateData();
1761
1102
  }
1762
1103
 
1763
1104
  public redraw(page: number = 1): void {
1764
- this._fireEvent('redraw');
1765
- this._dispatchEvent('redraw');
1105
+ this._emit('redraw');
1766
1106
 
1767
1107
  this._paginateData(page);
1768
1108
  }
@@ -1795,23 +1135,17 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1795
1135
  * @throws Error if the filter object is null or undefined.
1796
1136
  */
1797
1137
  public setFilter(filter: KTDataTableColumnFilterInterface): KTDataTable<T> {
1798
- this._config._state.filters = [
1799
- ...(this.getState().filters || []).filter(
1800
- (f) => f.column !== filter.column,
1801
- ),
1802
- filter,
1803
- ];
1804
- this._config._state.page = 1;
1138
+ this._stateStore.setFilter(filter);
1805
1139
  return this;
1806
1140
  }
1807
1141
 
1808
1142
  public override dispose(): void {
1143
+ this._remoteProvider?.dispose();
1809
1144
  this._dispose();
1810
1145
  }
1811
1146
 
1812
1147
  public search(query: string | object): void {
1813
- this._config._state.search = query;
1814
- this._config._state.page = 1;
1148
+ this._stateStore.setSearch(query);
1815
1149
  this.reload();
1816
1150
  }
1817
1151
 
@@ -1924,8 +1258,7 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1924
1258
  */
1925
1259
  public check(): void {
1926
1260
  this._checkbox.check();
1927
- this._fireEvent('checked');
1928
- this._dispatchEvent('checked');
1261
+ this._emit('checked');
1929
1262
  }
1930
1263
 
1931
1264
  /**
@@ -1934,8 +1267,7 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1934
1267
  */
1935
1268
  public uncheck(): void {
1936
1269
  this._checkbox.uncheck();
1937
- this._fireEvent('unchecked');
1938
- this._dispatchEvent('unchecked');
1270
+ this._emit('unchecked');
1939
1271
  }
1940
1272
 
1941
1273
  /**