@keenthemes/ktui 1.2.5 → 1.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/README.md +14 -5
  2. package/dist/ktui.js +1538 -786
  3. package/dist/ktui.min.js +1 -1
  4. package/dist/ktui.min.js.map +1 -1
  5. package/dist/styles.css +85 -5
  6. package/lib/cjs/components/datatable/datatable-checkbox.d.ts +37 -1
  7. package/lib/cjs/components/datatable/datatable-checkbox.d.ts.map +1 -1
  8. package/lib/cjs/components/datatable/datatable-checkbox.js +143 -156
  9. package/lib/cjs/components/datatable/datatable-checkbox.js.map +1 -1
  10. package/lib/cjs/components/datatable/datatable-column-utils.d.ts +30 -0
  11. package/lib/cjs/components/datatable/datatable-column-utils.d.ts.map +1 -0
  12. package/lib/cjs/components/datatable/datatable-column-utils.js +42 -0
  13. package/lib/cjs/components/datatable/datatable-column-utils.js.map +1 -0
  14. package/lib/cjs/components/datatable/datatable-contracts.d.ts +2 -4
  15. package/lib/cjs/components/datatable/datatable-contracts.d.ts.map +1 -1
  16. package/lib/cjs/components/datatable/datatable-defaults.d.ts +20 -0
  17. package/lib/cjs/components/datatable/datatable-defaults.d.ts.map +1 -0
  18. package/lib/cjs/components/datatable/datatable-defaults.js +193 -0
  19. package/lib/cjs/components/datatable/datatable-defaults.js.map +1 -0
  20. package/lib/cjs/components/datatable/datatable-layout-plugin.d.ts +7 -0
  21. package/lib/cjs/components/datatable/datatable-layout-plugin.d.ts.map +1 -0
  22. package/lib/cjs/components/datatable/datatable-layout-plugin.js +338 -0
  23. package/lib/cjs/components/datatable/datatable-layout-plugin.js.map +1 -0
  24. package/lib/cjs/components/datatable/datatable-local-provider.d.ts +2 -2
  25. package/lib/cjs/components/datatable/datatable-local-provider.d.ts.map +1 -1
  26. package/lib/cjs/components/datatable/datatable-local-provider.js +85 -27
  27. package/lib/cjs/components/datatable/datatable-local-provider.js.map +1 -1
  28. package/lib/cjs/components/datatable/datatable-pagination-renderer.d.ts.map +1 -1
  29. package/lib/cjs/components/datatable/datatable-pagination-renderer.js +13 -13
  30. package/lib/cjs/components/datatable/datatable-pagination-renderer.js.map +1 -1
  31. package/lib/cjs/components/datatable/datatable-registry.d.ts +18 -0
  32. package/lib/cjs/components/datatable/datatable-registry.d.ts.map +1 -0
  33. package/lib/cjs/components/datatable/datatable-registry.js +66 -0
  34. package/lib/cjs/components/datatable/datatable-registry.js.map +1 -0
  35. package/lib/cjs/components/datatable/datatable-remote-provider.d.ts.map +1 -1
  36. package/lib/cjs/components/datatable/datatable-remote-provider.js +1 -2
  37. package/lib/cjs/components/datatable/datatable-remote-provider.js.map +1 -1
  38. package/lib/cjs/components/datatable/datatable-search-handler.d.ts +10 -0
  39. package/lib/cjs/components/datatable/datatable-search-handler.d.ts.map +1 -0
  40. package/lib/cjs/components/datatable/datatable-search-handler.js +65 -0
  41. package/lib/cjs/components/datatable/datatable-search-handler.js.map +1 -0
  42. package/lib/cjs/components/datatable/datatable-sort.d.ts +31 -4
  43. package/lib/cjs/components/datatable/datatable-sort.d.ts.map +1 -1
  44. package/lib/cjs/components/datatable/datatable-sort.js +86 -58
  45. package/lib/cjs/components/datatable/datatable-sort.js.map +1 -1
  46. package/lib/cjs/components/datatable/datatable-spinner.d.ts +30 -0
  47. package/lib/cjs/components/datatable/datatable-spinner.d.ts.map +1 -0
  48. package/lib/cjs/components/datatable/datatable-spinner.js +54 -0
  49. package/lib/cjs/components/datatable/datatable-spinner.js.map +1 -0
  50. package/lib/cjs/components/datatable/datatable-state-persistence.d.ts +19 -0
  51. package/lib/cjs/components/datatable/datatable-state-persistence.d.ts.map +1 -0
  52. package/lib/cjs/components/datatable/datatable-state-persistence.js +59 -0
  53. package/lib/cjs/components/datatable/datatable-state-persistence.js.map +1 -0
  54. package/lib/cjs/components/datatable/datatable-table-renderer.d.ts +2 -0
  55. package/lib/cjs/components/datatable/datatable-table-renderer.d.ts.map +1 -1
  56. package/lib/cjs/components/datatable/datatable-table-renderer.js +75 -16
  57. package/lib/cjs/components/datatable/datatable-table-renderer.js.map +1 -1
  58. package/lib/cjs/components/datatable/datatable-utils.d.ts +10 -0
  59. package/lib/cjs/components/datatable/datatable-utils.d.ts.map +1 -0
  60. package/lib/cjs/components/datatable/datatable-utils.js +15 -0
  61. package/lib/cjs/components/datatable/datatable-utils.js.map +1 -0
  62. package/lib/cjs/components/datatable/datatable.d.ts +35 -34
  63. package/lib/cjs/components/datatable/datatable.d.ts.map +1 -1
  64. package/lib/cjs/components/datatable/datatable.js +233 -497
  65. package/lib/cjs/components/datatable/datatable.js.map +1 -1
  66. package/lib/cjs/components/datatable/index.d.ts +1 -1
  67. package/lib/cjs/components/datatable/index.d.ts.map +1 -1
  68. package/lib/cjs/components/datatable/types.d.ts +127 -11
  69. package/lib/cjs/components/datatable/types.d.ts.map +1 -1
  70. package/lib/cjs/index.d.ts +1 -1
  71. package/lib/cjs/index.d.ts.map +1 -1
  72. package/lib/cjs/index.js +6 -0
  73. package/lib/cjs/index.js.map +1 -1
  74. package/lib/esm/components/datatable/datatable-checkbox.d.ts +37 -1
  75. package/lib/esm/components/datatable/datatable-checkbox.d.ts.map +1 -1
  76. package/lib/esm/components/datatable/datatable-checkbox.js +142 -155
  77. package/lib/esm/components/datatable/datatable-checkbox.js.map +1 -1
  78. package/lib/esm/components/datatable/datatable-column-utils.d.ts +30 -0
  79. package/lib/esm/components/datatable/datatable-column-utils.d.ts.map +1 -0
  80. package/lib/esm/components/datatable/datatable-column-utils.js +38 -0
  81. package/lib/esm/components/datatable/datatable-column-utils.js.map +1 -0
  82. package/lib/esm/components/datatable/datatable-contracts.d.ts +2 -4
  83. package/lib/esm/components/datatable/datatable-contracts.d.ts.map +1 -1
  84. package/lib/esm/components/datatable/datatable-defaults.d.ts +20 -0
  85. package/lib/esm/components/datatable/datatable-defaults.d.ts.map +1 -0
  86. package/lib/esm/components/datatable/datatable-defaults.js +190 -0
  87. package/lib/esm/components/datatable/datatable-defaults.js.map +1 -0
  88. package/lib/esm/components/datatable/datatable-layout-plugin.d.ts +7 -0
  89. package/lib/esm/components/datatable/datatable-layout-plugin.d.ts.map +1 -0
  90. package/lib/esm/components/datatable/datatable-layout-plugin.js +334 -0
  91. package/lib/esm/components/datatable/datatable-layout-plugin.js.map +1 -0
  92. package/lib/esm/components/datatable/datatable-local-provider.d.ts +2 -2
  93. package/lib/esm/components/datatable/datatable-local-provider.d.ts.map +1 -1
  94. package/lib/esm/components/datatable/datatable-local-provider.js +85 -27
  95. package/lib/esm/components/datatable/datatable-local-provider.js.map +1 -1
  96. package/lib/esm/components/datatable/datatable-pagination-renderer.d.ts.map +1 -1
  97. package/lib/esm/components/datatable/datatable-pagination-renderer.js +13 -13
  98. package/lib/esm/components/datatable/datatable-pagination-renderer.js.map +1 -1
  99. package/lib/esm/components/datatable/datatable-registry.d.ts +18 -0
  100. package/lib/esm/components/datatable/datatable-registry.d.ts.map +1 -0
  101. package/lib/esm/components/datatable/datatable-registry.js +63 -0
  102. package/lib/esm/components/datatable/datatable-registry.js.map +1 -0
  103. package/lib/esm/components/datatable/datatable-remote-provider.d.ts.map +1 -1
  104. package/lib/esm/components/datatable/datatable-remote-provider.js +1 -2
  105. package/lib/esm/components/datatable/datatable-remote-provider.js.map +1 -1
  106. package/lib/esm/components/datatable/datatable-search-handler.d.ts +10 -0
  107. package/lib/esm/components/datatable/datatable-search-handler.d.ts.map +1 -0
  108. package/lib/esm/components/datatable/datatable-search-handler.js +62 -0
  109. package/lib/esm/components/datatable/datatable-search-handler.js.map +1 -0
  110. package/lib/esm/components/datatable/datatable-sort.d.ts +31 -4
  111. package/lib/esm/components/datatable/datatable-sort.d.ts.map +1 -1
  112. package/lib/esm/components/datatable/datatable-sort.js +85 -57
  113. package/lib/esm/components/datatable/datatable-sort.js.map +1 -1
  114. package/lib/esm/components/datatable/datatable-spinner.d.ts +30 -0
  115. package/lib/esm/components/datatable/datatable-spinner.d.ts.map +1 -0
  116. package/lib/esm/components/datatable/datatable-spinner.js +51 -0
  117. package/lib/esm/components/datatable/datatable-spinner.js.map +1 -0
  118. package/lib/esm/components/datatable/datatable-state-persistence.d.ts +19 -0
  119. package/lib/esm/components/datatable/datatable-state-persistence.d.ts.map +1 -0
  120. package/lib/esm/components/datatable/datatable-state-persistence.js +55 -0
  121. package/lib/esm/components/datatable/datatable-state-persistence.js.map +1 -0
  122. package/lib/esm/components/datatable/datatable-table-renderer.d.ts +2 -0
  123. package/lib/esm/components/datatable/datatable-table-renderer.d.ts.map +1 -1
  124. package/lib/esm/components/datatable/datatable-table-renderer.js +75 -16
  125. package/lib/esm/components/datatable/datatable-table-renderer.js.map +1 -1
  126. package/lib/esm/components/datatable/datatable-utils.d.ts +10 -0
  127. package/lib/esm/components/datatable/datatable-utils.d.ts.map +1 -0
  128. package/lib/esm/components/datatable/datatable-utils.js +12 -0
  129. package/lib/esm/components/datatable/datatable-utils.js.map +1 -0
  130. package/lib/esm/components/datatable/datatable.d.ts +35 -34
  131. package/lib/esm/components/datatable/datatable.d.ts.map +1 -1
  132. package/lib/esm/components/datatable/datatable.js +235 -499
  133. package/lib/esm/components/datatable/datatable.js.map +1 -1
  134. package/lib/esm/components/datatable/index.d.ts +1 -1
  135. package/lib/esm/components/datatable/index.d.ts.map +1 -1
  136. package/lib/esm/components/datatable/types.d.ts +127 -11
  137. package/lib/esm/components/datatable/types.d.ts.map +1 -1
  138. package/lib/esm/index.d.ts +1 -1
  139. package/lib/esm/index.d.ts.map +1 -1
  140. package/lib/esm/index.js +6 -0
  141. package/lib/esm/index.js.map +1 -1
  142. package/package.json +5 -1
  143. package/skills/ktui/SKILL.md +711 -0
  144. package/skills/ktui-datatable/SKILL.md +302 -0
  145. package/skills/ktui-install/SKILL.md +150 -0
  146. package/skills/ktui-select/SKILL.md +271 -0
  147. package/src/components/__tests__/component.test.ts +347 -0
  148. package/src/components/collapse/collapse.css +2 -2
  149. package/src/components/datatable/__tests__/architecture-boundaries.test.ts +56 -8
  150. package/src/components/datatable/__tests__/currency-sort.test.ts +25 -28
  151. package/src/components/datatable/__tests__/datatable-checkbox.test.ts +527 -0
  152. package/src/components/datatable/__tests__/datatable-column-utils.test.ts +117 -0
  153. package/src/components/datatable/__tests__/datatable-defaults.test.ts +57 -0
  154. package/src/components/datatable/__tests__/datatable-finalize-extended.test.ts +361 -0
  155. package/src/components/datatable/__tests__/datatable-fixed-layout.test.ts +427 -0
  156. package/src/components/datatable/__tests__/datatable-improvements.test.ts +484 -0
  157. package/src/components/datatable/__tests__/datatable-pagination-extended.test.ts +508 -0
  158. package/src/components/datatable/__tests__/datatable-public-api.test.ts +269 -0
  159. package/src/components/datatable/__tests__/datatable-registry.test.ts +172 -0
  160. package/src/components/datatable/__tests__/datatable-remote-provider.test.ts +468 -0
  161. package/src/components/datatable/__tests__/datatable-search-handler.test.ts +124 -0
  162. package/src/components/datatable/__tests__/datatable-sort-extended.test.ts +417 -0
  163. package/src/components/datatable/__tests__/datatable-spinner.test.ts +95 -0
  164. package/src/components/datatable/__tests__/datatable-table-renderer-extended.test.ts +425 -0
  165. package/src/components/datatable/__tests__/datatable-types.test.ts +117 -0
  166. package/src/components/datatable/__tests__/datatable-utils.test.ts +52 -0
  167. package/src/components/datatable/__tests__/locked-layout.test.ts +257 -0
  168. package/src/components/datatable/__tests__/multi-row-headers.test.ts +7 -7
  169. package/src/components/datatable/__tests__/pagination-reset.test.ts +147 -6
  170. package/src/components/datatable/__tests__/race-conditions.test.ts +11 -11
  171. package/src/components/datatable/__tests__/setup.ts +12 -4
  172. package/src/components/datatable/datatable-checkbox.ts +139 -143
  173. package/src/components/datatable/datatable-column-utils.ts +63 -0
  174. package/src/components/datatable/datatable-contracts.ts +2 -3
  175. package/src/components/datatable/datatable-defaults.ts +204 -0
  176. package/src/components/datatable/datatable-layout-plugin.ts +459 -0
  177. package/src/components/datatable/datatable-local-provider.ts +106 -35
  178. package/src/components/datatable/datatable-pagination-renderer.ts +13 -15
  179. package/src/components/datatable/datatable-registry.ts +89 -0
  180. package/src/components/datatable/datatable-remote-provider.ts +1 -3
  181. package/src/components/datatable/datatable-search-handler.ts +97 -0
  182. package/src/components/datatable/datatable-sort.ts +111 -66
  183. package/src/components/datatable/datatable-spinner.ts +103 -0
  184. package/src/components/datatable/datatable-state-persistence.ts +67 -0
  185. package/src/components/datatable/datatable-table-renderer.ts +81 -18
  186. package/src/components/datatable/datatable-utils.ts +12 -0
  187. package/src/components/datatable/datatable.css +98 -0
  188. package/src/components/datatable/datatable.ts +288 -583
  189. package/src/components/datatable/index.ts +8 -0
  190. package/src/components/datatable/types.ts +157 -23
  191. package/src/helpers/__tests__/dom.test.ts +776 -0
  192. package/src/helpers/__tests__/utils.test.ts +332 -0
  193. package/src/index.ts +15 -0
  194. package/skills/ktui-components/SKILL.md +0 -41
  195. package/skills/ktui-theming/SKILL.md +0 -50
  196. package/src/components/datatable/datatable-event-adapter.ts +0 -21
