@keenthemes/ktui 1.2.6 → 1.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/README.md +14 -5
  2. package/dist/ktui.js +3775 -2298
  3. package/dist/ktui.min.js +1 -1
  4. package/dist/ktui.min.js.map +1 -1
  5. package/dist/styles.css +25 -5
  6. package/lib/cjs/components/datatable/datatable-checkbox.d.ts +37 -1
  7. package/lib/cjs/components/datatable/datatable-checkbox.d.ts.map +1 -1
  8. package/lib/cjs/components/datatable/datatable-checkbox.js +143 -156
  9. package/lib/cjs/components/datatable/datatable-checkbox.js.map +1 -1
  10. package/lib/cjs/components/datatable/datatable-column-utils.d.ts +30 -0
  11. package/lib/cjs/components/datatable/datatable-column-utils.d.ts.map +1 -0
  12. package/lib/cjs/components/datatable/datatable-column-utils.js +42 -0
  13. package/lib/cjs/components/datatable/datatable-column-utils.js.map +1 -0
  14. package/lib/cjs/components/datatable/datatable-contracts.d.ts +2 -4
  15. package/lib/cjs/components/datatable/datatable-contracts.d.ts.map +1 -1
  16. package/lib/cjs/components/datatable/datatable-defaults.d.ts +20 -0
  17. package/lib/cjs/components/datatable/datatable-defaults.d.ts.map +1 -0
  18. package/lib/cjs/components/datatable/datatable-defaults.js +193 -0
  19. package/lib/cjs/components/datatable/datatable-defaults.js.map +1 -0
  20. package/lib/cjs/components/datatable/datatable-layout-plugin.d.ts.map +1 -1
  21. package/lib/cjs/components/datatable/datatable-layout-plugin.js +11 -1
  22. package/lib/cjs/components/datatable/datatable-layout-plugin.js.map +1 -1
  23. package/lib/cjs/components/datatable/datatable-local-provider.d.ts.map +1 -1
  24. package/lib/cjs/components/datatable/datatable-local-provider.js +80 -24
  25. package/lib/cjs/components/datatable/datatable-local-provider.js.map +1 -1
  26. package/lib/cjs/components/datatable/datatable-pagination-renderer.d.ts.map +1 -1
  27. package/lib/cjs/components/datatable/datatable-pagination-renderer.js +3 -2
  28. package/lib/cjs/components/datatable/datatable-pagination-renderer.js.map +1 -1
  29. package/lib/cjs/components/datatable/datatable-registry.d.ts +18 -0
  30. package/lib/cjs/components/datatable/datatable-registry.d.ts.map +1 -0
  31. package/lib/cjs/components/datatable/datatable-registry.js +66 -0
  32. package/lib/cjs/components/datatable/datatable-registry.js.map +1 -0
  33. package/lib/cjs/components/datatable/datatable-remote-provider.d.ts.map +1 -1
  34. package/lib/cjs/components/datatable/datatable-remote-provider.js +1 -2
  35. package/lib/cjs/components/datatable/datatable-remote-provider.js.map +1 -1
  36. package/lib/cjs/components/datatable/datatable-search-handler.d.ts +10 -0
  37. package/lib/cjs/components/datatable/datatable-search-handler.d.ts.map +1 -0
  38. package/lib/cjs/components/datatable/datatable-search-handler.js +65 -0
  39. package/lib/cjs/components/datatable/datatable-search-handler.js.map +1 -0
  40. package/lib/cjs/components/datatable/datatable-sort.d.ts +31 -4
  41. package/lib/cjs/components/datatable/datatable-sort.d.ts.map +1 -1
  42. package/lib/cjs/components/datatable/datatable-sort.js +86 -58
  43. package/lib/cjs/components/datatable/datatable-sort.js.map +1 -1
  44. package/lib/cjs/components/datatable/datatable-spinner.d.ts +30 -0
  45. package/lib/cjs/components/datatable/datatable-spinner.d.ts.map +1 -0
  46. package/lib/cjs/components/datatable/datatable-spinner.js +54 -0
  47. package/lib/cjs/components/datatable/datatable-spinner.js.map +1 -0
  48. package/lib/cjs/components/datatable/datatable-state-persistence.d.ts +19 -0
  49. package/lib/cjs/components/datatable/datatable-state-persistence.d.ts.map +1 -0
  50. package/lib/cjs/components/datatable/datatable-state-persistence.js +59 -0
  51. package/lib/cjs/components/datatable/datatable-state-persistence.js.map +1 -0
  52. package/lib/cjs/components/datatable/datatable-table-renderer.d.ts +2 -0
  53. package/lib/cjs/components/datatable/datatable-table-renderer.d.ts.map +1 -1
  54. package/lib/cjs/components/datatable/datatable-table-renderer.js +75 -16
  55. package/lib/cjs/components/datatable/datatable-table-renderer.js.map +1 -1
  56. package/lib/cjs/components/datatable/datatable-utils.d.ts +10 -0
  57. package/lib/cjs/components/datatable/datatable-utils.d.ts.map +1 -0
  58. package/lib/cjs/components/datatable/datatable-utils.js +15 -0
  59. package/lib/cjs/components/datatable/datatable-utils.js.map +1 -0
  60. package/lib/cjs/components/datatable/datatable.d.ts +26 -34
  61. package/lib/cjs/components/datatable/datatable.d.ts.map +1 -1
  62. package/lib/cjs/components/datatable/datatable.js +155 -492
  63. package/lib/cjs/components/datatable/datatable.js.map +1 -1
  64. package/lib/cjs/components/datatable/index.d.ts +1 -1
  65. package/lib/cjs/components/datatable/index.d.ts.map +1 -1
  66. package/lib/cjs/components/datatable/types.d.ts +100 -11
  67. package/lib/cjs/components/datatable/types.d.ts.map +1 -1
  68. package/lib/cjs/index.d.ts +1 -1
  69. package/lib/cjs/index.d.ts.map +1 -1
  70. package/lib/cjs/index.js +6 -0
  71. package/lib/cjs/index.js.map +1 -1
  72. package/lib/esm/components/datatable/datatable-checkbox.d.ts +37 -1
  73. package/lib/esm/components/datatable/datatable-checkbox.d.ts.map +1 -1
  74. package/lib/esm/components/datatable/datatable-checkbox.js +142 -155
  75. package/lib/esm/components/datatable/datatable-checkbox.js.map +1 -1
  76. package/lib/esm/components/datatable/datatable-column-utils.d.ts +30 -0
  77. package/lib/esm/components/datatable/datatable-column-utils.d.ts.map +1 -0
  78. package/lib/esm/components/datatable/datatable-column-utils.js +38 -0
  79. package/lib/esm/components/datatable/datatable-column-utils.js.map +1 -0
  80. package/lib/esm/components/datatable/datatable-contracts.d.ts +2 -4
  81. package/lib/esm/components/datatable/datatable-contracts.d.ts.map +1 -1
  82. package/lib/esm/components/datatable/datatable-defaults.d.ts +20 -0
  83. package/lib/esm/components/datatable/datatable-defaults.d.ts.map +1 -0
  84. package/lib/esm/components/datatable/datatable-defaults.js +190 -0
  85. package/lib/esm/components/datatable/datatable-defaults.js.map +1 -0
  86. package/lib/esm/components/datatable/datatable-layout-plugin.d.ts.map +1 -1
  87. package/lib/esm/components/datatable/datatable-layout-plugin.js +11 -1
  88. package/lib/esm/components/datatable/datatable-layout-plugin.js.map +1 -1
  89. package/lib/esm/components/datatable/datatable-local-provider.d.ts.map +1 -1
  90. package/lib/esm/components/datatable/datatable-local-provider.js +80 -24
  91. package/lib/esm/components/datatable/datatable-local-provider.js.map +1 -1
  92. package/lib/esm/components/datatable/datatable-pagination-renderer.d.ts.map +1 -1
  93. package/lib/esm/components/datatable/datatable-pagination-renderer.js +3 -2
  94. package/lib/esm/components/datatable/datatable-pagination-renderer.js.map +1 -1
  95. package/lib/esm/components/datatable/datatable-registry.d.ts +18 -0
  96. package/lib/esm/components/datatable/datatable-registry.d.ts.map +1 -0
  97. package/lib/esm/components/datatable/datatable-registry.js +63 -0
  98. package/lib/esm/components/datatable/datatable-registry.js.map +1 -0
  99. package/lib/esm/components/datatable/datatable-remote-provider.d.ts.map +1 -1
  100. package/lib/esm/components/datatable/datatable-remote-provider.js +1 -2
  101. package/lib/esm/components/datatable/datatable-remote-provider.js.map +1 -1
  102. package/lib/esm/components/datatable/datatable-search-handler.d.ts +10 -0
  103. package/lib/esm/components/datatable/datatable-search-handler.d.ts.map +1 -0
  104. package/lib/esm/components/datatable/datatable-search-handler.js +62 -0
  105. package/lib/esm/components/datatable/datatable-search-handler.js.map +1 -0
  106. package/lib/esm/components/datatable/datatable-sort.d.ts +31 -4
  107. package/lib/esm/components/datatable/datatable-sort.d.ts.map +1 -1
  108. package/lib/esm/components/datatable/datatable-sort.js +85 -57
  109. package/lib/esm/components/datatable/datatable-sort.js.map +1 -1
  110. package/lib/esm/components/datatable/datatable-spinner.d.ts +30 -0
  111. package/lib/esm/components/datatable/datatable-spinner.d.ts.map +1 -0
  112. package/lib/esm/components/datatable/datatable-spinner.js +51 -0
  113. package/lib/esm/components/datatable/datatable-spinner.js.map +1 -0
  114. package/lib/esm/components/datatable/datatable-state-persistence.d.ts +19 -0
  115. package/lib/esm/components/datatable/datatable-state-persistence.d.ts.map +1 -0
  116. package/lib/esm/components/datatable/datatable-state-persistence.js +55 -0
  117. package/lib/esm/components/datatable/datatable-state-persistence.js.map +1 -0
  118. package/lib/esm/components/datatable/datatable-table-renderer.d.ts +2 -0
  119. package/lib/esm/components/datatable/datatable-table-renderer.d.ts.map +1 -1
  120. package/lib/esm/components/datatable/datatable-table-renderer.js +75 -16
  121. package/lib/esm/components/datatable/datatable-table-renderer.js.map +1 -1
  122. package/lib/esm/components/datatable/datatable-utils.d.ts +10 -0
  123. package/lib/esm/components/datatable/datatable-utils.d.ts.map +1 -0
  124. package/lib/esm/components/datatable/datatable-utils.js +12 -0
  125. package/lib/esm/components/datatable/datatable-utils.js.map +1 -0
  126. package/lib/esm/components/datatable/datatable.d.ts +26 -34
  127. package/lib/esm/components/datatable/datatable.d.ts.map +1 -1
  128. package/lib/esm/components/datatable/datatable.js +157 -494
  129. package/lib/esm/components/datatable/datatable.js.map +1 -1
  130. package/lib/esm/components/datatable/index.d.ts +1 -1
  131. package/lib/esm/components/datatable/index.d.ts.map +1 -1
  132. package/lib/esm/components/datatable/types.d.ts +100 -11
  133. package/lib/esm/components/datatable/types.d.ts.map +1 -1
  134. package/lib/esm/index.d.ts +1 -1
  135. package/lib/esm/index.d.ts.map +1 -1
  136. package/lib/esm/index.js +6 -0
  137. package/lib/esm/index.js.map +1 -1
  138. package/package.json +5 -1
  139. package/skills/ktui/SKILL.md +711 -0
  140. package/skills/ktui-datatable/SKILL.md +302 -0
  141. package/skills/ktui-install/SKILL.md +150 -0
  142. package/skills/ktui-select/SKILL.md +271 -0
  143. package/src/components/__tests__/component.test.ts +347 -0
  144. package/src/components/collapse/collapse.css +2 -2
  145. package/src/components/datatable/__tests__/architecture-boundaries.test.ts +56 -8
  146. package/src/components/datatable/__tests__/currency-sort.test.ts +25 -28
  147. package/src/components/datatable/__tests__/datatable-checkbox.test.ts +527 -0
  148. package/src/components/datatable/__tests__/datatable-column-utils.test.ts +117 -0
  149. package/src/components/datatable/__tests__/datatable-defaults.test.ts +57 -0
  150. package/src/components/datatable/__tests__/datatable-finalize-extended.test.ts +361 -0
  151. package/src/components/datatable/__tests__/datatable-fixed-layout.test.ts +427 -0
  152. package/src/components/datatable/__tests__/datatable-improvements.test.ts +484 -0
  153. package/src/components/datatable/__tests__/datatable-pagination-extended.test.ts +508 -0
  154. package/src/components/datatable/__tests__/datatable-public-api.test.ts +269 -0
  155. package/src/components/datatable/__tests__/datatable-registry.test.ts +172 -0
  156. package/src/components/datatable/__tests__/datatable-remote-provider.test.ts +468 -0
  157. package/src/components/datatable/__tests__/datatable-search-handler.test.ts +124 -0
  158. package/src/components/datatable/__tests__/datatable-sort-extended.test.ts +417 -0
  159. package/src/components/datatable/__tests__/datatable-spinner.test.ts +95 -0
  160. package/src/components/datatable/__tests__/datatable-table-renderer-extended.test.ts +425 -0
  161. package/src/components/datatable/__tests__/datatable-types.test.ts +117 -0
  162. package/src/components/datatable/__tests__/datatable-utils.test.ts +52 -0
  163. package/src/components/datatable/__tests__/multi-row-headers.test.ts +7 -7
  164. package/src/components/datatable/__tests__/pagination-reset.test.ts +129 -6
  165. package/src/components/datatable/__tests__/race-conditions.test.ts +11 -11
  166. package/src/components/datatable/__tests__/setup.ts +12 -4
  167. package/src/components/datatable/datatable-checkbox.ts +144 -145
  168. package/src/components/datatable/datatable-column-utils.ts +63 -0
  169. package/src/components/datatable/datatable-contracts.ts +2 -3
  170. package/src/components/datatable/datatable-defaults.ts +204 -0
  171. package/src/components/datatable/datatable-layout-plugin.ts +11 -1
  172. package/src/components/datatable/datatable-local-provider.ts +91 -28
  173. package/src/components/datatable/datatable-pagination-renderer.ts +3 -2
  174. package/src/components/datatable/datatable-registry.ts +89 -0
  175. package/src/components/datatable/datatable-remote-provider.ts +1 -3
  176. package/src/components/datatable/datatable-search-handler.ts +97 -0
  177. package/src/components/datatable/datatable-sort.ts +111 -66
  178. package/src/components/datatable/datatable-spinner.ts +103 -0
  179. package/src/components/datatable/datatable-state-persistence.ts +67 -0
  180. package/src/components/datatable/datatable-table-renderer.ts +81 -18
  181. package/src/components/datatable/datatable-utils.ts +12 -0
  182. package/src/components/datatable/datatable.ts +191 -580
  183. package/src/components/datatable/index.ts +3 -0
  184. package/src/components/datatable/types.ts +124 -23
  185. package/src/helpers/__tests__/dom.test.ts +776 -0
  186. package/src/helpers/__tests__/utils.test.ts +332 -0
  187. package/src/index.ts +10 -0
  188. package/skills/ktui-components/SKILL.md +0 -41
  189. package/skills/ktui-theming/SKILL.md +0 -50
  190. package/src/components/datatable/datatable-event-adapter.ts +0 -21
