@keenthemes/ktui 1.2.5 → 1.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/README.md +14 -5
  2. package/dist/ktui.js +1538 -786
  3. package/dist/ktui.min.js +1 -1
  4. package/dist/ktui.min.js.map +1 -1
  5. package/dist/styles.css +85 -5
  6. package/lib/cjs/components/datatable/datatable-checkbox.d.ts +37 -1
  7. package/lib/cjs/components/datatable/datatable-checkbox.d.ts.map +1 -1
  8. package/lib/cjs/components/datatable/datatable-checkbox.js +143 -156
  9. package/lib/cjs/components/datatable/datatable-checkbox.js.map +1 -1
  10. package/lib/cjs/components/datatable/datatable-column-utils.d.ts +30 -0
  11. package/lib/cjs/components/datatable/datatable-column-utils.d.ts.map +1 -0
  12. package/lib/cjs/components/datatable/datatable-column-utils.js +42 -0
  13. package/lib/cjs/components/datatable/datatable-column-utils.js.map +1 -0
  14. package/lib/cjs/components/datatable/datatable-contracts.d.ts +2 -4
  15. package/lib/cjs/components/datatable/datatable-contracts.d.ts.map +1 -1
  16. package/lib/cjs/components/datatable/datatable-defaults.d.ts +20 -0
  17. package/lib/cjs/components/datatable/datatable-defaults.d.ts.map +1 -0
  18. package/lib/cjs/components/datatable/datatable-defaults.js +193 -0
  19. package/lib/cjs/components/datatable/datatable-defaults.js.map +1 -0
  20. package/lib/cjs/components/datatable/datatable-layout-plugin.d.ts +7 -0
  21. package/lib/cjs/components/datatable/datatable-layout-plugin.d.ts.map +1 -0
  22. package/lib/cjs/components/datatable/datatable-layout-plugin.js +338 -0
  23. package/lib/cjs/components/datatable/datatable-layout-plugin.js.map +1 -0
  24. package/lib/cjs/components/datatable/datatable-local-provider.d.ts +2 -2
  25. package/lib/cjs/components/datatable/datatable-local-provider.d.ts.map +1 -1
  26. package/lib/cjs/components/datatable/datatable-local-provider.js +85 -27
  27. package/lib/cjs/components/datatable/datatable-local-provider.js.map +1 -1
  28. package/lib/cjs/components/datatable/datatable-pagination-renderer.d.ts.map +1 -1
  29. package/lib/cjs/components/datatable/datatable-pagination-renderer.js +13 -13
  30. package/lib/cjs/components/datatable/datatable-pagination-renderer.js.map +1 -1
  31. package/lib/cjs/components/datatable/datatable-registry.d.ts +18 -0
  32. package/lib/cjs/components/datatable/datatable-registry.d.ts.map +1 -0
  33. package/lib/cjs/components/datatable/datatable-registry.js +66 -0
  34. package/lib/cjs/components/datatable/datatable-registry.js.map +1 -0
  35. package/lib/cjs/components/datatable/datatable-remote-provider.d.ts.map +1 -1
  36. package/lib/cjs/components/datatable/datatable-remote-provider.js +1 -2
  37. package/lib/cjs/components/datatable/datatable-remote-provider.js.map +1 -1
  38. package/lib/cjs/components/datatable/datatable-search-handler.d.ts +10 -0
  39. package/lib/cjs/components/datatable/datatable-search-handler.d.ts.map +1 -0
  40. package/lib/cjs/components/datatable/datatable-search-handler.js +65 -0
  41. package/lib/cjs/components/datatable/datatable-search-handler.js.map +1 -0
  42. package/lib/cjs/components/datatable/datatable-sort.d.ts +31 -4
  43. package/lib/cjs/components/datatable/datatable-sort.d.ts.map +1 -1
  44. package/lib/cjs/components/datatable/datatable-sort.js +86 -58
  45. package/lib/cjs/components/datatable/datatable-sort.js.map +1 -1
  46. package/lib/cjs/components/datatable/datatable-spinner.d.ts +30 -0
  47. package/lib/cjs/components/datatable/datatable-spinner.d.ts.map +1 -0
  48. package/lib/cjs/components/datatable/datatable-spinner.js +54 -0
  49. package/lib/cjs/components/datatable/datatable-spinner.js.map +1 -0
  50. package/lib/cjs/components/datatable/datatable-state-persistence.d.ts +19 -0
  51. package/lib/cjs/components/datatable/datatable-state-persistence.d.ts.map +1 -0
  52. package/lib/cjs/components/datatable/datatable-state-persistence.js +59 -0
  53. package/lib/cjs/components/datatable/datatable-state-persistence.js.map +1 -0
  54. package/lib/cjs/components/datatable/datatable-table-renderer.d.ts +2 -0
  55. package/lib/cjs/components/datatable/datatable-table-renderer.d.ts.map +1 -1
  56. package/lib/cjs/components/datatable/datatable-table-renderer.js +75 -16
  57. package/lib/cjs/components/datatable/datatable-table-renderer.js.map +1 -1
  58. package/lib/cjs/components/datatable/datatable-utils.d.ts +10 -0
  59. package/lib/cjs/components/datatable/datatable-utils.d.ts.map +1 -0
  60. package/lib/cjs/components/datatable/datatable-utils.js +15 -0
  61. package/lib/cjs/components/datatable/datatable-utils.js.map +1 -0
  62. package/lib/cjs/components/datatable/datatable.d.ts +35 -34
  63. package/lib/cjs/components/datatable/datatable.d.ts.map +1 -1
  64. package/lib/cjs/components/datatable/datatable.js +233 -497
  65. package/lib/cjs/components/datatable/datatable.js.map +1 -1
  66. package/lib/cjs/components/datatable/index.d.ts +1 -1
  67. package/lib/cjs/components/datatable/index.d.ts.map +1 -1
  68. package/lib/cjs/components/datatable/types.d.ts +127 -11
  69. package/lib/cjs/components/datatable/types.d.ts.map +1 -1
  70. package/lib/cjs/index.d.ts +1 -1
  71. package/lib/cjs/index.d.ts.map +1 -1
  72. package/lib/cjs/index.js +6 -0
  73. package/lib/cjs/index.js.map +1 -1
  74. package/lib/esm/components/datatable/datatable-checkbox.d.ts +37 -1
  75. package/lib/esm/components/datatable/datatable-checkbox.d.ts.map +1 -1
  76. package/lib/esm/components/datatable/datatable-checkbox.js +142 -155
  77. package/lib/esm/components/datatable/datatable-checkbox.js.map +1 -1
  78. package/lib/esm/components/datatable/datatable-column-utils.d.ts +30 -0
  79. package/lib/esm/components/datatable/datatable-column-utils.d.ts.map +1 -0
  80. package/lib/esm/components/datatable/datatable-column-utils.js +38 -0
  81. package/lib/esm/components/datatable/datatable-column-utils.js.map +1 -0
  82. package/lib/esm/components/datatable/datatable-contracts.d.ts +2 -4
  83. package/lib/esm/components/datatable/datatable-contracts.d.ts.map +1 -1
  84. package/lib/esm/components/datatable/datatable-defaults.d.ts +20 -0
  85. package/lib/esm/components/datatable/datatable-defaults.d.ts.map +1 -0
  86. package/lib/esm/components/datatable/datatable-defaults.js +190 -0
  87. package/lib/esm/components/datatable/datatable-defaults.js.map +1 -0
  88. package/lib/esm/components/datatable/datatable-layout-plugin.d.ts +7 -0
  89. package/lib/esm/components/datatable/datatable-layout-plugin.d.ts.map +1 -0
  90. package/lib/esm/components/datatable/datatable-layout-plugin.js +334 -0
  91. package/lib/esm/components/datatable/datatable-layout-plugin.js.map +1 -0
  92. package/lib/esm/components/datatable/datatable-local-provider.d.ts +2 -2
  93. package/lib/esm/components/datatable/datatable-local-provider.d.ts.map +1 -1
  94. package/lib/esm/components/datatable/datatable-local-provider.js +85 -27
  95. package/lib/esm/components/datatable/datatable-local-provider.js.map +1 -1
  96. package/lib/esm/components/datatable/datatable-pagination-renderer.d.ts.map +1 -1
  97. package/lib/esm/components/datatable/datatable-pagination-renderer.js +13 -13
  98. package/lib/esm/components/datatable/datatable-pagination-renderer.js.map +1 -1
  99. package/lib/esm/components/datatable/datatable-registry.d.ts +18 -0
  100. package/lib/esm/components/datatable/datatable-registry.d.ts.map +1 -0
  101. package/lib/esm/components/datatable/datatable-registry.js +63 -0
  102. package/lib/esm/components/datatable/datatable-registry.js.map +1 -0
  103. package/lib/esm/components/datatable/datatable-remote-provider.d.ts.map +1 -1
  104. package/lib/esm/components/datatable/datatable-remote-provider.js +1 -2
  105. package/lib/esm/components/datatable/datatable-remote-provider.js.map +1 -1
  106. package/lib/esm/components/datatable/datatable-search-handler.d.ts +10 -0
  107. package/lib/esm/components/datatable/datatable-search-handler.d.ts.map +1 -0
  108. package/lib/esm/components/datatable/datatable-search-handler.js +62 -0
  109. package/lib/esm/components/datatable/datatable-search-handler.js.map +1 -0
  110. package/lib/esm/components/datatable/datatable-sort.d.ts +31 -4
  111. package/lib/esm/components/datatable/datatable-sort.d.ts.map +1 -1
  112. package/lib/esm/components/datatable/datatable-sort.js +85 -57
  113. package/lib/esm/components/datatable/datatable-sort.js.map +1 -1
  114. package/lib/esm/components/datatable/datatable-spinner.d.ts +30 -0
  115. package/lib/esm/components/datatable/datatable-spinner.d.ts.map +1 -0
  116. package/lib/esm/components/datatable/datatable-spinner.js +51 -0
  117. package/lib/esm/components/datatable/datatable-spinner.js.map +1 -0
  118. package/lib/esm/components/datatable/datatable-state-persistence.d.ts +19 -0
  119. package/lib/esm/components/datatable/datatable-state-persistence.d.ts.map +1 -0
  120. package/lib/esm/components/datatable/datatable-state-persistence.js +55 -0
  121. package/lib/esm/components/datatable/datatable-state-persistence.js.map +1 -0
  122. package/lib/esm/components/datatable/datatable-table-renderer.d.ts +2 -0
  123. package/lib/esm/components/datatable/datatable-table-renderer.d.ts.map +1 -1
  124. package/lib/esm/components/datatable/datatable-table-renderer.js +75 -16
  125. package/lib/esm/components/datatable/datatable-table-renderer.js.map +1 -1
  126. package/lib/esm/components/datatable/datatable-utils.d.ts +10 -0
  127. package/lib/esm/components/datatable/datatable-utils.d.ts.map +1 -0
  128. package/lib/esm/components/datatable/datatable-utils.js +12 -0
  129. package/lib/esm/components/datatable/datatable-utils.js.map +1 -0
  130. package/lib/esm/components/datatable/datatable.d.ts +35 -34
  131. package/lib/esm/components/datatable/datatable.d.ts.map +1 -1
  132. package/lib/esm/components/datatable/datatable.js +235 -499
  133. package/lib/esm/components/datatable/datatable.js.map +1 -1
  134. package/lib/esm/components/datatable/index.d.ts +1 -1
  135. package/lib/esm/components/datatable/index.d.ts.map +1 -1
  136. package/lib/esm/components/datatable/types.d.ts +127 -11
  137. package/lib/esm/components/datatable/types.d.ts.map +1 -1
  138. package/lib/esm/index.d.ts +1 -1
  139. package/lib/esm/index.d.ts.map +1 -1
  140. package/lib/esm/index.js +6 -0
  141. package/lib/esm/index.js.map +1 -1
  142. package/package.json +5 -1
  143. package/skills/ktui/SKILL.md +711 -0
  144. package/skills/ktui-datatable/SKILL.md +302 -0
  145. package/skills/ktui-install/SKILL.md +150 -0
  146. package/skills/ktui-select/SKILL.md +271 -0
  147. package/src/components/__tests__/component.test.ts +347 -0
  148. package/src/components/collapse/collapse.css +2 -2
  149. package/src/components/datatable/__tests__/architecture-boundaries.test.ts +56 -8
  150. package/src/components/datatable/__tests__/currency-sort.test.ts +25 -28
  151. package/src/components/datatable/__tests__/datatable-checkbox.test.ts +527 -0
  152. package/src/components/datatable/__tests__/datatable-column-utils.test.ts +117 -0
  153. package/src/components/datatable/__tests__/datatable-defaults.test.ts +57 -0
  154. package/src/components/datatable/__tests__/datatable-finalize-extended.test.ts +361 -0
  155. package/src/components/datatable/__tests__/datatable-fixed-layout.test.ts +427 -0
  156. package/src/components/datatable/__tests__/datatable-improvements.test.ts +484 -0
  157. package/src/components/datatable/__tests__/datatable-pagination-extended.test.ts +508 -0
  158. package/src/components/datatable/__tests__/datatable-public-api.test.ts +269 -0
  159. package/src/components/datatable/__tests__/datatable-registry.test.ts +172 -0
  160. package/src/components/datatable/__tests__/datatable-remote-provider.test.ts +468 -0
  161. package/src/components/datatable/__tests__/datatable-search-handler.test.ts +124 -0
  162. package/src/components/datatable/__tests__/datatable-sort-extended.test.ts +417 -0
  163. package/src/components/datatable/__tests__/datatable-spinner.test.ts +95 -0
  164. package/src/components/datatable/__tests__/datatable-table-renderer-extended.test.ts +425 -0
  165. package/src/components/datatable/__tests__/datatable-types.test.ts +117 -0
  166. package/src/components/datatable/__tests__/datatable-utils.test.ts +52 -0
  167. package/src/components/datatable/__tests__/locked-layout.test.ts +257 -0
  168. package/src/components/datatable/__tests__/multi-row-headers.test.ts +7 -7
  169. package/src/components/datatable/__tests__/pagination-reset.test.ts +147 -6
  170. package/src/components/datatable/__tests__/race-conditions.test.ts +11 -11
  171. package/src/components/datatable/__tests__/setup.ts +12 -4
  172. package/src/components/datatable/datatable-checkbox.ts +139 -143
  173. package/src/components/datatable/datatable-column-utils.ts +63 -0
  174. package/src/components/datatable/datatable-contracts.ts +2 -3
  175. package/src/components/datatable/datatable-defaults.ts +204 -0
  176. package/src/components/datatable/datatable-layout-plugin.ts +459 -0
  177. package/src/components/datatable/datatable-local-provider.ts +106 -35
  178. package/src/components/datatable/datatable-pagination-renderer.ts +13 -15
  179. package/src/components/datatable/datatable-registry.ts +89 -0
  180. package/src/components/datatable/datatable-remote-provider.ts +1 -3
  181. package/src/components/datatable/datatable-search-handler.ts +97 -0
  182. package/src/components/datatable/datatable-sort.ts +111 -66
  183. package/src/components/datatable/datatable-spinner.ts +103 -0
  184. package/src/components/datatable/datatable-state-persistence.ts +67 -0
  185. package/src/components/datatable/datatable-table-renderer.ts +81 -18
  186. package/src/components/datatable/datatable-utils.ts +12 -0
  187. package/src/components/datatable/datatable.css +98 -0
  188. package/src/components/datatable/datatable.ts +288 -583
  189. package/src/components/datatable/index.ts +8 -0
  190. package/src/components/datatable/types.ts +157 -23
  191. package/src/helpers/__tests__/dom.test.ts +776 -0
  192. package/src/helpers/__tests__/utils.test.ts +332 -0
  193. package/src/index.ts +15 -0
  194. package/skills/ktui-components/SKILL.md +0 -41
  195. package/skills/ktui-theming/SKILL.md +0 -50
  196. package/src/components/datatable/datatable-event-adapter.ts +0 -21
