@keenthemes/ktui 1.2.2 → 1.2.4

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 (166) hide show
  1. package/dist/ktui.js +1738 -986
  2. package/dist/ktui.min.js +1 -1
  3. package/dist/ktui.min.js.map +1 -1
  4. package/dist/styles.css +110 -197
  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-contracts.d.ts +66 -0
  18. package/lib/cjs/components/datatable/datatable-contracts.d.ts.map +1 -0
  19. package/lib/cjs/components/datatable/datatable-contracts.js +7 -0
  20. package/lib/cjs/components/datatable/datatable-contracts.js.map +1 -0
  21. package/lib/cjs/components/datatable/datatable-event-adapter.d.ts +7 -0
  22. package/lib/cjs/components/datatable/datatable-event-adapter.d.ts.map +1 -0
  23. package/lib/cjs/components/datatable/datatable-event-adapter.js +16 -0
  24. package/lib/cjs/components/datatable/datatable-event-adapter.js.map +1 -0
  25. package/lib/cjs/components/datatable/datatable-local-provider.d.ts +25 -0
  26. package/lib/cjs/components/datatable/datatable-local-provider.d.ts.map +1 -0
  27. package/lib/cjs/components/datatable/datatable-local-provider.js +184 -0
  28. package/lib/cjs/components/datatable/datatable-local-provider.js.map +1 -0
  29. package/lib/cjs/components/datatable/datatable-pagination-renderer.d.ts +15 -0
  30. package/lib/cjs/components/datatable/datatable-pagination-renderer.d.ts.map +1 -0
  31. package/lib/cjs/components/datatable/datatable-pagination-renderer.js +128 -0
  32. package/lib/cjs/components/datatable/datatable-pagination-renderer.js.map +1 -0
  33. package/lib/cjs/components/datatable/datatable-remote-provider.d.ts +25 -0
  34. package/lib/cjs/components/datatable/datatable-remote-provider.d.ts.map +1 -0
  35. package/lib/cjs/components/datatable/datatable-remote-provider.js +188 -0
  36. package/lib/cjs/components/datatable/datatable-remote-provider.js.map +1 -0
  37. package/lib/cjs/components/datatable/datatable-state-store.d.ts +21 -0
  38. package/lib/cjs/components/datatable/datatable-state-store.d.ts.map +1 -0
  39. package/lib/cjs/components/datatable/datatable-state-store.js +81 -0
  40. package/lib/cjs/components/datatable/datatable-state-store.js.map +1 -0
  41. package/lib/cjs/components/datatable/datatable-table-renderer.d.ts +16 -0
  42. package/lib/cjs/components/datatable/datatable-table-renderer.d.ts.map +1 -0
  43. package/lib/cjs/components/datatable/datatable-table-renderer.js +133 -0
  44. package/lib/cjs/components/datatable/datatable-table-renderer.js.map +1 -0
  45. package/lib/cjs/components/datatable/datatable.d.ts +9 -87
  46. package/lib/cjs/components/datatable/datatable.d.ts.map +1 -1
  47. package/lib/cjs/components/datatable/datatable.js +115 -687
  48. package/lib/cjs/components/datatable/datatable.js.map +1 -1
  49. package/lib/cjs/components/modal/modal.d.ts.map +1 -1
  50. package/lib/cjs/components/modal/modal.js +19 -13
  51. package/lib/cjs/components/modal/modal.js.map +1 -1
  52. package/lib/cjs/components/select/index.d.ts +1 -1
  53. package/lib/cjs/components/select/index.d.ts.map +1 -1
  54. package/lib/cjs/components/theme-switch/theme-switch.d.ts +3 -0
  55. package/lib/cjs/components/theme-switch/theme-switch.d.ts.map +1 -1
  56. package/lib/cjs/components/theme-switch/theme-switch.js +17 -4
  57. package/lib/cjs/components/theme-switch/theme-switch.js.map +1 -1
  58. package/lib/cjs/components/toggle/toggle.d.ts +2 -0
  59. package/lib/cjs/components/toggle/toggle.d.ts.map +1 -1
  60. package/lib/cjs/components/toggle/toggle.js +11 -2
  61. package/lib/cjs/components/toggle/toggle.js.map +1 -1
  62. package/lib/cjs/index.d.ts +5 -1
  63. package/lib/cjs/index.d.ts.map +1 -1
  64. package/lib/cjs/index.js +7 -7
  65. package/lib/cjs/index.js.map +1 -1
  66. package/lib/cjs/init-all.d.ts +6 -0
  67. package/lib/cjs/init-all.d.ts.map +1 -0
  68. package/lib/cjs/init-all.js +17 -0
  69. package/lib/cjs/init-all.js.map +1 -0
  70. package/lib/cjs/legacy.d.ts +8 -0
  71. package/lib/cjs/legacy.d.ts.map +1 -0
  72. package/lib/cjs/legacy.js +26 -0
  73. package/lib/cjs/legacy.js.map +1 -0
  74. package/lib/esm/components/context-menu/context-menu.d.ts +66 -0
  75. package/lib/esm/components/context-menu/context-menu.d.ts.map +1 -0
  76. package/lib/esm/components/context-menu/context-menu.js +420 -0
  77. package/lib/esm/components/context-menu/context-menu.js.map +1 -0
  78. package/lib/esm/components/context-menu/index.d.ts +7 -0
  79. package/lib/esm/components/context-menu/index.d.ts.map +1 -0
  80. package/lib/esm/components/context-menu/index.js +6 -0
  81. package/lib/esm/components/context-menu/index.js.map +1 -0
  82. package/lib/esm/components/context-menu/types.d.ts +30 -0
  83. package/lib/esm/components/context-menu/types.d.ts.map +1 -0
  84. package/lib/esm/components/context-menu/types.js +6 -0
  85. package/lib/esm/components/context-menu/types.js.map +1 -0
  86. package/lib/esm/components/datatable/datatable-contracts.d.ts +66 -0
  87. package/lib/esm/components/datatable/datatable-contracts.d.ts.map +1 -0
  88. package/lib/esm/components/datatable/datatable-contracts.js +6 -0
  89. package/lib/esm/components/datatable/datatable-contracts.js.map +1 -0
  90. package/lib/esm/components/datatable/datatable-event-adapter.d.ts +7 -0
  91. package/lib/esm/components/datatable/datatable-event-adapter.d.ts.map +1 -0
  92. package/lib/esm/components/datatable/datatable-event-adapter.js +13 -0
  93. package/lib/esm/components/datatable/datatable-event-adapter.js.map +1 -0
  94. package/lib/esm/components/datatable/datatable-local-provider.d.ts +25 -0
  95. package/lib/esm/components/datatable/datatable-local-provider.d.ts.map +1 -0
  96. package/lib/esm/components/datatable/datatable-local-provider.js +181 -0
  97. package/lib/esm/components/datatable/datatable-local-provider.js.map +1 -0
  98. package/lib/esm/components/datatable/datatable-pagination-renderer.d.ts +15 -0
  99. package/lib/esm/components/datatable/datatable-pagination-renderer.d.ts.map +1 -0
  100. package/lib/esm/components/datatable/datatable-pagination-renderer.js +125 -0
  101. package/lib/esm/components/datatable/datatable-pagination-renderer.js.map +1 -0
  102. package/lib/esm/components/datatable/datatable-remote-provider.d.ts +25 -0
  103. package/lib/esm/components/datatable/datatable-remote-provider.d.ts.map +1 -0
  104. package/lib/esm/components/datatable/datatable-remote-provider.js +185 -0
  105. package/lib/esm/components/datatable/datatable-remote-provider.js.map +1 -0
  106. package/lib/esm/components/datatable/datatable-state-store.d.ts +21 -0
  107. package/lib/esm/components/datatable/datatable-state-store.d.ts.map +1 -0
  108. package/lib/esm/components/datatable/datatable-state-store.js +78 -0
  109. package/lib/esm/components/datatable/datatable-state-store.js.map +1 -0
  110. package/lib/esm/components/datatable/datatable-table-renderer.d.ts +16 -0
  111. package/lib/esm/components/datatable/datatable-table-renderer.d.ts.map +1 -0
  112. package/lib/esm/components/datatable/datatable-table-renderer.js +130 -0
  113. package/lib/esm/components/datatable/datatable-table-renderer.js.map +1 -0
  114. package/lib/esm/components/datatable/datatable.d.ts +9 -87
  115. package/lib/esm/components/datatable/datatable.d.ts.map +1 -1
  116. package/lib/esm/components/datatable/datatable.js +115 -687
  117. package/lib/esm/components/datatable/datatable.js.map +1 -1
  118. package/lib/esm/components/modal/modal.d.ts.map +1 -1
  119. package/lib/esm/components/modal/modal.js +19 -13
  120. package/lib/esm/components/modal/modal.js.map +1 -1
  121. package/lib/esm/components/select/index.d.ts +1 -1
  122. package/lib/esm/components/select/index.d.ts.map +1 -1
  123. package/lib/esm/components/theme-switch/theme-switch.d.ts +3 -0
  124. package/lib/esm/components/theme-switch/theme-switch.d.ts.map +1 -1
  125. package/lib/esm/components/theme-switch/theme-switch.js +17 -4
  126. package/lib/esm/components/theme-switch/theme-switch.js.map +1 -1
  127. package/lib/esm/components/toggle/toggle.d.ts +2 -0
  128. package/lib/esm/components/toggle/toggle.d.ts.map +1 -1
  129. package/lib/esm/components/toggle/toggle.js +11 -2
  130. package/lib/esm/components/toggle/toggle.js.map +1 -1
  131. package/lib/esm/index.d.ts +5 -1
  132. package/lib/esm/index.d.ts.map +1 -1
  133. package/lib/esm/index.js +4 -5
  134. package/lib/esm/index.js.map +1 -1
  135. package/lib/esm/init-all.d.ts +6 -0
  136. package/lib/esm/init-all.d.ts.map +1 -0
  137. package/lib/esm/init-all.js +13 -0
  138. package/lib/esm/init-all.js.map +1 -0
  139. package/lib/esm/legacy.d.ts +8 -0
  140. package/lib/esm/legacy.d.ts.map +1 -0
  141. package/lib/esm/legacy.js +8 -0
  142. package/lib/esm/legacy.js.map +1 -0
  143. package/package.json +34 -4
  144. package/src/__tests__/entrypoints.test.ts +71 -0
  145. package/src/components/context-menu/__tests__/context-menu.test.ts +117 -0
  146. package/src/components/context-menu/context-menu.css +32 -0
  147. package/src/components/context-menu/context-menu.ts +529 -0
  148. package/src/components/context-menu/index.ts +10 -0
  149. package/src/components/context-menu/types.ts +32 -0
  150. package/src/components/datatable/__tests__/architecture-boundaries.test.ts +259 -0
  151. package/src/components/datatable/datatable-contracts.ts +96 -0
  152. package/src/components/datatable/datatable-event-adapter.ts +21 -0
  153. package/src/components/datatable/datatable-local-provider.ts +194 -0
  154. package/src/components/datatable/datatable-pagination-renderer.ts +211 -0
  155. package/src/components/datatable/datatable-remote-provider.ts +175 -0
  156. package/src/components/datatable/datatable-state-store.ts +94 -0
  157. package/src/components/datatable/datatable-table-renderer.ts +206 -0
  158. package/src/components/datatable/datatable.ts +128 -839
  159. package/src/components/modal/modal.ts +22 -14
  160. package/src/components/select/index.ts +1 -1
  161. package/src/components/theme-switch/theme-switch.ts +22 -4
  162. package/src/components/toggle/toggle.ts +12 -2
  163. package/src/index.ts +9 -5
  164. package/src/init-all.ts +15 -0
  165. package/src/legacy.ts +9 -0
  166. 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