@@ -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
+ }
@@ -12,6 +12,7 @@ import {
12
12
  KTDataTableTableRenderer,
13
13
  KTDataTableTableRendererInput,
14
14
  } from './datatable-contracts';
15
+ import { resolveColumns } from './datatable-column-utils';
15
16
 
16
17
  export class KTDataTableDomTableRenderer<
17
18
  T extends KTDataTableDataInterface,
@@ -26,10 +27,12 @@ export class KTDataTableDomTableRenderer<
26
27
  const tbodyElement =
27
28
  input.tableElement.createTBody() as HTMLTableSectionElement;
28
29
 
29
- if (input.originalTbodyClass) {
30
- tbodyElement.className = input.originalTbodyClass;
30
+ if (input.originalClasses.tbody) {
31
+ tbodyElement.className = input.originalClasses.tbody;
31
32
  }
32
33
 
34
+ this.applyTableLayout(input);
35
+
33
36
  this.renderContent(input, tbodyElement);
34
37
 
35
38
  return tbodyElement;
@@ -44,9 +47,71 @@ export class KTDataTableDomTableRenderer<
44
47
  const cell = row.insertCell();
45
48
  const logicalCount = getLogicalColumnCount();
46
49
  cell.colSpan = logicalCount > 0 ? logicalCount : 1;
50
+ cell.style.textAlign = 'center';
47
51
  cell.innerHTML = message;
48
52
  }