@@ -11,15 +11,20 @@ import {
11
11
  KTDataTableSortOrderInterface,
12
12
  KTDataTableStateInterface,
13
13
  KTDataTableColumnFilterInterface,
14
+ KTDataTableLayoutPluginContextInterface,
15
+ KTDataTableLayoutPluginInterface,
16
+ OriginalTableClasses,
14
17
  } from './types';
15
18
  import { KTOptionType } from '../../types';
16
- import KTComponents from '../../index';
17
19
  import KTData from '../../helpers/data';
18
20
  import {
19
- createCheckboxHandler,
21
+ KTDataTableCheckboxHandler,
20
22
  KTDataTableCheckboxAPI,
21
23
  } from './datatable-checkbox';
22
- import { createSortHandler, KTDataTableSortAPI } from './datatable-sort';
24
+ import { KTDataTableSortHandler, KTDataTableSortAPI } from './datatable-sort';
25
+ import { createStickyLayoutPlugin } from './datatable-layout-plugin';
26
+ import { DATATABLE_DEFAULTS, DEFAULT_PAGE_SIZES, DEFAULT_SEARCH_DELAY } from './datatable-defaults';
27
+ import { getLogicalColumnCount } from './datatable-column-utils';
23
28
  import {
24
29
  KTDataTableCleanup,
25
30
  KTDataTableEventAdapter,
@@ -27,12 +32,20 @@ import {
27
32
  KTDataTableStateStore,
28
33
  KTDataTableTableRenderer,
29
34
  } from './datatable-contracts';
30
- import { createDataTableEventAdapter } from './datatable-event-adapter';
31
35
  import { KTDataTableLocalDataProvider } from './datatable-local-provider';
32
36
  import { KTDataTableRemoteDataProvider } from './datatable-remote-provider';
33
37
  import { KTDataTableConfigStateStore } from './datatable-state-store';
34
38
  import { KTDataTableDomPaginationRenderer } from './datatable-pagination-renderer';
35
39
  import { KTDataTableDomTableRenderer } from './datatable-table-renderer';
40
+ import KTUtils from '../../helpers/utils';
41
+ import { createSearchHandler } from './datatable-search-handler';
42
+ import {
43
+ createStatePersistence,
44
+ resolveTableNamespace,
45
+ } from './datatable-state-persistence';
46
+ import { createSpinner } from './datatable-spinner';
47
+ import { createDataTableRegistry } from './datatable-registry';
48
+ import { stripHtml } from './datatable-utils';
36
49
 
37
50
  /**
38
51
  * Custom DataTable plugin class with server-side API, pagination, and sorting
@@ -43,24 +56,14 @@ import { KTDataTableDomTableRenderer } from './datatable-table-renderer';
43
56
  * @param {HTMLElement} element The table element
44
57
  * @param {KTDataTableConfigInterface} [config] Additional configuration options
45
58
  */
59
+ const datatableRegistry = createDataTableRegistry<
60
+ KTDataTable<KTDataTableDataInterface>
61
+ >();
62
+
46
63
  export class KTDataTable<T extends KTDataTableDataInterface>
47
64
  extends KTComponent
48
65
  implements KTDataTableInterface
49
66
  {
50
- private static asElementWithInstance(element: HTMLElement): HTMLElement & {
51
- instance?: KTDataTable<KTDataTableDataInterface>;
52
- } {
53
- return element as HTMLElement & {
54
- instance?: KTDataTable<KTDataTableDataInterface>;
55
- };
56
- }
57
-
58
- private static asSearchElementWithDebounce(
59
- element: HTMLInputElement,
60
- ): HTMLInputElement & { _debouncedSearch?: EventListener } {
61
- return element as HTMLInputElement & { _debouncedSearch?: EventListener };
62
- }
63
-
64
67
  protected override _name: string = 'datatable';
65
68
  protected override _config: KTDataTableConfigInterface;
66
69
  protected override _defaultConfig: KTDataTableConfigInterface;
@@ -68,11 +71,13 @@ export class KTDataTable<T extends KTDataTableDataInterface>
68
71
  private _tableElement: HTMLTableElement;
69
72
  private _tbodyElement: HTMLTableSectionElement;
70
73
  private _theadElement: HTMLTableSectionElement;
71
- private _originalTbodyClass: string = ''; // Store original tbody class
72
- private _originalTrClasses: string[] = []; // Store original tr classes
73
- private _originalTheadClass: string = ''; // Store original thead class
74
- private _originalTdClasses: string[][] = []; // Store original td classes as a 2D array [row][col]
75
- private _originalThClasses: string[] = []; // Store original th classes
74
+ private _originalClasses: OriginalTableClasses = {
75
+ tbody: '',
76
+ thead: '',
77
+ tr: [],
78
+ td: [],
79
+ th: [],
80
+ };
76
81
 
77
82
  private _infoElement: HTMLElement | null = null;
78
83
  private _sizeElement: HTMLSelectElement | null = null;
@@ -80,6 +85,7 @@ export class KTDataTable<T extends KTDataTableDataInterface>
80
85
 
81
86
  private _checkbox: KTDataTableCheckboxAPI;
82
87
  private _sortHandler: KTDataTableSortAPI<T>;
88
+ private _layoutPlugin: KTDataTableLayoutPluginInterface | null = null;
83
89
  private _eventAdapter: KTDataTableEventAdapter;
84
90
  private _stateStore: KTDataTableStateStore;
85
91
  private _localProvider: KTDataTableLocalDataProvider<T>;
@@ -88,6 +94,10 @@ export class KTDataTable<T extends KTDataTableDataInterface>
88
94
  private _paginationRenderer: KTDataTablePaginationRenderer;
89
95
  private _cleanupCallbacks: KTDataTableCleanup[] = [];
90
96
 
97
+ private _searchHandler = createSearchHandler();
98
+ private _statePersistence = createStatePersistence();
99
+ private _spinner = createSpinner();
100
+
91
101
  private _data: T[] = [];
92
102
  private _isFetching: boolean = false;
93
103
 
@@ -95,10 +105,10 @@ export class KTDataTable<T extends KTDataTableDataInterface>
95
105
  super();
96
106
 
97
107
  if (KTData.has(element as HTMLElement, this._name)) {
98
- // Already initialized (e.g. by createInstances). Merge user config so columns/sortType etc. apply.
108
+ // Already initialized (e.g. by createInstances). Merge demo config and redraw once.
99
109
  const existing = KTDataTable.getInstance(element as HTMLElement);
100
110
  if (existing && config) {
101
- existing._mergeConfig(config);
111
+ existing._applyRuntimeConfig(config);
102
112
  }
103
113
  return;
104
114
  }
@@ -109,43 +119,54 @@ export class KTDataTable<T extends KTDataTableDataInterface>
109
119
  if (!this._element) {
110
120
  return;
111
121
  }
122
+ if (!this._element.hasAttribute('data-kt-datatable')) {
123
+ this._element.setAttribute('data-kt-datatable', 'true');
124
+ }
112
125
  this._buildConfig();
126
+ this._normalizePageSizeConfig();
113
127
  this._stateStore = new KTDataTableConfigStateStore(this._config);
114
- this._eventAdapter = createDataTableEventAdapter(
115
- this._fireEvent.bind(this),
116
- this._dispatchEvent.bind(this),
117
- );
128
+ this._eventAdapter = {
129
+ emit: (eventName: string, eventData?: object) => {
130
+ this._emit(eventName, eventData);
131
+ },
132
+ };
118
133
 
119
134
  // Store the instance directly on the element
120
- KTDataTable.asElementWithInstance(element).instance = this;
135
+ datatableRegistry.register(element, this);
121
136
 
122
137
  this._initElements();
138
+ this._layoutPlugin = this._createLayoutPlugin();
123
139
  this._tableRenderer = new KTDataTableDomTableRenderer<T>();
124
140
  this._paginationRenderer = new KTDataTableDomPaginationRenderer();
125
141
  this._initDataProviders();
126
142
 
127
143
  // Initialize checkbox handler
128
- this._checkbox = createCheckboxHandler(
144
+ this._checkbox = new KTDataTableCheckboxHandler(
129
145
  this._element,
130
146
  this._config,
131
147
  this._emit.bind(this),
148
+ {
149
+ getState: () => this._stateStore.getState(),
150
+ setSelectedRows: (rows) => {
151
+ this._stateStore.patchState({ selectedRows: rows });
152
+ },
153
+ },
132
154
  );
133
155
 
134
156
  // Initialize sort handler
135
- this._sortHandler = createSortHandler(
136
- this._config,
137
- this._theadElement,
138
- () => ({
157
+ this._sortHandler = new KTDataTableSortHandler({
158
+ config: this._config,
159
+ theadElement: this._theadElement,
160
+ getState: () => ({
139
161
  sortField: this.getState().sortField,
140
162
  sortOrder: this.getState().sortOrder,
141
163
  }),
142
- (field, order) => {
164
+ setState: (field, order) => {
143
165
  this._stateStore.setSort(field as never, order);
144
166
  },
145
- this._fireEvent.bind(this),
146
- this._dispatchEvent.bind(this),
147
- this._updateData.bind(this),
148
- );
167
+ emit: this._emit.bind(this),
168
+ updateData: this._updateData.bind(this),
169
+ });
149
170
 
150
171
  this._sortHandler.initSort();
151
172
 
@@ -155,15 +176,15 @@ export class KTDataTable<T extends KTDataTableDataInterface>
155
176
 
156
177
  if (this._config.stateSave) {
157
178
  this._loadState();
179
+ this._normalizePageState();
158
180
  }
159
181
 
160
182
  this._updateData();
161
-
162
- this._emit('init');
163
183
  }
164
184
 
165
185
  private _emit(eventName: string, eventData?: object): void {
166
- this._eventAdapter.emit(eventName, eventData);
186
+ this._fireEvent(eventName, eventData);
187
+ this._dispatchEvent(`kt.datatable.${eventName}`, eventData);
167
188
  }
168
189
 
169
190
  private _initDataProviders(): void {
@@ -188,122 +209,111 @@ export class KTDataTable<T extends KTDataTableDataInterface>
188
209
  });
189
210
  }
190
211
 
212
+ private _createLayoutPlugin(): KTDataTableLayoutPluginInterface | null {
213
+ if (this._config.layoutPlugin) {
214
+ return this._config.layoutPlugin;
215
+ }
216
+
217
+ if (this._config.lockedLayout) {
218
+ return createStickyLayoutPlugin();
219
+ }
220
+
221
+ return null;
222
+ }
223
+
224
+ /**
225
+ * Apply config from a late constructor call (e.g. docs demo script after auto-init).
226
+ */
227
+ private _applyRuntimeConfig(config: KTDataTableConfigInterface): void {
228
+ this._mergeConfig(config);
229
+ this._normalizePageSizeConfig();
230
+ this._layoutPlugin = this._createLayoutPlugin();
231
+ this.reload();
232
+ }
233
+
234
+ private _normalizePageSizeConfig(): void {
235
+ const configuredPageSizes = Array.isArray(this._config.pageSizes)
236
+ ? this._config.pageSizes
237
+ : [];
238
+ const pageSizes = configuredPageSizes
239
+ .map((size) => Number(size))
240
+ .filter((size) => Number.isFinite(size) && size > 0)
241
+ .map((size) => Math.floor(size));
242
+ const fallbackPageSizes: number[] = [...DEFAULT_PAGE_SIZES];
243
+ this._config.pageSizes =
244
+ pageSizes.length > 0 ? Array.from(new Set(pageSizes)) : fallbackPageSizes;
245
+
246
+ const configuredPageSize = Number(this._config.pageSize);
247
+ this._config.pageSize =
248
+ Number.isFinite(configuredPageSize) && configuredPageSize > 0
249
+ ? Math.floor(configuredPageSize)
250
+ : this._config.pageSizes[0];
251
+ }
252
+
253
+ private _normalizePageState(): void {
254
+ const statePageSize = Number(this._config._state.pageSize);
255
+ this._config._state.pageSize =
256
+ Number.isFinite(statePageSize) && statePageSize > 0
257
+ ? Math.floor(statePageSize)
258
+ : this._config.pageSize;
259
+
260
+ const statePage = Number(this._config._state.page);
261
+ this._config._state.page =
262
+ Number.isFinite(statePage) && statePage > 0 ? Math.floor(statePage) : 1;
263
+ }
264
+
265
+ private _getLayoutPluginContext(): KTDataTableLayoutPluginContextInterface {
266
+ return {
267
+ rootElement: this._element,
268
+ tableElement: this._tableElement,
269
+ theadElement: this._theadElement,
270
+ tbodyElement: this._tbodyElement,
271
+ config: this._config,
272
+ };
273
+ }
274
+
191
275
  /**
192
276
  * Initialize default configuration for the datatable
193
277
  * @param config User-provided configuration options
194
278
  * @returns Default configuration merged with user-provided options
195
279
  */
280
+ private _createDefaultSearchCallback(): (
281
+ data: KTDataTableDataInterface[],
282
+ search: string,
283
+ ) => KTDataTableDataInterface[] {
284
+ return ((data: T[], search: string): T[] => {
285
+ if (!data || !search) {
286
+ return [];
287
+ }
288
+ const searchLower = search.toLowerCase();
289
+ return data.filter((item: T) => {
290
+ if (!item) {
291
+ return false;
292
+ }
293
+ return Object.values(item).some((value: KTOptionType) => {
294
+ if (
295
+ typeof value !== 'string' &&
296
+ typeof value !== 'number' &&
297
+ typeof value !== 'boolean'
298
+ ) {
299
+ return false;
300
+ }
301
+ const valueText = stripHtml(value).toLowerCase();
302
+ return valueText.includes(searchLower);
303
+ });
304
+ });
305
+ }) as unknown as (data: KTDataTableDataInterface[], search: string) => KTDataTableDataInterface[];
306
+ }
307
+
196
308
  private _initDefaultConfig(
197
309
  config?: KTDataTableConfigInterface,
198
310
  ): KTDataTableConfigInterface {
199
311
  return {
200
- /**
201
- * HTTP method for server-side API call
202
- */
203
- requestMethod: 'GET',
204
- /**
205
- * Custom HTTP headers for the API request
206
- */
207
- requestHeaders: {
208
- 'Content-Type': 'application/x-www-form-urlencoded',
209
- },
210
- /**
211
- * Pagination info template
212
- */
213
- info: '{start}-{end} of {total}',
214
- /**
215
- * Info text when there is no data
216
- */
217
- infoEmpty: 'No records found',
218
- /**
219
- * Available page sizes
220
- */
221
- pageSizes: [5, 10, 20, 30, 50],
222
- /**
223
- * Default page size
224
- */
225
- pageSize: 10,
226
- /**
227
- * Enable or disable pagination more button
228
- */
229
- pageMore: true,
230
- /**
231
- * Maximum number of pages before enabling pagination more button
232
- */
233
- pageMoreLimit: 3,
234
- /**
235
- * Pagination button templates
236
- */
237
- pagination: {
238
- number: {
239
- /**
240
- * CSS classes to be added to the pagination button
241
- */
242
- class: 'kt-datatable-pagination-button',
243
- /**
244
- * Text to be displayed in the pagination button
245
- */
246
- text: '{page}',
247
- },
248
- previous: {
249
- /**
250
- * CSS classes to be added to the previous pagination button
251
- */
252
- class: 'kt-datatable-pagination-button kt-datatable-pagination-prev',
253
- /**
254
- * Text to be displayed in the previous pagination button
255
- */
256
- text: `
257
- <svg class="rtl:transform rtl:rotate-180 size-3.5 shrink-0" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
258
- <path d="M8.86501 16.7882V12.8481H21.1459C21.3724 12.8481 21.5897 12.7581 21.7498 12.5979C21.91 12.4378 22 12.2205 22 11.994C22 11.7675 21.91 11.5503 21.7498 11.3901C21.5897 11.2299 21.3724 11.1399 21.1459 11.1399H8.86501V7.2112C8.86628 7.10375 8.83517 6.9984 8.77573 6.90887C8.7163 6.81934 8.63129 6.74978 8.53177 6.70923C8.43225 6.66869 8.32283 6.65904 8.21775 6.68155C8.11267 6.70405 8.0168 6.75766 7.94262 6.83541L2.15981 11.6182C2.1092 11.668 2.06901 11.7274 2.04157 11.7929C2.01413 11.8584 2 11.9287 2 11.9997C2 12.0707 2.01413 12.141 2.04157 12.2065C2.06901 12.272 2.1092 12.3314 2.15981 12.3812L7.94262 17.164C8.0168 17.2417 8.11267 17.2953 8.21775 17.3178C8.32283 17.3403 8.43225 17.3307 8.53177 17.2902C8.63129 17.2496 8.7163 17.18 8.77573 17.0905C8.83517 17.001 8.86628 16.8956 8.86501 16.7882Z" fill="currentColor"/>
259
- </svg>
260
- `,
261
- },
262
- next: {
263
- /**
264
- * CSS classes to be added to the next pagination button
265
- */
266
- class: 'kt-datatable-pagination-button kt-datatable-pagination-next',
267
- /**
268
- * Text to be displayed in the next pagination button
269
- */
270
- text: `
271
- <svg class="rtl:transform rtl:rotate-180 size-3.5 shrink-0" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
272
- <path d="M15.135 7.21144V11.1516H2.85407C2.62756 11.1516 2.41032 11.2415 2.25015 11.4017C2.08998 11.5619 2 11.7791 2 12.0056C2 12.2321 2.08998 12.4494 2.25015 12.6096C2.41032 12.7697 2.62756 12.8597 2.85407 12.8597H15.135V16.7884C15.1337 16.8959 15.1648 17.0012 15.2243 17.0908C15.2837 17.1803 15.3687 17.2499 15.4682 17.2904C15.5677 17.3309 15.6772 17.3406 15.7822 17.3181C15.8873 17.2956 15.9832 17.242 16.0574 17.1642L21.8402 12.3814C21.8908 12.3316 21.931 12.2722 21.9584 12.2067C21.9859 12.1412 22 12.0709 22 11.9999C22 11.9289 21.9859 11.8586 21.9584 11.7931C21.931 11.7276 21.8908 11.6683 21.8402 11.6185L16.0574 6.83565C15.9832 6.75791 15.8873 6.70429 15.7822 6.68179C15.6772 6.65929 15.5677 6.66893 15.4682 6.70948C15.3687 6.75002 15.2837 6.81959 15.2243 6.90911C15.1648 6.99864 15.1337 7.10399 15.135 7.21144Z" fill="currentColor"/>
273
- </svg>
274
- `,
275
- },
276
- more: {
277
- /**
278
- * CSS classes to be added to the pagination more button
279
- */
280
- class: 'kt-datatable-pagination-button kt-datatable-pagination-more',
281
- /**
282
- * Text to be displayed in the pagination more button
283
- */
284
- text: '...',
285
- },
286
- },
287
- /**
288
- * Sorting options
289
- */
312
+ ...DATATABLE_DEFAULTS,
313
+ // Per-instance state; DATATABLE_DEFAULTS._state is a shared singleton.
314
+ _state: {} as KTDataTableStateInterface,
290
315
  sort: {
291
- /**
292
- * CSS classes to be added to the sortable headers
293
- */
294
- classes: {
295
- base: 'kt-table-col',
296
- asc: 'asc',
297
- desc: 'desc',
298
- },
299
- /**
300
- * Local sorting callback function
301
- * Sorts the data array based on the sort field and order
302
- * @param data Data array to be sorted
303
- * @param sortField Property name of the data object to be sorted by
304
- * @param sortOrder Sorting order (ascending or descending)
305
- * @returns Sorted data array
306
- */
316
+ ...DATATABLE_DEFAULTS.sort,
307
317
  callback: (
308
318
  data: T[],
309
319
  sortField: keyof T | number,
@@ -315,110 +325,9 @@ export class KTDataTable<T extends KTDataTableDataInterface>
315
325
  },
316
326
  },
317
327
  search: {
318
- /**
319
- * Delay in milliseconds before the search function is applied to the data array
320
- * @default 500
321
- */
322
- delay: 500, // ms
323
- /**
324
- * Local search callback function
325
- * Filters the data array based on the search string
326
- * @param data Data array to be filtered
327
- * @param search Search string used to filter the data array
328
- * @returns Filtered data array
329
- */
330
- callback: (data: T[], search: string): T[] => {
331
- if (!data || !search) {
332
- return [];
333
- }
334
-
335
- return data.filter((item: T) => {
336
- if (!item) {
337
- return false;
338
- }
339
-
340
- return Object.values(item).some((value: KTOptionType) => {
341
- if (
342
- typeof value !== 'string' &&
343
- typeof value !== 'number' &&
344
- typeof value !== 'boolean'
345
- ) {
346
- return false;
347
- }
348
-
349
- const valueText = String(value)
350
- .replace(/<|>|&nbsp;/g, '')
351
- .toLowerCase();
352
- return valueText.includes(search.toLowerCase());
353
- });
354
- });
355
- },
356
- },
357
- /**
358
- * Loading spinner options
359
- */
360
- loading: {
361
- /**
362
- * Template to be displayed during data fetching process
363
- */
364
- template: `
365
- <div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
366
- <div class="kt-datatable-loading">
367
- <svg class="animate-spin -ml-1 h-5 w-5 text-gray-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
368
- <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3"></circle>
369
- <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
370
- </svg>
371
- {content}
372
- </div>
373
- </div>
374
- `,
375
- /**
376
- * Loading text to be displayed in the template
377
- */
378
- content: 'Loading...',
379
- },
380
- /**
381
- * Selectors of the elements to be targeted
382
- */
383
- attributes: {
384
- /**
385
- * Data table element
386
- */
387
- table: 'table[data-kt-datatable-table="true"]',
388
- /**
389
- * Pagination info element
390
- */
391
- info: '[data-kt-datatable-info="true"]',
392
- /**
393
- * Page size dropdown element
394
- */
395
- size: '[data-kt-datatable-size="true"]',
396
- /**
397
- * Pagination element
398
- */
399
- pagination: '[data-kt-datatable-pagination="true"]',
400
- /**
401
- * Spinner element
402
- */
403
- spinner: '[data-kt-datatable-spinner="true"]',
404
- /**
405
- * Checkbox element
406
- */
407
- check: '[data-kt-datatable-check="true"]',
408
- checkbox: '[data-kt-datatable-row-check="true"]',
409
- },
410
- /**
411
- * Enable or disable state saving
412
- */
413
- stateSave: true,
414
- checkbox: {
415
- checkedClass: 'checked',
328
+ ...DATATABLE_DEFAULTS.search,
329
+ callback: this._createDefaultSearchCallback(),
416
330
  },
417
- /**
418
- * Private properties
419
- */
420
- _state: {} as KTDataTableStateInterface,
421
- loadingClass: 'loading',
422
331
  ...config,
423
332
  } as KTDataTableConfigInterface;
424
333
  }
@@ -431,7 +340,9 @@ export class KTDataTable<T extends KTDataTableDataInterface>
431
340
  const root = this._element;
432
341
  const attrs = this._config.attributes;
433
342
  if (!root || !attrs?.table) {
434
- throw new Error('KTDataTable: root element and table selector are required');
343
+ throw new Error(
344
+ 'KTDataTable: root element and table selector are required',
345
+ );
435
346
  }
436
347
 
437
348
  const tableEl = root.querySelector<HTMLTableElement>(attrs.table);
@@ -466,17 +377,17 @@ export class KTDataTable<T extends KTDataTableDataInterface>
466
377
  private _storeOriginalClasses(): void {
467
378
  // Store tbody class
468
379
  if (this._tbodyElement) {
469
- this._originalTbodyClass = this._tbodyElement.className || '';
380
+ this._originalClasses.tbody = this._tbodyElement.className || '';
470
381
  }
471
382
 
472
383
  // Store thead class and th classes
473
384
  if (this._theadElement) {
474
- this._originalTheadClass = this._theadElement.className || '';
385
+ this._originalClasses.thead = this._theadElement.className || '';
475
386
 
476
387
  // Store th classes
477
388
  const thElements =
478
389
  this._theadElement.querySelectorAll<HTMLTableCellElement>('th');
479
- this._originalThClasses = Array.from(thElements).map(
390
+ this._originalClasses.th = Array.from(thElements).map(
480
391
  (th) => th.className || '',
481
392
  );
482
393
  }
@@ -485,15 +396,15 @@ export class KTDataTable<T extends KTDataTableDataInterface>
485
396
  if (this._tbodyElement) {
486
397
  const originalRows =
487
398
  this._tbodyElement.querySelectorAll<HTMLTableRowElement>('tr');
488
- this._originalTrClasses = Array.from(originalRows).map(
399
+ this._originalClasses.tr = Array.from(originalRows).map(
489
400
  (row) => row.className || '',
490
401
  );
491
402
 
492
403
  // Store td classes as a 2D array
493
- this._originalTdClasses = [];
404
+ this._originalClasses.td = [];
494
405
  Array.from(originalRows).forEach((row, rowIndex) => {
495
406
  const tdElements = row.querySelectorAll<HTMLTableCellElement>('td');
496
- this._originalTdClasses[rowIndex] = Array.from(tdElements).map(
407
+ this._originalClasses.td[rowIndex] = Array.from(tdElements).map(
497
408
  (td) => td.className || '',
498
409
  );
499
410
  });
@@ -508,9 +419,8 @@ export class KTDataTable<T extends KTDataTableDataInterface>
508
419
  if (this._isFetching) return; // Prevent duplicate fetches
509
420
  this._isFetching = true;
510
421
  try {
511
- this._showSpinner(); // Show spinner before fetching data
422
+ this._spinner.show(this._element, this._config, this._tableElement); // Show spinner before fetching data
512
423
 
513
- this._emit('fetch');
514
424
  const result =
515
425
  typeof this._config.apiEndpoint === 'undefined'
516
426
  ? this._localProvider.fetchSync()
@@ -520,10 +430,11 @@ export class KTDataTable<T extends KTDataTableDataInterface>
520
430
  this._data = result.data;
521
431
  this._stateStore.patchState({ totalItems: result.totalItems });
522
432
  await this._draw();
523
- this._emit('fetched');
524
433
  }
525
434
 
526
435
  await this._finalize();
436
+
437
+ this._emit('update');
527
438
  } finally {
528
439
  // Finally block now correctly executes after promises resolve, not immediately
529
440
  this._isFetching = false;
@@ -545,63 +456,29 @@ export class KTDataTable<T extends KTDataTableDataInterface>
545
456
  this._sortHandler.initSort();
546
457
  }
547
458
 
548
- this._attachSearchEvent();
549
-
550
- if (typeof KTComponents !== 'undefined') {
551
- KTComponents.init();
552
- }
459
+ this._searchHandler.attach(
460
+ this._tableId(),
461
+ this.getState().search,
462
+ this._config.search?.delay ?? DEFAULT_SEARCH_DELAY,
463
+ (query) => this.search(query),
464
+ );
553
465
 
554
466
  /**
555
467
  * Hide spinner
556
468
  */
557
- this._hideSpinner();
558
- }
559
-
560
- /**
561
- * Attach search event to the search input element
562
- * @returns {void}
563
- */
564
- private _attachSearchEvent(): void {
565
- const tableId: string = this._tableId();
566
- const searchElement: HTMLInputElement | null =
567
- document.querySelector<HTMLInputElement>(
568
- `[data-kt-datatable-search="#${tableId}"]`,
569
- );
570
-
571
- // Get search state
572
- const { search } = this.getState();
573
- // Set search value
574
- if (searchElement) {
575
- searchElement.value =
576
- search === undefined || search === null
577
- ? ''
578
- : typeof search === 'string'
579
- ? search
580
- : String(search);
581
- }
582
-
583
- if (searchElement) {
584
- // Check if a debounced search function already exists
585
- const searchWithDebounce =
586
- KTDataTable.asSearchElementWithDebounce(searchElement);
587
- if (searchWithDebounce._debouncedSearch) {
588
- // Remove the existing debounced event listener
589
- searchElement.removeEventListener(
590
- 'keyup',
591
- searchWithDebounce._debouncedSearch,
592
- );
593
- }
594
-
595
- // Create a new debounced search function
596
- const debouncedSearch = this._debounce(() => {
597
- this.search(searchElement.value);
598
- }, this._config.search?.delay ?? 500);
599
-
600
- // Store the new debounced function as a property of the element
601
- searchWithDebounce._debouncedSearch = debouncedSearch;
602
-
603
- // Add the new debounced event listener
604
- searchElement.addEventListener('keyup', debouncedSearch);
469
+ this._spinner.hide(this._element, this._config);
470
+
471
+ // Update content checksum AFTER all DOM modifications (checkbox init
472
+ // adds checked-class to <tr> elements which changes tbody innerHTML).
473
+ // If we save the checksum earlier (in _draw), the next fetchSync()
474
+ // sees a mismatch, re-extracts from the DOM, and loses rows that
475
+ // were on other pages — making pagination show empty.
476
+ if (!this._config.apiEndpoint) {
477
+ this._stateStore.patchState({
478
+ _contentChecksum: KTUtils.checksum(
479
+ JSON.stringify(this._tbodyElement.innerHTML),
480
+ ),
481
+ });
605
482
  }
606
483
  }
607
484
 
@@ -611,18 +488,11 @@ export class KTDataTable<T extends KTDataTableDataInterface>
611
488
  * @returns {number} Number of data columns, or 0 if unknown
612
489
  */
613
490
  private _getLogicalColumnCount(): number {
614
- const { originalData } = this.getState();
615
- if (originalData && originalData.length > 0) {
616
- return Object.keys(originalData[0]).length;
617
- }
618
- if (this._tbodyElement) {
619
- const firstRow =
620
- this._tbodyElement.querySelector<HTMLTableRowElement>('tr');
621
- if (firstRow) {
622
- return firstRow.querySelectorAll<HTMLTableCellElement>('td').length;
623
- }
624
- }
625
- return 0;
491
+ return getLogicalColumnCount(
492
+ this._theadElement,
493
+ this._tbodyElement,
494
+ this.getState().originalData as Array<Record<string, unknown>> | undefined,
495
+ );
626
496
  }
627
497
 
628
498
  /**
@@ -698,14 +568,22 @@ export class KTDataTable<T extends KTDataTableDataInterface>
698
568
  * @returns {Promise<void>} A promise that resolves when the table and pagination controls are updated
699
569
  */
700
570
  private async _draw(): Promise<void> {
701
- this._stateStore.patchState({
702
- totalPages:
703
- Math.ceil(this.getState().totalItems / this.getState().pageSize) || 0,
704
- });
571
+ const normalizedPageSize = Math.max(
572
+ 1,
573
+ Number(this.getState().pageSize) || Number(this._config.pageSize) || 1,
574
+ );
575
+ const totalPages =
576
+ Math.ceil(this.getState().totalItems / normalizedPageSize) || 0;
577
+ const page =
578
+ totalPages > 0
579
+ ? Math.min(Math.max(1, this.getState().page), totalPages)
580
+ : 1;
705
581
 
706
- this._emit('draw');
582
+ this._stateStore.patchState({ totalPages, page });
707
583
 
708
- this._dispose();
584
+ this._layoutPlugin?.beforeDraw?.(this._getLayoutPluginContext());
585
+
586
+ this._cleanupForRedraw();
709
587
 
710
588
  // Update the table and pagination controls
711
589
  if (this._theadElement && this._tbodyElement) {
@@ -716,7 +594,7 @@ export class KTDataTable<T extends KTDataTableDataInterface>
716
594
  this._updatePagination();
717
595
  }
718
596
 
719
- this._emit('drew');
597
+ this._layoutPlugin?.afterDraw?.(this._getLayoutPluginContext());
720
598
 
721
599
  // Spinner is hidden in _finalize() to ensure it stays visible until the entire request completes
722
600
  // Removed duplicate _hideSpinner() call here to prevent premature hiding
@@ -731,18 +609,17 @@ export class KTDataTable<T extends KTDataTableDataInterface>
731
609
  * @returns {HTMLTableSectionElement} The new table body element
732
610
  */
733
611
  private _updateTable(): HTMLTableSectionElement {
734
- return this._tableRenderer.render({
612
+ this._tbodyElement = this._tableRenderer.render({
735
613
  config: this._config,
736
614
  context: this,
737
615
  data: this._data,
738
616
  getLogicalColumnCount: this._getLogicalColumnCount.bind(this),
739
617
  getState: this.getState.bind(this),
740
- originalTbodyClass: this._originalTbodyClass,
741
- originalTrClasses: this._originalTrClasses,
742
- originalTdClasses: this._originalTdClasses,
618
+ originalClasses: this._originalClasses,
743
619
  tableElement: this._tableElement,
744
620
  theadElement: this._theadElement,
745
621
  });
622
+ return this._tbodyElement;
746
623
  }
747
624
 
748
625
  /**
@@ -797,81 +674,21 @@ export class KTDataTable<T extends KTDataTableDataInterface>
797
674
  return;
798
675
  }
799
676
 
800
- this._emit('pagination', { page: page });
801
-
802
677
  if (page >= 1 && page <= this.getState().totalPages) {
803
678
  this._stateStore.setPage(page);
804
679
  this._updateData();
805
680
  }
806
681
  }
807
682
 
808
- // Method to show the loading spinner
809
- private _showSpinner(): void {
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();
817
- if (spinner) {
818
- spinner.style.display = 'block';
819
- }
820
- root?.classList.add(this._config.loadingClass ?? 'loading');
821
- }
822
-
823
- // Method to hide the loading spinner
824
- private _hideSpinner(): void {
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;
831
- if (spinner) {
832
- spinner.style.display = 'none';
833
- }
834
- root?.classList.remove(this._config.loadingClass ?? 'loading');
835
- }
836
-
837
- // Method to create a spinner element if it doesn't exist
838
- private _createSpinner(): HTMLElement | null {
839
- const loading = this._config.loading;
840
- if (!loading) {
841
- return null;
842
- }
843
-
844
- const template = document.createElement('template');
845
- template.innerHTML = loading.template
846
- .trim()
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;
853
- spinner.setAttribute('data-kt-datatable-spinner', 'true');
854
-
855
- this._tableElement.appendChild(spinner);
856
-
857
- return spinner;
858
- }
859
-
860
683
  /**
861
684
  * Saves the current state of the table to local storage.
862
685
  * @returns {void}
863
686
  */
864
687
  private _saveState(): void {
865
- this._emit('stateSave');
866
-
867
- const ns: string = this._tableNamespace();
868
-
869
- if (ns) {
870
- localStorage.setItem(
871
- ns,
872
- JSON.stringify(this.getState() as KTDataTableStateInterface),
873
- );
874
- }
688
+ this._statePersistence.save(
689
+ this._tableNamespace(),
690
+ this.getState() as KTDataTableStateInterface,
691
+ );
875
692
  }
876
693
 
877
694
  /**
@@ -879,24 +696,16 @@ export class KTDataTable<T extends KTDataTableDataInterface>
879
696
  * @returns {Object} The saved state of the table, or null if no saved state exists.
880
697
  */
881
698
  private _loadState(): KTDataTableStateInterface | null {
882
- const stateString = localStorage.getItem(this._tableNamespace());
883
- if (!stateString) return null;
884
-
885
- try {
886
- const state = JSON.parse(stateString) as KTDataTableStateInterface;
887
- if (state) this._stateStore.replaceState(state);
888
- return state;
889
- } catch {}
890
-
891
- return null;
699
+ const ns = this._tableNamespace();
700
+ const saved = this._statePersistence.load(ns);
701
+ if (saved) this._stateStore.replaceState(saved);
702
+ return saved;
892
703
  }
893
704
 
894
705
  private _deleteState(): void {
895
- const ns = this._tableNamespace();
896
-
897
- if (ns) {
898
- localStorage.removeItem(ns);
899
- }
706
+ this._statePersistence.remove(
707
+ this._tableNamespace(),
708
+ );
900
709
  }
901
710
 
902
711
  /**
@@ -908,13 +717,12 @@ export class KTDataTable<T extends KTDataTableDataInterface>
908
717
  * @returns {string} The namespace for the table's state.
909
718
  */
910
719
  private _tableNamespace(): string {
911
- // Use the specified namespace, if one is given
912
- if (this._config.stateNamespace) {
913
- return this._config.stateNamespace;
914
- }
915
-
916
- // Fallback to the component's UID
917
- return this._tableId() ?? this._name;
720
+ return resolveTableNamespace(
721
+ this._config,
722
+ this._tableElement,
723
+ this._element,
724
+ this._name,
725
+ );
918
726
  }
919
727
 
920
728
  private _tableId(): string {
@@ -933,106 +741,51 @@ export class KTDataTable<T extends KTDataTableDataInterface>
933
741
  * Clean up all event listeners, handlers, and DOM nodes created by this instance.
934
742
  * This method is called before re-rendering or when disposing the component.
935
743
  */
936
- private _dispose() {
937
- const root = this._element;
938
- if (!root) {
744
+ /**
745
+ * Clean up event listeners and DOM artifacts for a redraw cycle.
746
+ * Does NOT remove the instance from the registry — the datatable
747
+ * remains accessible via getInstance() during the redraw window.
748
+ */
749
+ private _cleanupForRedraw(): void {
750
+ this._layoutPlugin?.dispose?.(this._getLayoutPluginContext());
751
+
752
+ if (!this._element) {
939
753
  return;
940
754
  }
941
755
 
942
756
  this._cleanupCallbacks.forEach((cleanup) => cleanup());
943
757
  this._cleanupCallbacks = [];
944
758
 
945
- // --- 1. Remove search input event listener (debounced) ---
946
- const tableId: string = this._tableId();
947
- const searchElement: HTMLInputElement | null =
948
- document.querySelector<HTMLInputElement>(
949
- `[data-kt-datatable-search="#${tableId}"]`,
950
- );
951
- if (searchElement) {
952
- const searchWithDebounce =
953
- KTDataTable.asSearchElementWithDebounce(searchElement);
954
- if (searchWithDebounce._debouncedSearch) {
955
- searchElement.removeEventListener(
956
- 'keyup',
957
- searchWithDebounce._debouncedSearch,
958
- );
959
- delete searchWithDebounce._debouncedSearch;
960
- }
961
- }
759
+ this._searchHandler.detach(this._tableId());
962
760
 
963
- // --- 2. Remove page size dropdown event listener ---
964
761
  if (this._sizeElement && this._sizeElement.onchange) {
965
762
  this._sizeElement.onchange = null;
966
763
  }
967
764
 
968
- // --- 3. Remove all pagination button event listeners ---
969
765
  if (this._paginationElement) {
970
- // Remove all child nodes (buttons) to ensure no lingering listeners
971
766
  while (this._paginationElement.firstChild) {
972
767
  this._paginationElement.removeChild(this._paginationElement.firstChild);
973
768
  }
974
769
  }
975
770
 
976
- // --- 4. Dispose of handler objects (checkbox, sort) ---
977
- // KTDataTableCheckboxAPI does not have a dispose method, but we can remove header checkbox listener
978
- const checkboxWithDispose = this._checkbox as KTDataTableCheckboxAPI & {
979
- dispose?: () => void;
980
- };
981
- if (this._checkbox && typeof checkboxWithDispose.dispose === 'function') {
982
- checkboxWithDispose.dispose();
983
- } else {
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
- }
990
- }
991
- }
992
- // KTDataTableSortAPI does not have a dispose method, but we can remove th click listeners by replacing them
993
- if (this._theadElement) {
994
- const ths = this._theadElement.querySelectorAll('th');
995
- ths.forEach((th) => {
996
- th.replaceWith(th.cloneNode(true));
997
- });
998
- }
999
-
1000
- // --- 5. Remove spinner DOM node if it exists ---
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
- }
1007
- }
1008
- root.classList.remove(this._config.loadingClass ?? 'loading');
1009
-
1010
- // --- 6. Remove instance reference from the DOM element ---
1011
- const elementWithInstance = KTDataTable.asElementWithInstance(root);
1012
- if (elementWithInstance.instance) {
1013
- delete elementWithInstance.instance;
1014
- }
1015
-
1016
- KTData.remove(root, this._name);
771
+ this._checkbox.dispose();
772
+ this._sortHandler.dispose();
1017
773
 
1018
- // --- 7. (Optional) Clear localStorage state ---
1019
- // Uncomment the following line if you want to clear state on dispose:
1020
- // this._deleteState();
774
+ this._spinner.remove(this._element, this._config);
1021
775
  }
1022
776
 
1023
- private _debounce<TArgs extends unknown[]>(
1024
- func: (...args: TArgs) => void,
1025
- wait: number,
1026
- ): (...args: TArgs) => void {
1027
- let timeout: number | undefined;
1028
- return function (...args: TArgs) {
1029
- const later = () => {
1030
- clearTimeout(timeout);
1031
- func(...args);
1032
- };
1033
- clearTimeout(timeout);
1034
- timeout = window.setTimeout(later, wait);
1035
- };
777
+ /**
778
+ * Full disposal cleans up listeners AND removes the instance from
779
+ * the registry. Only called when the component is being destroyed.
780
+ */
781
+ private _dispose(): void {
782
+ this._cleanupForRedraw();
783
+
784
+ const root = this._element;
785
+ if (root) {
786
+ datatableRegistry.remove(root);
787
+ KTData.remove(root, this._name);
788
+ }
1036
789
  }
1037
790
 
1038
791
  /**
@@ -1045,16 +798,22 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1045
798
 
1046
799
  /**
1047
800
  * Sorts the data in the table by the specified field.
801
+ * When `order` is provided, applies that sort direction instead of toggling.
1048
802
  * @param field The field to sort by.
1049
- */
1050
- public sort(field: keyof T | number): void {
1051
- // Use the sort handler to update state and trigger sorting
1052
- const state = this.getState();
1053
- const sortOrder = this._sortHandler.toggleSortOrder(
1054
- state.sortField,
1055
- state.sortOrder,
1056
- field,
1057
- );
803
+ * @param order Optional sort direction (`asc`, `desc`, or `''` to clear).
804
+ */
805
+ public sort(
806
+ field: keyof T | number,
807
+ order?: KTDataTableSortOrderInterface,
808
+ ): void {
809
+ const sortOrder =
810
+ order !== undefined
811
+ ? order
812
+ : this._sortHandler.toggleSortOrder(
813
+ this.getState().sortField,
814
+ this.getState().sortOrder,
815
+ field,
816
+ );
1058
817
  this._sortHandler.setSortIcon(field as keyof T, sortOrder);
1059
818
  this._stateStore.setSort(field as never, sortOrder);
1060
819
  this._emit('sort', { field, order: sortOrder });
@@ -1091,19 +850,15 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1091
850
  }
1092
851
 
1093
852
  /**
1094
- * Reloads the data from the server and updates the table.
1095
- * Triggers the 'reload' event and the 'kt.datatable.reload' custom event.
853
+ * Reloads the data from the source (API or DOM) and redraws the table.
854
+ * @returns {Promise<void>}
1096
855
  */
1097
856
  public reload(): void {
1098
- this._emit('reload');
1099
-
1100
857
  // Fetch the data from the server using the current sort and filter settings
1101
858
  this._updateData();
1102
859
  }
1103
860
 
1104
861
  public redraw(page: number = 1): void {
1105
- this._emit('redraw');
1106
-
1107
862
  this._paginateData(page);
1108
863
  }
1109
864
 
@@ -1111,20 +866,14 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1111
866
  * Show the loading spinner of the data table.
1112
867
  */
1113
868
  public showSpinner(): void {
1114
- /**
1115
- * Show the loading spinner of the data table.
1116
- */
1117
- this._showSpinner();
869
+ this._spinner.show(this._element, this._config, this._tableElement);
1118
870
  }
1119
871
 
1120
872
  /**
1121
873
  * Hide the loading spinner of the data table.
1122
874
  */
1123
875
  public hideSpinner(): void {
1124
- /**
1125
- * Hide the loading spinner of the data table.
1126
- */
1127
- this._hideSpinner();
876
+ this._spinner.hide(this._element, this._config);
1128
877
  }
1129
878
 
1130
879
  /**
@@ -1149,37 +898,12 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1149
898
  this.reload();
1150
899
  }
1151
900
 
1152
- /**
1153
- * Static variables
1154
- */
1155
- private static _instances = new Map<
1156
- HTMLElement,
1157
- KTDataTable<KTDataTableDataInterface>
1158
- >();
1159
-
1160
901
  /**
1161
902
  * Create KTDataTable instances for all elements with a data-kt-datatable="true" attribute.
1162
903
  * This function is now browser-guarded and must be called explicitly.
1163
904
  */
1164
905
  public static createInstances(): void {
1165
- if (typeof document === 'undefined') return;
1166
- const elements = document.querySelectorAll<HTMLElement>(
1167
- '[data-kt-datatable="true"]',
1168
- );
1169
-
1170
- elements.forEach((element) => {
1171
- if (
1172
- element.hasAttribute('data-kt-datatable') &&
1173
- !element.classList.contains('datatable-initialized')
1174
- ) {
1175
- /**
1176
- * Create an instance of KTDataTable for the given element
1177
- * @param element The element to create an instance for
1178
- */
1179
- const instance = new KTDataTable(element);
1180
- this._instances.set(element, instance);
1181
- }
1182
- });
906
+ datatableRegistry.createAll((el) => new KTDataTable(el));
1183
907
  }
1184
908
 
1185
909
  /**
@@ -1191,14 +915,7 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1191
915
  public static getInstance(
1192
916
  element: HTMLElement,
1193
917
  ): KTDataTable<KTDataTableDataInterface> | undefined {
1194
- // First check the static Map (for instances created via createInstances)
1195
- const instanceFromMap = this._instances.get(element);
1196
- if (instanceFromMap) {
1197
- return instanceFromMap;
1198
- }
1199
-
1200
- // Fallback to element's instance property (for manually created instances)
1201
- return KTDataTable.asElementWithInstance(element).instance;
918
+ return datatableRegistry.get(element);
1202
919
  }
1203
920
 
1204
921
  /**
@@ -1206,7 +923,6 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1206
923
  * This function is now browser-guarded and must be called explicitly.
1207
924
  */
1208
925
  public static init(): void {
1209
- if (typeof document === 'undefined') return;
1210
926
  KTDataTable.createInstances();
1211
927
  }
1212
928
 
@@ -1215,25 +931,7 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1215
931
  * Useful for Livewire wire:navigate where the DOM is replaced and new tables need to be initialized.
1216
932
  */
1217
933
  public static reinit(): void {
1218
- if (typeof document === 'undefined') return;
1219
- const elements = document.querySelectorAll<HTMLElement>(
1220
- '[data-kt-datatable="true"]',
1221
- );
1222
- elements.forEach((element) => {
1223
- try {
1224
- const instance = KTDataTable.getInstance(element);
1225
- if (instance && typeof instance.dispose === 'function') {
1226
- instance.dispose();
1227
- }
1228
- KTData.remove(element, 'datatable');
1229
- element.removeAttribute('data-kt-datatable-initialized');
1230
- element.classList.remove('datatable-initialized');
1231
- } catch {
1232
- // ignore per-element errors
1233
- }
1234
- });
1235
- KTDataTable._instances.clear();
1236
- KTDataTable.createInstances();
934
+ datatableRegistry.reinit((el) => new KTDataTable(el));
1237
935
  }
1238
936
 
1239
937
  /**
@@ -1279,13 +977,20 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1279
977
  }
1280
978
 
1281
979
  /**
1282
- * Reapply checked state to visible checkboxes (after redraw/pagination)
980
+ * Re-apply checkbox checked states to visible rows after a redraw or pagination change.
1283
981
  * @returns {void}
1284
982
  */
1285
- public update(): void {
983
+ public refreshCheckboxes(): void {
1286
984
  this._checkbox.updateState();
1287
985
  }
1288
986
 
987
+ /**
988
+ * @deprecated Use {@link refreshCheckboxes} instead.
989
+ */
990
+ public update(): void {
991
+ this.refreshCheckboxes();
992
+ }
993
+
1289
994
  // Other plugin methods can be added here
1290
995
  }
1291
996