@@ -69,22 +80,17 @@ export class KTDataTable<T extends KTDataTableDataInterface>
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
 
@@ -101,20 +107,25 @@ export class KTDataTable<T extends KTDataTableDataInterface>
101
107
 
102
108
  this._init(element);
103
109
  this._buildConfig();
110
+ this._stateStore = new KTDataTableConfigStateStore(this._config);
111
+ this._eventAdapter = createDataTableEventAdapter(
112
+ this._fireEvent.bind(this),
113
+ this._dispatchEvent.bind(this),
114
+ );
104
115
 
105
116
  // Store the instance directly on the element
106
117
  KTDataTable.asElementWithInstance(element).instance = this;
107
118
 
108
119
  this._initElements();
120
+ this._tableRenderer = new KTDataTableDomTableRenderer<T>();
121
+ this._paginationRenderer = new KTDataTableDomPaginationRenderer();
122
+ this._initDataProviders();
109
123
 
110
124
  // Initialize checkbox handler
111
125
  this._checkbox = createCheckboxHandler(
112
126
  this._element,
113
127
  this._config,
114
- (eventName: string, eventData?: object) => {
115
- this._fireEvent(eventName, eventData);
116
- this._dispatchEvent(eventName, eventData);
117
- },
128
+ this._emit.bind(this),
118
129
  );