49
53
 
54
+ private applyTableLayout(
55
+ input: KTDataTableTableRendererInput<T>,
56
+ ): void {
57
+ const tableLayout = input.config.tableLayout || 'auto';
58
+ const tableElement = input.tableElement;
59
+
60
+ tableElement.style.tableLayout = tableLayout;
61
+
62
+ if (tableLayout === 'fixed') {
63
+ if (!tableElement.style.width) {
64
+ tableElement.style.width = '100%';
65
+ }
66
+ this.updateColgroup(input);
67
+ } else {
68
+ const existingColgroup = tableElement.querySelector('colgroup');
69
+ if (existingColgroup) {
70
+ tableElement.removeChild(existingColgroup);
71
+ }
72
+ }
73
+ }
74
+
75
+ private updateColgroup(
76
+ input: KTDataTableTableRendererInput<T>,
77
+ ): void {
78
+ const tableElement = input.tableElement;
79
+ const existingColgroup = tableElement.querySelector('colgroup');
80
+ if (existingColgroup) {
81
+ tableElement.removeChild(existingColgroup);
82
+ }
83
+
84
+ const colgroup = document.createElement('colgroup');
85
+
86
+ if (input.config.columns) {
87
+ const columns = input.config.columns;
88
+ for (const key of Object.keys(columns)) {
89
+ const col = document.createElement('col');
90
+ if (columns[key].width) {
91
+ col.style.width = columns[key].width;
92
+ }
93
+ colgroup.appendChild(col);
94
+ }
95
+ } else {
96
+ const { columnsByIndex } = resolveColumns(input.theadElement);
97
+ for (const th of columnsByIndex) {
98
+ const col = document.createElement('col');
99
+ const width = th.getAttribute('data-kt-datatable-column-width');
100
+ if (width) {
101
+ col.style.width = width;
102
+ }
103
+ colgroup.appendChild(col);
104
+ }
105
+ }
106
+
107
+ const thead = tableElement.querySelector('thead');
108
+ if (thead) {
109
+ tableElement.insertBefore(colgroup, thead);
110
+ } else {
111
+ tableElement.appendChild(colgroup);
112
+ }
113
+ }
114
+
50
115
  private renderContent(
51
116
  input: KTDataTableTableRendererInput<T>,
52
117
  tbodyElement: HTMLTableSectionElement,
@@ -64,15 +129,7 @@ export class KTDataTableDomTableRenderer<
64
129
  return tbodyElement;
65
130
  }
