@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
@@ -0,0 +1,97 @@
1
+ /**
2
+ * KTUI - Free & Open-Source Tailwind UI Components by Keenthemes
3
+ * Copyright 2025 by Keenthemes Inc
4
+ */
5
+
6
+ /**
7
+ * Search binding utilities for KTDataTable.
8
+ * Manages debounced search input binding and cleanup.
9
+ */
10
+
11
+ type SearchElementWithDebounce = HTMLInputElement & {
12
+ _debouncedSearch?: EventListener;
13
+ };
14
+
15
+ export interface KTDataTableSearchHandler {
16
+ attach(
17
+ tableId: string,
18
+ currentSearch: string | object | undefined,
19
+ delay: number,
20
+ onSearch: (query: string) => void,
21
+ ): void;
22
+ detach(tableId: string): void;
23
+ }
24
+
25
+ export function createSearchHandler(): KTDataTableSearchHandler {
26
+ function findSearchElement(tableId: string): HTMLInputElement | null {
27
+ return document.querySelector<HTMLInputElement>(
28
+ `[data-kt-datatable-search="#${tableId}"]`,
29
+ );
30
+ }
31
+
32
+ function asSearchElementWithDebounce(
33
+ element: HTMLInputElement,
34
+ ): SearchElementWithDebounce {
35
+ return element as SearchElementWithDebounce;
36
+ }
37
+
38
+ function debounce<TArgs extends unknown[]>(
39
+ func: (...args: TArgs) => void,
40
+ wait: number,
41
+ ): (...args: TArgs) => void {
42
+ let timeout: number | undefined;
43
+ return function (...args: TArgs) {
44
+ const later = () => {
45
+ clearTimeout(timeout);
46
+ func(...args);
47
+ };
48
+ clearTimeout(timeout);
49
+ timeout = window.setTimeout(later, wait);
50
+ };
51
+ }
52
+
53
+ function attach(
54
+ tableId: string,
55
+ currentSearch: string | object | undefined,
56
+ delay: number,
57
+ onSearch: (query: string) => void,
58
+ ): void {
59
+ const searchElement = findSearchElement(tableId);
60
+ if (!searchElement) return;
61
+
62
+ // Restore search value from state
63
+ if (currentSearch !== undefined && currentSearch !== null) {
64
+ searchElement.value =
65
+ typeof currentSearch === 'string'
66
+ ? currentSearch
67
+ : String(currentSearch);
68
+ }
69
+
70
+ // Remove existing debounced listener if any
71
+ const el = asSearchElementWithDebounce(searchElement);
72
+ if (el._debouncedSearch) {
73
+ searchElement.removeEventListener('keyup', el._debouncedSearch);
74
+ }
75
+
76
+ // Create and attach new debounced search
77
+ const debouncedSearch = debounce(() => {
78
+ onSearch(searchElement.value);
79
+ }, delay);
80
+
81
+ el._debouncedSearch = debouncedSearch;
82
+ searchElement.addEventListener('keyup', debouncedSearch);
83
+ }
84
+
85
+ function detach(tableId: string): void {
86
+ const searchElement = findSearchElement(tableId);
87
+ if (!searchElement) return;
88
+
89
+ const el = asSearchElementWithDebounce(searchElement);
90
+ if (el._debouncedSearch) {
91
+ searchElement.removeEventListener('keyup', el._debouncedSearch);
92
+ delete el._debouncedSearch;
93
+ }
94
+ }
95
+
96
+ return { attach, detach };
97
+ }
@@ -10,6 +10,19 @@ import {
10
10
  KTDataTableSortOrderInterface,
11
11
  KTDataTableDataInterface,
12
12
  } from './types';