119
130
 
120
131
  // Initialize sort handler
@@ -126,8 +137,7 @@ export class KTDataTable<T extends KTDataTableDataInterface>
126
137
  sortOrder: this.getState().sortOrder,
127
138
  }),
128
139
  (field, order) => {
129
- this._config._state.sortField = field as never;
130
- this._config._state.sortOrder = order;
140
+ this._stateStore.setSort(field as never, order);
131
141
  },
132
142
  this._fireEvent.bind(this),
133
143
  this._dispatchEvent.bind(this),
@@ -146,8 +156,33 @@ export class KTDataTable<T extends KTDataTableDataInterface>
146
156
 
147
157
  this._updateData();
148
158
 
149
- this._fireEvent('init');
150
- this._dispatchEvent('init');
159
+ this._emit('init');
160
+ }
161
+
162
+ private _emit(eventName: string, eventData?: object): void {
163
+ this._eventAdapter.emit(eventName, eventData);
164
+ }
165
+
166
+ private _initDataProviders(): void {
167
+ this._localProvider = new KTDataTableLocalDataProvider<T>({
168
+ config: this._config,
169
+ elements: () => ({
170
+ tableElement: this._tableElement,
171
+ tbodyElement: this._tbodyElement,
172
+ theadElement: this._theadElement,
173
+ }),
174
+ getLogicalColumnCount: this._getLogicalColumnCount.bind(this),
175
+ storeOriginalClasses: this._storeOriginalClasses.bind(this),
176
+ stateStore: this._stateStore,
177
+ });
178
+
179
+ this._remoteProvider = new KTDataTableRemoteDataProvider<T>({
180
+ config: this._config,
181
+ createUrl: this._createUrl.bind(this),
182
+ eventAdapter: this._eventAdapter,
183
+ noticeOnTable: this._noticeOnTable.bind(this),
184
+ stateStore: this._stateStore,
185
+ });
151
186
  }