66
131
 
67
- const allThs: NodeListOf<HTMLTableCellElement> = input.theadElement
68
- ? input.theadElement.querySelectorAll('th')
69
- : ([] as unknown as NodeListOf<HTMLTableCellElement>);
70
-
71
- const ths: HTMLTableCellElement[] = Array.from(allThs).filter((th) =>
72
- th.hasAttribute('data-kt-datatable-column'),
73
- );
74
- const columnsToRender: HTMLTableCellElement[] =
75
- ths.length > 0 && ths.length !== allThs.length ? Array.from(allThs) : ths;
132
+ const { columnsByIndex: columnsToRender } = resolveColumns(input.theadElement);
76
133
  const logicalColumnCount =
77
134
  columnsToRender.length > 0
78
135
  ? columnsToRender.length
@@ -81,8 +138,8 @@ export class KTDataTableDomTableRenderer<
81
138
  input.data.forEach((item: T, rowIndex: number) => {
82
139
  const row = document.createElement('tr');
83
140
 
84
- if (input.originalTrClasses && input.originalTrClasses[rowIndex]) {
85
- row.className = input.originalTrClasses[rowIndex];
141
+ if (input.originalClasses.tr && input.originalClasses.tr[rowIndex]) {
142
+ row.className = input.originalClasses.tr[rowIndex];
86
143
  }
87
144
 
88
145
  if (!input.config.columns) {
@@ -174,7 +231,13 @@ export class KTDataTableDomTableRenderer<
174
231
  td.innerHTML = result as string;
175
232
  }
176
233
  } else {
177
- td.textContent = item[colKey] as string;
234
+ const cellValue = item[colKey];
235
+ if (cellValue === null || cellValue === undefined) {
236
+ td.textContent = '';
237
+ } else {
238
+ // Match implicit column rendering: preserve HTML from DOM extraction.
239
+ td.innerHTML = String(cellValue);
240
+ }
178
241
  }
179
242
 
180
243
  if (typeof columnDef.createdCell === 'function') {
@@ -192,11 +255,11 @@ export class KTDataTableDomTableRenderer<
192
255
  colIndex: number,
193
256
  ): void {
194
257
  if (
195
- input.originalTdClasses &&
196
- input.originalTdClasses[rowIndex] &&
197
- input.originalTdClasses[rowIndex][colIndex]
258
+ input.originalClasses.td &&
259
+ input.originalClasses.td[rowIndex] &&
260
+ input.originalClasses.td[rowIndex][colIndex]
198
261
  ) {
199
- td.className = input.originalTdClasses[rowIndex][colIndex];
262
+ td.className = input.originalClasses.td[rowIndex][colIndex];
200
263
  }
201
264
  }
202
265
 
@@ -0,0 +1,12 @@
1
+ /**
2
+ * KTUI - Free & Open-Source Tailwind UI Components by Keenthemes
3
+ * Copyright 2025 by Keenthemes Inc
4
+ */
5
+
6
+ /**
7
+ * Strip HTML tags and &nbsp; entities from a value, returning plain text.
8
+ * Used by sort, search, and filter pipelines.
9
+ */
10
+ export function stripHtml(value: unknown): string {
11
+ return String(value).replace(/<[^>]*>|&nbsp;/g, '');
12
+ }