13
+ import { stripHtml } from './datatable-utils';
14
+
15
+ export interface KTDataTableSortHandlerDeps<T> {
16
+ config: KTDataTableConfigInterface;
17
+ theadElement: HTMLTableSectionElement;
18
+ getState: () => {
19
+ sortField: keyof T | number;
20
+ sortOrder: KTDataTableSortOrderInterface;
21
+ };
22
+ setState: (field: keyof T | number, order: KTDataTableSortOrderInterface) => void;
23
+ emit: (eventName: string, eventData?: object) => void;
24
+ updateData: () => void;
25
+ }
13
26
 
14
27
  export interface KTDataTableSortAPI<T = KTDataTableDataInterface> {
15
28
  initSort(): void;
@@ -27,31 +40,42 @@ export interface KTDataTableSortAPI<T = KTDataTableDataInterface> {
27
40
  sortField: keyof T,
28
41
  sortOrder: KTDataTableSortOrderInterface,
29
42
  ): void;
43
+ dispose(): void;
30
44
  }
31
45
 
32
- export function createSortHandler<T = KTDataTableDataInterface>(
33
- config: KTDataTableConfigInterface,
34
- theadElement: HTMLTableSectionElement,
35
- getState: () => {
46
+ export class KTDataTableSortHandler<T = KTDataTableDataInterface>
47
+ implements KTDataTableSortAPI<T>
48
+ {
49
+ private _config: KTDataTableConfigInterface;
50
+ private _theadElement: HTMLTableSectionElement;
51
+ private _getState: () => {
36
52
  sortField: keyof T | number;
37
53
  sortOrder: KTDataTableSortOrderInterface;
38
- },
39
- setState: (
54
+ };
55
+ private _setState: (
40
56
  field: keyof T | number,
41
57
  order: KTDataTableSortOrderInterface,
42
- ) => void,
43
- fireEvent: (eventName: string, eventData?: object) => void,
44
- dispatchEvent: (eventName: string, eventData?: object) => void,
45
- updateData: () => void,
46
- ): KTDataTableSortAPI<T> {
47
- // Helper to compare values for sorting (string)
48
- function compareValues(
58
+ ) => void;
59
+ private _emit: (eventName: string, eventData?: object) => void;
60
+ private _updateData: () => void;
61
+ private _sortAbortController: AbortController | null = null;
62
+
63
+ constructor(deps: KTDataTableSortHandlerDeps<T>) {
64
+ this._config = deps.config;
65
+ this._theadElement = deps.theadElement;
66
+ this._getState = deps.getState;
67
+ this._setState = deps.setState;
68
+ this._emit = deps.emit;
69
+ this._updateData = deps.updateData;
70
+ }
71
+
72
+ private static _compareValues(
49
73
  a: unknown,
50
74
  b: unknown,
51
75
  sortOrder: KTDataTableSortOrderInterface,
52
76
  ): number {
53
- const aText = String(a).replace(/<[^>]*>|&nbsp;/g, '');
54
- const bText = String(b).replace(/<[^>]*>|&nbsp;/g, '');
77
+ const aText = stripHtml(a);
78
+ const bText = stripHtml(b);
55
79
  return aText > bText
56
80
  ? sortOrder === 'asc'
57
81
  ? 1
@@ -63,8 +87,7 @@ export function createSortHandler<T = KTDataTableDataInterface>(
63
87
  : 0;
64
88
  }
65
89
 
66
- // Parse value for numeric sort: strip currency/commas, then parseFloat
67
- function parseNumeric(value: unknown): number {
90
+ private static _parseNumeric(value: unknown): number {
68
91
  if (value === null || value === undefined || value === '') {
69
92
  return Number.NaN;
70
93
  }
@@ -73,8 +96,7 @@ export function createSortHandler<T = KTDataTableDataInterface>(
73
96
  return Number.isNaN(n) ? Number.NaN : n;
74
97
  }
75
98
 
76
- // Compare two numbers; NaN sorts to the end for both asc and desc
77
- function compareNumeric(
99
+ private static _compareNumeric(
78
100
  aNum: number,
79
101
  bNum: number,
80
102
  sortOrder: KTDataTableSortOrderInterface,
@@ -89,7 +111,7 @@ export function createSortHandler<T = KTDataTableDataInterface>(
89
111
  return 0;
90
112
  }
91
113
 
92
- function getColumnDef(sortField: keyof T | number):
114
+ private _getColumnDef(sortField: keyof T | number):
93
115
  | {
94
116
  sortType?: 'string' | 'numeric';
95
117
  sortValue?: (
@@ -100,7 +122,7 @@ export function createSortHandler<T = KTDataTableDataInterface>(
100
122
  ) => number | string;
101
123
  }
102
124
  | undefined {
103
- const columns = config.columns;
125
+ const columns = this._config.columns;
104
126
  if (!columns) return undefined;
105
127
  const key =
106
128
  typeof sortField === 'number'
@@ -109,15 +131,25 @@ export function createSortHandler<T = KTDataTableDataInterface>(
109
131
  return key !== undefined ? columns[key as string] : undefined;
110
132
  }
111
133
 
112
- function sortData(
134
+ public sortData(
113
135
  data: T[],
114
136
  sortField: keyof T | number,
115
137
  sortOrder: KTDataTableSortOrderInterface,
116
138
  ): T[] {
117
- const columnDef = getColumnDef(sortField);
139
+ const columnDef = this._getColumnDef(sortField);
118
140
  const sortValueFn = columnDef?.sortValue;
119
141
  const useNumeric = !sortValueFn && columnDef?.sortType === 'numeric';
120
142
 
143
+ // Pre-strip HTML from cell values once (instead of on every comparison).
144
+ // For N rows this runs N regex replacements instead of N*log(N).
145
+ const strippedCache = new Map<T, string>();
146
+
147
+ if (!sortValueFn) {
148
+ for (const item of data) {
149
+ strippedCache.set(item, stripHtml(item[sortField as keyof T]));
150
+ }
151
+ }
152
+
121
153
  return data.sort((a, b) => {
122
154
  const aRaw = a[sortField as keyof T] as unknown;
123
155
  const bRaw = b[sortField as keyof T] as unknown;
@@ -135,31 +167,33 @@ export function createSortHandler<T = KTDataTableDataInterface>(
135
167
  | string,
136
168
  b as KTDataTableDataInterface,
137
169
  );
138
- const aNum = typeof aVal === 'number' ? aVal : parseNumeric(aVal);
139
- const bNum = typeof bVal === 'number' ? bVal : parseNumeric(bVal);
170
+ const aNum =
171
+ typeof aVal === 'number'
172
+ ? aVal
173
+ : KTDataTableSortHandler._parseNumeric(aVal);
174
+ const bNum =
175
+ typeof bVal === 'number'
176
+ ? bVal
177
+ : KTDataTableSortHandler._parseNumeric(bVal);
140
178
  if (typeof aVal === 'number' && typeof bVal === 'number') {
141
- return compareNumeric(aNum, bNum, sortOrder);
179
+ return KTDataTableSortHandler._compareNumeric(aNum, bNum, sortOrder);
142
180
  }
143
- return compareValues(aVal, bVal, sortOrder);
181
+ return KTDataTableSortHandler._compareValues(aVal, bVal, sortOrder);
144
182
  }
145
183
  if (useNumeric) {
146
- const aNum = parseNumeric(
147
- aRaw as
148
- | KTDataTableDataInterface[keyof KTDataTableDataInterface]
149
- | string,
150
- );
151
- const bNum = parseNumeric(
152
- bRaw as
153
- | KTDataTableDataInterface[keyof KTDataTableDataInterface]
154
- | string,
155
- );
156
- return compareNumeric(aNum, bNum, sortOrder);
184
+ const aNum = KTDataTableSortHandler._parseNumeric(strippedCache.get(a) ?? aRaw);
185
+ const bNum = KTDataTableSortHandler._parseNumeric(strippedCache.get(b) ?? bRaw);
186
+ return KTDataTableSortHandler._compareNumeric(aNum, bNum, sortOrder);
157
187
  }
158
- return compareValues(aRaw, bRaw, sortOrder);
188
+ return KTDataTableSortHandler._compareValues(
189
+ strippedCache.get(a) ?? aRaw,
190
+ strippedCache.get(b) ?? bRaw,
191
+ sortOrder,
192
+ );
159
193
  });
160
194
  }
161
195
 
162
- function toggleSortOrder(
196
+ public toggleSortOrder(
163
197
  currentField: keyof T | number,
164
198
  currentOrder: KTDataTableSortOrderInterface,
165
199
  newField: keyof T | number,
@@ -177,18 +211,17 @@ export function createSortHandler<T = KTDataTableDataInterface>(
177
211
  return 'asc';
178
212
  }
179
213
 
180
- function setSortIcon(
214
+ public setSortIcon(
181
215
  sortField: keyof T,
182
216
  sortOrder: KTDataTableSortOrderInterface,
183
217
  ): void {
184
- const baseClass = config.sort?.classes?.base || '';
218
+ const baseClass = this._config.sort?.classes?.base || '';
185
219
  const sortClass = sortOrder
186
220
  ? sortOrder === 'asc'
187
- ? config.sort?.classes?.asc || ''
188
- : config.sort?.classes?.desc || ''
221
+ ? this._config.sort?.classes?.asc || ''
222
+ : this._config.sort?.classes?.desc || ''
189
223
  : '';
190
- // Clear all headers: remove sort state so only the active column shows highlighted arrow
191
- const allTh = theadElement.querySelectorAll('th');
224
+ const allTh = this._theadElement.querySelectorAll('th');
192
225
  allTh.forEach((header) => {
193
226
  const el = header as HTMLElement;
194
227
  el.setAttribute('aria-sort', 'none');
@@ -197,11 +230,10 @@ export function createSortHandler<T = KTDataTableDataInterface>(
197
230
  sortElement.className = baseClass;
198
231
  }
199
232
  });
200
- // Apply sort state to the active column so table.css [aria-sort='asc'] / [aria-sort='desc'] can highlight the arrow
201
233
  const th =
202
234
  typeof sortField === 'number'
203
235
  ? allTh[sortField]
204
- : (theadElement.querySelector(
236
+ : (this._theadElement.querySelector(
205
237
  `th[data-kt-datatable-column="${String(sortField)}"], th[data-kt-datatable-column-sort="${String(sortField)}"]`,
206
238
  ) as HTMLElement);
207
239
  if (th) {
@@ -217,17 +249,25 @@ export function createSortHandler<T = KTDataTableDataInterface>(
217
249
  }
218
250
  }
219
251
 
220
- function initSort(): void {
221
- if (!theadElement) return;
222
- // Set the initial sort icon
223
- setSortIcon(getState().sortField as keyof T, getState().sortOrder);
224
- // Get all the table headers
225
- const headers = Array.from(theadElement.querySelectorAll('th'));
252
+ public initSort(): void {
253
+ if (!this._theadElement) return;
254
+
255
+ // Abort previous sort listeners before attaching new ones
256
+ if (this._sortAbortController) {
257
+ this._sortAbortController.abort();
258
+ }
259
+ this._sortAbortController = new AbortController();
260
+ const signal = this._sortAbortController.signal;
261
+
262
+ this.setSortIcon(
263
+ this._getState().sortField as keyof T,
264
+ this._getState().sortOrder,
265
+ );
266
+ const headers = Array.from(this._theadElement.querySelectorAll('th'));
226
267
  headers.forEach((header) => {
227
- // If the sort class is not found, it's not a sortable column
228
- if (!header.querySelector(`.${config.sort?.classes?.base}`)) return;
268
+ if (!header.querySelector(`.${this._config.sort?.classes?.base}`))
269
+ return;
229
270
 
230
- // Check if sorting is disabled for this column
231
271
  const sortDisabled =
232
272
  header.getAttribute('data-kt-datatable-column-sort') === 'false';
233
273
  if (sortDisabled) return;
@@ -239,20 +279,25 @@ export function createSortHandler<T = KTDataTableDataInterface>(
239
279
  ? (sortAttribute as keyof T)
240
280
  : (header.cellIndex as keyof T);
241
281
  header.addEventListener('click', () => {
242
- const state = getState();
243
- const sortOrder = toggleSortOrder(
282
+ const state = this._getState();
283
+ const sortOrder = this.toggleSortOrder(
244
284
  state.sortField,
245
285
  state.sortOrder,
246
286
  sortField,
247
287
  );
248
- setSortIcon(sortField, sortOrder);
249
- setState(sortField, sortOrder);
250
- fireEvent('sort', { field: sortField, order: sortOrder });
251
- dispatchEvent('sort', { field: sortField, order: sortOrder });
252
- updateData();
253
- });
288
+ this.setSortIcon(sortField, sortOrder);
289
+ this._setState(sortField, sortOrder);
290
+ this._emit('sort', { field: sortField, order: sortOrder });
291
+ this._updateData();
292
+ }, { signal });
254
293
  });
255
294
  }
256
295
 
257
- return { initSort, sortData, toggleSortOrder, setSortIcon };
296
+ public dispose(): void {
297
+ if (this._sortAbortController) {
298
+ this._sortAbortController.abort();
299
+ this._sortAbortController = null;
300
+ }
301
+ }
258
302
  }
303
+
@@ -0,0 +1,103 @@
1
+ /**
2
+ * KTUI - Free & Open-Source Tailwind UI Components by Keenthemes
3
+ * Copyright 2025 by Keenthemes Inc
4
+ */
5
+
6
+ export interface KTDataTableSpinner {
7
+ show(
8
+ root: HTMLElement | null,
9
+ config: {
10
+ attributes?: { spinner?: string };
11
+ loadingClass?: string;
12
+ loading?: { template: string; content: string };
13
+ },
14
+ tableElement: HTMLTableElement,
15
+ ): void;
16
+ hide(
17
+ root: HTMLElement | null,
18
+ config: {
19
+ attributes?: { spinner?: string };
20
+ loadingClass?: string;
21
+ },
22
+ ): void;
23
+ remove(
24
+ root: HTMLElement | null,
25
+ config: {
26
+ attributes?: { spinner?: string };
27
+ loadingClass?: string;
28
+ },
29
+ ): void;
30
+ }
31
+
32
+ export function createSpinner(): KTDataTableSpinner {
33
+ function findSpinner(
34
+ root: HTMLElement | null,
35
+ spinnerSel: string | undefined,
36
+ ): HTMLElement | null {
37
+ return root && spinnerSel
38
+ ? root.querySelector<HTMLElement>(spinnerSel)
39
+ : null;
40
+ }
41
+
42
+ function createSpinnerElement(
43
+ tableElement: HTMLTableElement,
44
+ loading: { template: string; content: string },
45
+ ): HTMLElement | null {
46
+ const template = document.createElement('template');
47
+ template.innerHTML = loading.template
48
+ .trim()
49
+ .replace('{content}', loading.content);
50
+ const first = template.content.firstChild;
51
+ if (!first || !(first instanceof HTMLElement)) return null;
52
+ const spinner = first;
53
+ spinner.setAttribute('data-kt-datatable-spinner', 'true');
54
+ tableElement.appendChild(spinner);
55
+ return spinner;
56
+ }
57
+
58
+ function show(
59
+ root: HTMLElement | null,
60
+ config: {
61
+ attributes?: { spinner?: string };
62
+ loadingClass?: string;
63
+ loading?: { template: string; content: string };
64
+ },
65
+ tableElement: HTMLTableElement,
66
+ ): void {
67
+ const spinnerSel = config.attributes?.spinner;
68
+ const fromDom = findSpinner(root, spinnerSel);
69
+ const spinner =
70
+ fromDom ??
71
+ (config.loading
72
+ ? createSpinnerElement(tableElement, config.loading)
73
+ : null);
74
+ if (spinner) spinner.style.display = 'block';
75
+ root?.classList.add(config.loadingClass ?? 'loading');
76
+ }
77
+
78
+ function hide(
79
+ root: HTMLElement | null,
80
+ config: {
81
+ attributes?: { spinner?: string };
82
+ loadingClass?: string;
83
+ },
84
+ ): void {
85
+ const spinner = findSpinner(root, config.attributes?.spinner);
86
+ if (spinner) spinner.style.display = 'none';
87
+ root?.classList.remove(config.loadingClass ?? 'loading');
88
+ }
89
+
90
+ function remove(
91
+ root: HTMLElement | null,
92
+ config: {
93
+ attributes?: { spinner?: string };
94
+ loadingClass?: string;
95
+ },
96
+ ): void {
97
+ const spinner = findSpinner(root, config.attributes?.spinner);
98
+ if (spinner?.parentNode) spinner.parentNode.removeChild(spinner);
99
+ root?.classList.remove(config.loadingClass ?? 'loading');
100
+ }
101
+
102
+ return { show, hide, remove };
103
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * KTUI - Free & Open-Source Tailwind UI Components by Keenthemes
3
+ * Copyright 2025 by Keenthemes Inc
4
+ */
5
+
6
+ import { KTDataTableStateInterface } from './types';
7
+
8
+ export interface KTDataTableStatePersistence {
9
+ save(namespace: string, state: KTDataTableStateInterface): void;
10
+ load(namespace: string): KTDataTableStateInterface | null;
11
+ remove(namespace: string): void;
12
+ }
13
+
14
+ export function createStatePersistence(): KTDataTableStatePersistence {
15
+ function save(namespace: string, state: KTDataTableStateInterface): void {
16
+ if (namespace) {
17
+ try {
18
+ localStorage.setItem(namespace, JSON.stringify(state));
19
+ } catch {
20
+ // localStorage unavailable (e.g. Node.js without --localstorage-file)
21
+ }
22
+ }
23
+ }
24
+
25
+ function load(namespace: string): KTDataTableStateInterface | null {
26
+ try {
27
+ const stateString = localStorage.getItem(namespace);
28
+ if (!stateString) return null;
29
+
30
+ return JSON.parse(stateString) as KTDataTableStateInterface;
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+
36
+ function remove(namespace: string): void {
37
+ if (namespace) {
38
+ try {
39
+ localStorage.removeItem(namespace);
40
+ } catch {
41
+ // localStorage unavailable
42
+ }
43
+ }
44
+ }
45
+
46
+ return { save, load, remove };
47
+ }
48
+
49
+ /**
50
+ * Resolve the namespace for a datatable state key.
51
+ * Priority: config.stateNamespace > table element ID > root element ID > fallback name.
52
+ */
53
+ export function resolveTableNamespace(
54
+ config: { stateNamespace?: string },
55
+ tableElement: HTMLTableElement | null,
56
+ rootElement: HTMLElement | null,
57
+ fallbackName: string,
58
+ ): string {
59
+ if (config.stateNamespace) {
60
+ return config.stateNamespace;
61
+ }
62
+ const tableIdAttr = tableElement?.getAttribute('id');
63
+ if (tableIdAttr) return tableIdAttr;
64
+ const rootIdAttr = rootElement?.getAttribute('id');
65
+ if (rootIdAttr) return rootIdAttr;
66
+ return fallbackName;
67
+ }