152
187
 
153
188
  /**
@@ -480,14 +515,20 @@ export class KTDataTable<T extends KTDataTableDataInterface>
480
515
  try {
481
516
  this._showSpinner(); // Show spinner before fetching data
482
517
 
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();
518
+ this._emit('fetch');
519
+ const result =
520
+ typeof this._config.apiEndpoint === 'undefined'
521
+ ? this._localProvider.fetchSync()
522
+ : await this._remoteProvider.fetch();
523
+
524
+ if (!result.skipped) {
525
+ this._data = result.data;
526
+ this._stateStore.patchState({ totalItems: result.totalItems });
527
+ await this._draw();
528
+ this._emit('fetched');
490
529
  }
530
+
531
+ await this._finalize();
491
532
  } finally {
492
533
  // Finally block now correctly executes after promises resolve, not immediately
493
534
  this._isFetching = false;
@@ -569,188 +610,6 @@ export class KTDataTable<T extends KTDataTableDataInterface>
569
610
  }
570
611
  }
571
612
 
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
613
  /**
755
614
  * Returns the logical data column count (number of data columns), used for multi-row headers
756
615
  * where querySelectorAll('th') would overcount. Prefers state.originalData, then first tbody row td count.
@@ -771,184 +630,6 @@ export class KTDataTable<T extends KTDataTableDataInterface>
771
630
  return 0;
772
631
  }
773
632
 
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
633
  /**
953
634
  * Creates a complete URL from a relative path or a full URL.
954
635
  *
@@ -988,11 +669,12 @@ export class KTDataTable<T extends KTDataTableDataInterface>
988
669
  * @returns {Promise<void>} A promise that resolves when the table and pagination controls are updated
989
670
  */