@@ -98,6 +98,10 @@ describe('KTDataTable - Pagination Reset', () => {
98
98
  };
99
99
 
100
100
  beforeEach(() => {
101
+ // Dispose previous test's datatable to cancel in-flight async operations
102
+ if (datatable) {
103
+ try { datatable.dispose(); } catch { /* already disposed */ }
104
+ }
101
105
  // Clear any existing elements
102
106
  document.body.innerHTML = '';
103
107
  vi.clearAllMocks();
@@ -313,7 +317,7 @@ describe('KTDataTable - Pagination Reset', () => {
313
317
  expect(datatable.getState().page).toBe(1);
314
318
 
315
319
  // Apply third filter (page should stay at 1)
316
- datatable.setFilter({ column: 'id', type: 'numeric', value: '10' });
320
+ datatable.setFilter({ column: 'id', type: 'numeric', value: 10 });
317
321
  expect(datatable.getState().page).toBe(1);
318
322
  });
319
323
 
@@ -438,7 +442,17 @@ describe('KTDataTable - Pagination Reset', () => {
438
442
  });
439
443
 
440
444
  describe('Scenario: State persistence respects pagination reset', () => {
445
+ const isLocalStorageAvailable = (): boolean => {
446
+ try {
447
+ localStorage.getItem('test');
448
+ return true;
449
+ } catch {
450
+ return false;
451
+ }
452
+ };
453
+
441
454
  it('should save page 1 to state when search resets pagination', async () => {
455
+ if (!isLocalStorageAvailable()) return;
442
456
  const { container } = createMockDataTable(25);
443
457
 
444
458
  // Enable state saving with unique namespace
@@ -476,6 +490,7 @@ describe('KTDataTable - Pagination Reset', () => {
476
490
  });
477
491
 
478
492
  it('should save page 1 to state when filter resets pagination', () => {
493
+ if (!isLocalStorageAvailable()) return;
479
494
  const { container } = createMockDataTable(25);
480
495
 
481
496
  datatable = new KTDataTable(container, {
@@ -503,6 +518,7 @@ describe('KTDataTable - Pagination Reset', () => {
503
518
  });
504
519
 
505
520
  it('should restore to page 1 with active search on reload', async () => {
521
+ if (!isLocalStorageAvailable()) return;
506
522
  const { container } = createMockDataTable(25);
507
523
  const namespace = 'test-datatable-restore';
508
524
 
@@ -570,6 +586,113 @@ describe('KTDataTable - Pagination Reset', () => {
570
586
  expect(datatable.getState().totalPages).toBe(2);
571
587
  });
572
588
 
589
+ it('does not shrink originalData when tbody checksum mismatches after pagination', async () => {
590
+ const { container } = createMockDataTable(18);
591
+ datatable = new KTDataTable(container, {
592
+ pageSize: 5,
593
+ stateSave: false,
594
+ });
595
+
596
+ await new Promise((resolve) => setTimeout(resolve, 50));
597
+ expect(datatable.getState().totalPages).toBe(4);
598
+
599
+ // Simulate a fetch before _contentChecksum was aligned with the paginated tbody.
600
+ datatable.getState()._contentChecksum = 'stale-checksum';
601
+ datatable.reload();
602
+
603
+ await new Promise((resolve) => setTimeout(resolve, 50));
604
+ expect(datatable.getState().totalPages).toBe(4);
605
+ });
606
+
607
+ it('keeps 4 pages when thead has checkbox and actions columns (bulk-actions demo)', async () => {
608
+ const container = document.createElement('div');
609
+ container.id = 'test-bulk-actions-datatable';
610
+
611
+ const table = document.createElement('table');
612
+ table.setAttribute('data-kt-datatable-table', 'true');
613
+
614
+ const thead = document.createElement('thead');
615
+ thead.innerHTML = `
616
+ <tr>
617
+ <th><input type="checkbox" data-kt-datatable-check="true" /></th>
618
+ <th data-kt-datatable-column="label">Label</th>
619
+ <th data-kt-datatable-column="method">Method</th>
620
+ <th data-kt-datatable-column="status">Status</th>
621
+ <th data-kt-datatable-column="lastSession">Last Session</th>
622
+ <th data-kt-datatable-column="actions"></th>
623
+ </tr>
624
+ `;
625
+
626
+ const tbody = document.createElement('tbody');
627
+ for (let i = 1; i <= 18; i++) {
628
+ const row = document.createElement('tr');
629
+ row.innerHTML = `
630
+ <td><input type="checkbox" data-kt-datatable-row-check="true" value="${i - 1}" /></td>
631
+ <td>User ${i}</td>
632
+ <td>Web</td>
633
+ <td>active</td>
634
+ <td>22 Jul 2024</td>
635
+ <td><button type="button">Edit</button></td>
636
+ `;
637
+ tbody.appendChild(row);
638
+ }
639
+
640
+ table.appendChild(thead);
641
+ table.appendChild(tbody);
642
+
643
+ const infoElement = document.createElement('div');
644
+ infoElement.setAttribute('data-kt-datatable-info', 'true');
645
+ const sizeElement = document.createElement('select');
646
+ sizeElement.setAttribute('data-kt-datatable-size', 'true');
647
+ const paginationElement = document.createElement('div');
648
+ paginationElement.setAttribute('data-kt-datatable-pagination', 'true');
649
+
650
+ container.appendChild(table);
651
+ container.appendChild(infoElement);
652
+ container.appendChild(sizeElement);
653
+ container.appendChild(paginationElement);
654
+ document.body.appendChild(container);
655
+
656
+ datatable = new KTDataTable(container, {
657
+ pageSize: 5,
658
+ stateSave: false,
659
+ });
660
+
661
+ await new Promise((resolve) => setTimeout(resolve, 50));
662
+
663
+ expect(datatable.getState().totalPages).toBe(4);
664
+ datatable.goPage(2);
665
+ await new Promise((resolve) => setTimeout(resolve, 50));
666
+ expect(datatable.getState().page).toBe(2);
667
+ expect(datatable.getState().totalPages).toBe(4);
668
+ });
669
+
670
+ it('shows page 2 rows with tableLayout fixed and columns config (docs column-widths demo)', async () => {
671
+ const { container } = createMockDataTable(18);
672
+ datatable = new KTDataTable(container, {
673
+ pageSize: 5,
674
+ stateSave: false,
675
+ tableLayout: 'fixed',
676
+ columns: {
677
+ id: { width: '60px' },
678
+ name: { width: '140px' },
679
+ status: { width: '100px' },
680
+ },
681
+ });
682
+
683
+ await new Promise((resolve) => setTimeout(resolve, 50));
684
+
685
+ datatable.goPage(2);
686
+ await new Promise((resolve) => setTimeout(resolve, 50));
687
+
688
+ expect(datatable.getState().page).toBe(2);
689
+
690
+ const rows = container.querySelectorAll('tbody tr');
691
+ expect(rows.length).toBe(5);
692
+ expect(rows[0].cells[0].textContent).toBe('6');
693
+ expect(datatable.getState().totalPages).toBe(4);
694
+ });
695
+
573
696
  it('should handle search reset on page 1 (no-op)', () => {
574
697
  const { container } = createMockDataTable(25);
575
698
  datatable = new KTDataTable(container, {
@@ -669,9 +792,9 @@ describe('KTDataTable - Pagination Reset', () => {
669
792
  stateSave: false,
670
793
  });
671
794
 
672
- const reloadSpy = vi.fn();
673
- // Listen for 'reload' event directly (CustomEvent)
674
- container.addEventListener('reload', reloadSpy);
795
+ const updateSpy = vi.fn();
796
+ // Listen for 'kt.datatable.update' event directly (CustomEvent)
797
+ container.addEventListener('kt.datatable.update', updateSpy);
675
798
 
676
799
  datatable.goPage(2);
677
800
  datatable.search('test');
@@ -679,8 +802,8 @@ describe('KTDataTable - Pagination Reset', () => {
679
802
  // Wait for async reload to complete
680
803
  await new Promise((resolve) => setTimeout(resolve, 50));
681
804
 
682
- // reload event should still fire
683
- expect(reloadSpy).toHaveBeenCalled();
805
+ // update event should still fire
806
+ expect(updateSpy).toHaveBeenCalled();
684
807
  });
685
808
  });
686
809
  });
@@ -407,13 +407,13 @@ describe('KTDataTable Race Condition Fixes', () => {
407
407
 
408
408
  describe('Event Handling During Race Conditions', () => {
409
409
  it('should fire fetch event for successful requests', async () => {
410
- const fetchEvents: Event[] = [];
410
+ const updateEvents: Event[] = [];
411
411
 
412
412
  const element = container.querySelector(
413
413
  '[data-kt-datatable="true"]',
414
414
  ) as HTMLElement;
415
- element.addEventListener('fetch', (e) => {
416
- fetchEvents.push(e);
415
+ element.addEventListener('kt.datatable.update', (e) => {
416
+ updateEvents.push(e);
417
417
  });
418
418
 
419
419
  const datatable = new KTDataTable(element, {
@@ -425,18 +425,18 @@ describe('KTDataTable Race Condition Fixes', () => {
425
425
  datatable.search('test');
426
426
  await waitFor(150); // Complete search
427
427
 
428
- // Should fire fetch for initial and search
429
- expect(fetchEvents.length).toBeGreaterThanOrEqual(2);
428
+ // Should fire update for initial and search
429
+ expect(updateEvents.length).toBeGreaterThanOrEqual(2);
430
430
  });
431
431
 
432
432
  it('should fire fetched event after successful data load', async () => {
433
- const fetchedEvents: Event[] = [];
433
+ const updateEvents: Event[] = [];
434
434
 
435
435
  const element = container.querySelector(
436
436
  '[data-kt-datatable="true"]',
437
437
  ) as HTMLElement;
438
- element.addEventListener('fetched', (e) => {
439
- fetchedEvents.push(e);
438
+ element.addEventListener('kt.datatable.update', (e) => {
439
+ updateEvents.push(e);
440
440
  });
441
441
 
442
442
  new KTDataTable(element, {
@@ -445,8 +445,8 @@ describe('KTDataTable Race Condition Fixes', () => {
445
445
 
446
446
  await waitFor(150);
447
447
 
448
- // Should fire fetched for initial request
449
- expect(fetchedEvents.length).toBeGreaterThanOrEqual(1);
448
+ // Should fire update for initial request
449
+ expect(updateEvents.length).toBeGreaterThanOrEqual(1);
450
450
  });
451
451
 
452
452
  it('should not fire error events for AbortError', async () => {
@@ -455,7 +455,7 @@ describe('KTDataTable Race Condition Fixes', () => {
455
455
  const element = container.querySelector(
456
456
  '[data-kt-datatable="true"]',
457
457
  ) as HTMLElement;
458
- element.addEventListener('error.kt.datatable', (e) => {
458
+ element.addEventListener('kt.datatable.error', (e) => {
459
459
  errorEvents.push(e);
460
460
  });
461
461
 
@@ -17,8 +17,12 @@ vi.mock('../../../index', () => ({
17
17
 
18
18
  // Setup DOM environment before each test
19
19
  beforeEach(() => {
20
- // Clear localStorage
21
- localStorage.clear();
20
+ // Clear localStorage (may be unavailable in Node.js without --localstorage-file)
21
+ try {
22
+ localStorage.clear();
23
+ } catch {
24
+ // localStorage not available
25
+ }
22
26
 
23
27
  // Reset document body
24
28
  document.body.innerHTML = '';
@@ -29,8 +33,12 @@ beforeEach(() => {
29
33
 
30
34
  // Cleanup after each test
31
35
  afterEach(() => {
32
- // Clear localStorage
33
- localStorage.clear();
36
+ // Clear localStorage (may be unavailable in Node.js without --localstorage-file)
37
+ try {
38
+ localStorage.clear();
39
+ } catch {
40
+ // localStorage not available
41
+ }
34
42
 
35
43
  // Reset document body
36
44
  document.body.innerHTML = '';
@@ -8,11 +8,15 @@
8
8
  import {
9
9
  KTDataTableConfigInterface,
10
10
  KTDataTableCheckChangePayloadInterface,
11
- KTDataTableStateInterface,
12
11
  } from './types';
13
12
  import KTEventHandler from '../../helpers/event-handler';
14
13
  import { KTCallableType } from '../../types';
15
14
 
15
+ export interface KTDataTableCheckboxDeps {
16
+ getState: () => { selectedRows?: string[] };
17
+ setSelectedRows: (rows: string[]) => void;
18
+ }
19
+
16
20
  export interface KTDataTableCheckboxAPI {
17
21
  init(): void;
18
22
  check(): void;
@@ -21,241 +25,236 @@ export interface KTDataTableCheckboxAPI {
21
25
  isChecked(): boolean;
22
26
  getChecked(): string[];
23
27
  updateState(): void;
28
+ dispose(): void;
24
29
  }
25
30
 
26
- // Main function to create checkbox logic for a datatable instance
27
- export function createCheckboxHandler(
28
- element: HTMLElement,
29
- config: KTDataTableConfigInterface,
30
- fireEvent: (eventName: string, eventData?: object) => void,
31
- ): KTDataTableCheckboxAPI {
32
- let headerChecked = false;
33
- let headerCheckElement: HTMLInputElement | null = null;
34
- let targetElements: NodeListOf<HTMLInputElement> | null = null;
31
+ export class KTDataTableCheckboxHandler implements KTDataTableCheckboxAPI {
32
+ private _element: HTMLElement;
33
+ private _config: KTDataTableConfigInterface;
34
+ private _fireEvent: (eventName: string, eventData?: object) => void;
35
+ private _deps: KTDataTableCheckboxDeps;
36
+ private _headerChecked = false;
37
+ private _headerCheckElement: HTMLInputElement | null = null;
38
+ private _targetElements: NodeListOf<HTMLInputElement> | null = null;
39
+ private _delegatedEventId: string | null = null;
40
+ private readonly _preserveSelection: boolean;
35
41
 
36
- // Default: preserve selection across all pages
37
- const preserveSelection = config.checkbox?.preserveSelection !== false;
38
-
39
- function ensureState(): KTDataTableStateInterface {
40
- let state = config._state;
41
- if (!state) {
42
- state = {} as KTDataTableStateInterface;
43
- config._state = state;
44
- }
45
- return state;
42
+ constructor(
43
+ element: HTMLElement,
44
+ config: KTDataTableConfigInterface,
45
+ fireEvent: (eventName: string, eventData?: object) => void,
46
+ deps: KTDataTableCheckboxDeps,
47
+ ) {
48
+ this._element = element;
49
+ this._config = config;
50
+ this._fireEvent = fireEvent;
51
+ this._deps = deps;
52
+ this._preserveSelection = config.checkbox?.preserveSelection !== false;
46
53
  }
47
54
 
48
- // Helper: get selectedRows from state, always as string[]
49
- function getSelectedRows(): string[] {
50
- const state = ensureState();
51
- if (!Array.isArray(state.selectedRows)) state.selectedRows = [];
52
- return state.selectedRows.map(String);
55
+ private _checkboxListener = (event: MouseEvent) => {
56
+ this._checkboxToggle(event);
57
+ };
58
+
59
+ private _getSelectedRows(): string[] {
60
+ const rows = this._deps.getState().selectedRows;
61
+ return Array.isArray(rows) ? rows.map(String) : [];
53
62
  }
54
63
 
55
- // Helper: set selectedRows in state
56
- function setSelectedRows(rows: string[]) {
57
- const state = ensureState();
58
- state.selectedRows = Array.from(new Set(rows.map(String)));
64
+ private _setSelectedRows(rows: string[]) {
65
+ this._deps.setSelectedRows(Array.from(new Set(rows.map(String))));
59
66
  }
60
67
 
61
- // Helper: get all visible row IDs (values)
62
- function getVisibleRowIds(): string[] {
63
- if (!targetElements) return [];
64
- return Array.from(targetElements)
68
+ private _getVisibleRowIds(): string[] {
69
+ if (!this._targetElements) return [];
70
+ return Array.from(this._targetElements)
65
71
  .map((el) => el.value)
66
72
  .filter((v) => v != null && v !== '');
67
73
  }
68
74
 
69
- // Listener for header checkbox
70
- const checkboxListener = (event: MouseEvent) => {
71
- checkboxToggle(event);
72
- };
73
-
74
- function init() {
75
- const attrs = config.attributes;
75
+ public init() {
76
+ const attrs = this._config.attributes;
76
77
  if (!attrs?.check || !attrs.checkbox) {
77
78
  return;
78
79
  }
79
- headerCheckElement = element.querySelector<HTMLInputElement>(attrs.check);
80
- if (!headerCheckElement) return;
81
- headerChecked = headerCheckElement.checked;
82
- targetElements = element.querySelectorAll<HTMLInputElement>(attrs.checkbox);
83
- checkboxHandler();
84
- reapplyCheckedStates();
85
- updateHeaderCheckboxState();
80
+ this._headerCheckElement =
81
+ this._element.querySelector<HTMLInputElement>(attrs.check);
82
+ if (!this._headerCheckElement) return;
83
+ this._headerChecked = this._headerCheckElement.checked;
84
+ this._targetElements =
85
+ this._element.querySelectorAll<HTMLInputElement>(attrs.checkbox);
86
+ this._checkboxHandler();
87
+ this._reapplyCheckedStates();
88
+ this._updateHeaderCheckboxState();
86
89
  }
87
90
 
88
- function checkboxHandler() {
89
- if (!headerCheckElement) return;
90
- const rowCheckboxSelector = config.attributes?.checkbox;
91
+ private _checkboxHandler() {
92
+ if (!this._headerCheckElement) return;
93
+ const rowCheckboxSelector = this._config.attributes?.checkbox;
91
94
  if (!rowCheckboxSelector) return;
92
- headerCheckElement.addEventListener('click', checkboxListener);
93
- KTEventHandler.on(document.body, rowCheckboxSelector, 'input', ((
94
- event?: Event,
95
- ) => {
96
- if (event) handleRowCheckboxChange(event);
97
- }) as KTCallableType);
95
+ this._headerCheckElement.addEventListener('click', this._checkboxListener);
96
+ this._delegatedEventId = KTEventHandler.on(
97
+ this._element,
98
+ rowCheckboxSelector,
99
+ 'input',
100
+ ((event?: Event) => {
101
+ if (event) this._handleRowCheckboxChange(event);
102
+ }) as KTCallableType,
103
+ );
98
104
  }
99
105
 
100
- // When a row checkbox is changed
101
- function handleRowCheckboxChange(event: Event) {
106
+ private _handleRowCheckboxChange(event: Event) {
102
107
  const input = event.target as HTMLInputElement;
103
108
  if (!input) return;
104
109
  const value = input.value;
105
- let selectedRows = getSelectedRows();
110
+ let selectedRows = this._getSelectedRows();
106
111
  const wasChecked = selectedRows.includes(value);
107
112
  const isNowChecked = input.checked;
108
113
 
109
- // Update state first
110
114
  if (isNowChecked) {
111
115
  if (!wasChecked) selectedRows.push(value);
112
116
  } else {
113
117
  selectedRows = selectedRows.filter((v) => v !== value);
114
118
  }
115
- setSelectedRows(selectedRows);
116
- updateHeaderCheckboxState();
119
+ this._setSelectedRows(selectedRows);
120
+ this._updateHeaderCheckboxState();
117
121
 
118
- // Fire specific checked/unchecked events after state is updated
119
122
  if (isNowChecked && !wasChecked) {
120
- fireEvent('checked');
123
+ this._fireEvent('checked', { value });
121
124
  } else if (!isNowChecked && wasChecked) {
122
- fireEvent('unchecked');
125
+ this._fireEvent('unchecked', { value });
123
126
  }
124
127
 
125
- // Always fire changed event for backward compatibility
126
- fireEvent('changed');
128
+ this._fireEvent('changed');
127
129
  }
128
130
 
129
- // When the header checkbox is toggled
130
- function checkboxToggle(_event?: Event) {
131
- const checked = !isChecked();
132
- // Update state first, then fire events
133
- change(checked);
134
- // Fire checked/unchecked events after state is updated
131
+ private _checkboxToggle(_event?: Event) {
132
+ const checked = !this.isChecked();
133
+ this._change(checked);
135
134
  const eventType = checked ? 'checked' : 'unchecked';
136
- fireEvent(eventType);
135
+ this._fireEvent(eventType);
137
136
  }
138
137
 
139
- // Change all visible checkboxes and update selectedRows
140
- function change(checked: boolean) {
138
+ private _change(checked: boolean) {
141
139
  const payload: KTDataTableCheckChangePayloadInterface = { cancel: false };
142
- fireEvent('change', payload);
140
+ this._fireEvent('change', payload);
143
141
  if (payload.cancel === true) return;
144
- headerChecked = checked;
145
- if (headerCheckElement) headerCheckElement.checked = checked;
146
- if (targetElements) {
147
- const visibleIds = getVisibleRowIds();
148
- let selectedRows = getSelectedRows();
142
+ this._headerChecked = checked;
143
+ if (this._headerCheckElement) this._headerCheckElement.checked = checked;
144
+ if (this._targetElements) {
145
+ const visibleIds = this._getVisibleRowIds();
146
+ let selectedRows = this._getSelectedRows();
149
147
  if (checked) {
150
- // Add all visible IDs to selectedRows
151
- selectedRows = preserveSelection
148
+ selectedRows = this._preserveSelection
152
149
  ? Array.from(new Set([...selectedRows, ...visibleIds]))
153
150
  : visibleIds;
154
151
  } else {
155
- // Remove all visible IDs from selectedRows
156
- selectedRows = preserveSelection
152
+ selectedRows = this._preserveSelection
157
153
  ? selectedRows.filter((v) => !visibleIds.includes(v))
158
154
  : [];
159
155
  }
160
- setSelectedRows(selectedRows);
161
- // Update visible checkboxes
162
- targetElements.forEach((element: HTMLInputElement) => {
156
+ this._setSelectedRows(selectedRows);
157
+ this._targetElements.forEach((element: HTMLInputElement) => {
163
158
  if (element) {
164
159
  element.checked = checked;
165
160
  }
166
161
  });
167
162
  }
168
- updateHeaderCheckboxState();
169
- fireEvent('changed');
163
+ this._updateHeaderCheckboxState();
164
+ this._fireEvent('changed');
170
165
  }
171
166
 
172
- // Reapply checked state to visible checkboxes based on selectedRows
173
- function reapplyCheckedStates() {
174
- const selectedRows = getSelectedRows();
175
- if (!targetElements) return;
176
- targetElements.forEach((element: HTMLInputElement) => {
167
+ private _reapplyCheckedStates() {
168
+ const selectedRows = this._getSelectedRows();
169
+ if (!this._targetElements) return;
170
+ this._targetElements.forEach((element: HTMLInputElement) => {
177
171
  if (!element) return;
178
172
  const value = element.value;
179
173
  element.checked = selectedRows.includes(value);
180
- // Update row class
181
174
  const row = element.closest('tr');
182
- if (row && config.checkbox?.checkedClass) {
175
+ if (row && this._config.checkbox?.checkedClass) {
183
176
  if (element.checked) {
184
- row.classList.add(config.checkbox.checkedClass);
177
+ row.classList.add(this._config.checkbox.checkedClass);
185
178
  } else {
186
- row.classList.remove(config.checkbox.checkedClass);
179
+ row.classList.remove(this._config.checkbox.checkedClass);
187
180
  }
188
181
  }
189
182
  });
190
183
  }
191
184
 
192
- // Update header checkbox state (checked/indeterminate/unchecked)
193
- function updateHeaderCheckboxState() {
194
- if (!headerCheckElement || !targetElements) return;
195
- const total = targetElements.length;
185
+ private _updateHeaderCheckboxState() {
186
+ if (!this._headerCheckElement || !this._targetElements) return;
187
+ const total = this._targetElements.length;
196
188
  let checked = 0;
197
189
  for (let i = 0; i < total; i++) {
198
- if (targetElements[i].checked) checked++;
190
+ if (this._targetElements[i].checked) checked++;
199
191
  }
200
192
  if (checked === 0) {
201
- headerCheckElement.indeterminate = false;
202
- headerCheckElement.checked = false;
203
- headerChecked = false;
193
+ this._headerCheckElement.indeterminate = false;
194
+ this._headerCheckElement.checked = false;
195
+ this._headerChecked = false;
204
196
  } else if (checked > 0 && checked < total) {
205
- headerCheckElement.indeterminate = true;
206
- headerCheckElement.checked = false;
207
- headerChecked = false;
197
+ this._headerCheckElement.indeterminate = true;
198
+ this._headerCheckElement.checked = false;
199
+ this._headerChecked = false;
208
200
  } else if (checked === total) {
209
- headerCheckElement.indeterminate = false;
210
- headerCheckElement.checked = true;
211
- headerChecked = true;
201
+ this._headerCheckElement.indeterminate = false;
202
+ this._headerCheckElement.checked = true;
203
+ this._headerChecked = true;
212
204
  }
213
205
  }
214
206
 
215
- // Fix: isChecked() implementation
216
- function isChecked(): boolean {
217
- return headerChecked;
207
+ public isChecked(): boolean {
208
+ return this._headerChecked;
218
209
  }
219
210
 
220
- function getChecked(): string[] {
221
- return getSelectedRows();
211
+ public getChecked(): string[] {
212
+ return this._getSelectedRows();
222
213
  }
223
214
 
224
- function check() {
225
- change(true);
226
- reapplyCheckedStates();
227
- updateHeaderCheckboxState();
215
+ public check() {
216
+ this._change(true);
217
+ this._reapplyCheckedStates();
218
+ this._updateHeaderCheckboxState();
228
219
  }
229
220
 
230
- function uncheck() {
231
- change(false);
232
- reapplyCheckedStates();
233
- updateHeaderCheckboxState();
221
+ public uncheck() {
222
+ this._change(false);
223
+ this._reapplyCheckedStates();
224
+ this._updateHeaderCheckboxState();
234
225
  }
235
226
 
236
- function toggle() {
237
- checkboxToggle();
238
- reapplyCheckedStates();
239
- updateHeaderCheckboxState();
227
+ public toggle() {
228
+ this._checkboxToggle();
229
+ this._reapplyCheckedStates();
230
+ this._updateHeaderCheckboxState();
240
231
  }
241
232
 
242
- function updateState() {
243
- const rowCheckSel = config.attributes?.checkbox;
233
+ public updateState() {
234
+ const rowCheckSel = this._config.attributes?.checkbox;
244
235
  if (!rowCheckSel) {
245
236
  return;
246
237
  }
247
- targetElements = element.querySelectorAll<HTMLInputElement>(rowCheckSel);
248
- reapplyCheckedStates();
249
- updateHeaderCheckboxState();
238
+ this._targetElements =
239
+ this._element.querySelectorAll<HTMLInputElement>(rowCheckSel);
240
+ this._reapplyCheckedStates();
241
+ this._updateHeaderCheckboxState();
250
242
  }
251
243
 
252
- return {
253
- init,
254
- check,
255
- uncheck,
256
- toggle,
257
- isChecked,
258
- getChecked,
259
- updateState,
260
- };
244
+ public dispose() {
245
+ if (this._headerCheckElement) {
246
+ this._headerCheckElement.removeEventListener(
247
+ 'click',
248
+ this._checkboxListener,
249
+ );
250
+ }
251
+ const rowCheckboxSelector = this._config.attributes?.checkbox;
252
+ if (this._delegatedEventId && rowCheckboxSelector) {
253
+ KTEventHandler.off(this._element, 'input', this._delegatedEventId);
254
+ this._delegatedEventId = null;
255
+ }
256
+ this._headerCheckElement = null;
257
+ this._targetElements = null;
258
+ }
261
259
  }
260
+