990
671
  private async _draw(): Promise<void> {
991
- this._config._state.totalPages =
992
- Math.ceil(this.getState().totalItems / this.getState().pageSize) || 0;
672
+ this._stateStore.patchState({
673
+ totalPages:
674
+ Math.ceil(this.getState().totalItems / this.getState().pageSize) || 0,
675
+ });
993
676
 
994
- this._fireEvent('draw');
995
- this._dispatchEvent('draw');
677
+ this._emit('draw');
996
678
 
997
679
  this._dispose();
998
680
 
@@ -1005,8 +687,7 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1005
687
  this._updatePagination();
1006
688
  }
1007
689
 
1008
- this._fireEvent('drew');
1009
- this._dispatchEvent('drew');
690
+ this._emit('drew');
1010
691
 
1011
692
  // Spinner is hidden in _finalize() to ensure it stays visible until the entire request completes
1012
693
  // Removed duplicate _hideSpinner() call here to prevent premature hiding
@@ -1021,143 +702,18 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1021
702
  * @returns {HTMLTableSectionElement} The new table body element
1022
703
  */
1023
704
  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);
705
+ return this._tableRenderer.render({
706
+ config: this._config,
707
+ context: this,
708
+ data: this._data,
709
+ getLogicalColumnCount: this._getLogicalColumnCount.bind(this),
710
+ getState: this.getState.bind(this),
711
+ originalTbodyClass: this._originalTbodyClass,
712
+ originalTrClasses: this._originalTrClasses,
713
+ originalTdClasses: this._originalTdClasses,
714
+ tableElement: this._tableElement,
715
+ theadElement: this._theadElement,
1157
716
  });
1158
-
1159
- tbodyElement.appendChild(fragment);
1160
- return tbodyElement;
1161
717
  }
1162
718
 
1163
719
  /**
@@ -1166,82 +722,30 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1166
722
  * @returns {void}
1167
723
  */
1168
724
  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;
725
+ this._tableRenderer.notice(
726
+ this._tableElement,
727
+ this._getLogicalColumnCount.bind(this),
728
+ message,
729
+ );
1175
730
  }
1176
731
 
1177
732
  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
- }
733
+ const cleanup = this._paginationRenderer.render({
734
+ config: this._config,
735
+ dataLength: this._data.length,
736
+ infoElement: this._infoElement,
737
+ paginateData: this._paginateData.bind(this),
738
+ paginationElement: this._paginationElement,
739
+ reloadPageSize: this._reloadPageSize.bind(this),
740
+ sizeElement: this._sizeElement,
741
+ state: this.getState(),
742
+ });
1193
743
 
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);
744
+ if (typeof cleanup === 'function') {
745
+ this._cleanupCallbacks.push(cleanup);
1198
746
  }
1199
747
  }
1200
748
 
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;
1212
- }
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
- }
1244
-
1245
749
  /**
1246
750
  * Reloads the data with the specified page size and optional page number.
1247
751
  * @param pageSize The new page size.
@@ -1249,191 +753,12 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1249
753
  */
1250
754
  private _reloadPageSize(pageSize: number, page: number = 1): void {
1251
755
  // Update the page size and page number in the state
1252
- this._config._state.pageSize = pageSize;
1253
- this._config._state.page = page;
756
+ this._stateStore.setPageSize(pageSize, page);
1254
757
 
1255
758
  // Update the data with the new page size and page number
1256
759
  this._updateData();
1257
760
  }
1258
761
 
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
762
  /**
1438
763
  * Method for handling pagination
1439
764
  * @param page - The page number to navigate to
@@ -1443,11 +768,10 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1443
768
  return;
1444
769
  }
1445
770
 
1446
- this._fireEvent('pagination', { page: page });
1447
- this._dispatchEvent('pagination', { page: page });
771
+ this._emit('pagination', { page: page });
1448
772
 
1449
773
  if (page >= 1 && page <= this.getState().totalPages) {
1450
- this._config._state.page = page;
774
+ this._stateStore.setPage(page);
1451
775
  this._updateData();
1452
776
  }
1453
777
  }
@@ -1498,8 +822,7 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1498
822
  * @returns {void}
1499
823
  */
1500
824
  private _saveState(): void {
1501
- this._fireEvent('stateSave');
1502
- this._dispatchEvent('stateSave');
825
+ this._emit('stateSave');
1503
826
 
1504
827
  const ns: string = this._tableNamespace();
1505
828
 
@@ -1521,7 +844,7 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1521
844
 
1522
845
  try {
1523
846
  const state = JSON.parse(stateString) as KTDataTableStateInterface;
1524
- if (state) this._config._state = state;
847
+ if (state) this._stateStore.replaceState(state);
1525
848
  return state;
1526
849
  } catch {}
1527
850
 
@@ -1574,6 +897,9 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1574
897
  * This method is called before re-rendering or when disposing the component.
1575
898
  */
1576
899
  private _dispose() {
900
+ this._cleanupCallbacks.forEach((cleanup) => cleanup());
901
+ this._cleanupCallbacks = [];
902
+
1577
903
  // --- 1. Remove search input event listener (debounced) ---
1578
904
  const tableId: string = this._tableId();
1579
905
  const searchElement: HTMLInputElement | null =
@@ -1583,14 +909,13 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1583
909
  if (searchElement) {
1584
910
  const searchWithDebounce =
1585
911
  KTDataTable.asSearchElementWithDebounce(searchElement);
1586
- if (!searchWithDebounce._debouncedSearch) {
1587
- return;
912
+ if (searchWithDebounce._debouncedSearch) {
913
+ searchElement.removeEventListener(
914
+ 'keyup',
915
+ searchWithDebounce._debouncedSearch,
916
+ );
917
+ delete searchWithDebounce._debouncedSearch;
1588
918
  }
1589
- searchElement.removeEventListener(
1590
- 'keyup',
1591
- searchWithDebounce._debouncedSearch,
1592
- );
1593
- delete searchWithDebounce._debouncedSearch;
1594
919
  }
1595
920
 
1596
921
  // --- 2. Remove page size dropdown event listener ---
@@ -1672,31 +997,7 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1672
997
  * @returns {KTDataTableStateInterface} The current state of the table.
1673
998
  */
1674
999
  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
- };
1000
+ return this._stateStore.getState();
1700
1001
  }
1701
1002
 
1702
1003
  /**
@@ -1712,10 +1013,8 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1712
1013
  field,
1713
1014
  );
1714
1015
  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 });
1016
+ this._stateStore.setSort(field as never, sortOrder);
1017
+ this._emit('sort', { field, order: sortOrder });
1719
1018
  this._updateData();
1720
1019
  }
1721
1020
 
@@ -1753,16 +1052,14 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1753
1052
  * Triggers the 'reload' event and the 'kt.datatable.reload' custom event.
1754
1053
  */
1755
1054
  public reload(): void {
1756
- this._fireEvent('reload');
1757
- this._dispatchEvent('reload');
1055
+ this._emit('reload');
1758
1056
 
1759
1057
  // Fetch the data from the server using the current sort and filter settings
1760
1058
  this._updateData();
1761
1059
  }
1762
1060
 
1763
1061
  public redraw(page: number = 1): void {
1764
- this._fireEvent('redraw');
1765
- this._dispatchEvent('redraw');
1062
+ this._emit('redraw');
1766
1063
 
1767
1064
  this._paginateData(page);
1768
1065
  }
@@ -1795,23 +1092,17 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1795
1092
  * @throws Error if the filter object is null or undefined.
1796
1093
  */
1797
1094
  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;
1095
+ this._stateStore.setFilter(filter);
1805
1096
  return this;
1806
1097
  }
1807
1098
 
1808
1099
  public override dispose(): void {
1100
+ this._remoteProvider?.dispose();
1809
1101
  this._dispose();
1810
1102
  }
1811
1103
 
1812
1104
  public search(query: string | object): void {
1813
- this._config._state.search = query;
1814
- this._config._state.page = 1;
1105
+ this._stateStore.setSearch(query);
1815
1106
  this.reload();
1816
1107
  }
1817
1108
 
@@ -1924,8 +1215,7 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1924
1215
  */
1925
1216
  public check(): void {
1926
1217
  this._checkbox.check();
1927
- this._fireEvent('checked');
1928
- this._dispatchEvent('checked');
1218
+ this._emit('checked');
1929
1219
  }
1930
1220
 
1931
1221
  /**
@@ -1934,8 +1224,7 @@ export class KTDataTable<T extends KTDataTableDataInterface>
1934
1224
  */
1935
1225
  public uncheck(): void {
1936
1226
  this._checkbox.uncheck();
1937
- this._fireEvent('unchecked');
1938
- this._dispatchEvent('unchecked');
1227
+ this._emit('unchecked');
1939
1228
  }
1940
1229
 
1941
1230
  /**