@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
@@ -0,0 +1,204 @@
1
+ /**
2
+ * KTUI - Free & Open-Source Tailwind UI Components by Keenthemes
3
+ * Copyright 2025 by Keenthemes Inc
4
+ */
5
+
6
+ import { KTDataTableConfigInterface, KTDataTableStateInterface } from './types';
7
+
8
+ /** Default page size options shown in the size selector */
9
+ export const DEFAULT_PAGE_SIZES = [5, 10, 20, 30, 50] as const;
10
+
11
+ /** Default debounce delay (ms) for search input */
12
+ export const DEFAULT_SEARCH_DELAY = 500;
13
+
14
+ /** Default maximum visible page buttons before showing '...' */
15
+ export const DEFAULT_PAGE_MORE_LIMIT = 3;
16
+
17
+ /**
18
+ * Default configuration for KTDataTable.
19
+ * Extracted from _initDefaultConfig() for separation of concerns.
20
+ * Note: sort.callback and search.callback are NOT included here because
21
+ * they require instance context (this._sortHandler). They are added
22
+ * in datatable.ts _initDefaultConfig().
23
+ */
24
+ export const DATATABLE_DEFAULTS: Readonly<KTDataTableConfigInterface> = {
25
+ /**
26
+ * HTTP method for server-side API call
27
+ */
28
+ requestMethod: 'GET',
29
+ /**
30
+ * Custom HTTP headers for the API request
31
+ */
32
+ requestHeaders: {
33
+ 'Content-Type': 'application/x-www-form-urlencoded',
34
+ },
35
+ /**
36
+ * Pagination info template
37
+ */
38
+ info: '{start}-{end} of {total}',
39
+ /**
40
+ * Info text when there is no data
41
+ */
42
+ infoEmpty: 'No records found',
43
+ /**
44
+ * Available page sizes
45
+ */
46
+ pageSizes: [...DEFAULT_PAGE_SIZES],
47
+ /**
48
+ * Default page size
49
+ */
50
+ pageSize: 10,
51
+ /**
52
+ * Enable or disable pagination more button
53
+ */
54
+ pageMore: true,
55
+ /**
56
+ * Maximum number of pages before enabling pagination more button
57
+ */
58
+ pageMoreLimit: 3,
59
+ /**
60
+ * Pagination button templates
61
+ */
62
+ pagination: {
63
+ number: {
64
+ /**
65
+ * CSS classes to be added to the pagination button
66
+ */
67
+ class: 'kt-datatable-pagination-button',
68
+ /**
69
+ * Text to be displayed in the pagination button
70
+ */
71
+ text: '{page}',
72
+ },
73
+ previous: {
74
+ /**
75
+ * CSS classes to be added to the previous pagination button
76
+ */
77
+ class: 'kt-datatable-pagination-button kt-datatable-pagination-prev',
78
+ /**
79
+ * Text to be displayed in the previous pagination button
80
+ */
81
+ text: `
82
+ <svg class="rtl:transform rtl:rotate-180 size-3.5 shrink-0" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
83
+ <path d="M8.86501 16.7882V12.8481H21.1459C21.3724 12.8481 21.5897 12.7581 21.7498 12.5979C21.91 12.4378 22 12.2205 22 11.994C22 11.7675 21.91 11.5503 21.7498 11.3901C21.5897 11.2299 21.3724 11.1399 21.1459 11.1399H8.86501V7.2112C8.86628 7.10375 8.83517 6.9984 8.77573 6.90887C8.7163 6.81934 8.63129 6.74978 8.53177 6.70923C8.43225 6.66869 8.32283 6.65904 8.21775 6.68155C8.11267 6.70405 8.0168 6.75766 7.94262 6.83541L2.15981 11.6182C2.1092 11.668 2.06901 11.7274 2.04157 11.7929C2.01413 11.8584 2 11.9287 2 11.9997C2 12.0707 2.01413 12.141 2.04157 12.2065C2.06901 12.272 2.1092 12.3314 2.15981 12.3812L7.94262 17.164C8.0168 17.2417 8.11267 17.2953 8.21775 17.3178C8.32283 17.3403 8.43225 17.3307 8.53177 17.2902C8.63129 17.2496 8.7163 17.18 8.77573 17.0905C8.83517 17.001 8.86628 16.8956 8.86501 16.7882Z" fill="currentColor"/>
84
+ </svg>
85
+ `,
86
+ },
87
+ next: {
88
+ /**
89
+ * CSS classes to be added to the next pagination button
90
+ */
91
+ class: 'kt-datatable-pagination-button kt-datatable-pagination-next',
92
+ /**
93
+ * Text to be displayed in the next pagination button
94
+ */
95
+ text: `
96
+ <svg class="rtl:transform rtl:rotate-180 size-3.5 shrink-0" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
97
+ <path d="M15.135 7.21144V11.1516H2.85407C2.62756 11.1516 2.41032 11.2415 2.25015 11.4017C2.08998 11.5619 2 11.7791 2 12.0056C2 12.2321 2.08998 12.4494 2.25015 12.6096C2.41032 12.7697 2.62756 12.8597 2.85407 12.8597H15.135V16.7884C15.1337 16.8959 15.1648 17.0012 15.2243 17.0908C15.2837 17.1803 15.3687 17.2499 15.4682 17.2904C15.5677 17.3309 15.6772 17.3406 15.7822 17.3181C15.8873 17.2956 15.9832 17.242 16.0574 17.1642L21.8402 12.3814C21.8908 12.3316 21.931 12.2722 21.9584 12.2067C21.9859 12.1412 22 12.0709 22 11.9999C22 11.9289 21.9859 11.8586 21.9584 11.7931C21.931 11.7276 21.8908 11.6683 21.8402 11.6185L16.0574 6.83565C15.9832 6.75791 15.8873 6.70429 15.7822 6.68179C15.6772 6.65929 15.5677 6.66893 15.4682 6.70948C15.3687 6.75002 15.2837 6.81959 15.2243 6.90911C15.1648 6.99864 15.1337 7.10399 15.135 7.21144Z" fill="currentColor"/>
98
+ </svg>
99
+ `,
100
+ },
101
+ more: {
102
+ /**
103
+ * CSS classes to be added to the pagination more button
104
+ */
105
+ class: 'kt-datatable-pagination-button kt-datatable-pagination-more',
106
+ /**
107
+ * Text to be displayed in the pagination more button
108
+ */
109
+ text: '...',
110
+ },
111
+ },
112
+ /**
113
+ * Sorting options — classes only; callback is added in datatable.ts
114
+ */
115
+ sort: {
116
+ /**
117
+ * CSS classes to be added to the sortable headers
118
+ */
119
+ classes: {
120
+ base: 'kt-table-col',
121
+ asc: 'asc',
122
+ desc: 'desc',
123
+ },
124
+ },
125
+ /**
126
+ * Search options — delay only; callback is added in datatable.ts
127
+ */
128
+ search: {
129
+ /**
130
+ * Delay in milliseconds before the search function is applied to the data array
131
+ * @default 500
132
+ */
133
+ delay: 500, // ms
134
+ },
135
+ /**
136
+ * Loading spinner options
137
+ */
138
+ loading: {
139
+ /**
140
+ * Template to be displayed during data fetching process
141
+ */
142
+ template: `
143
+ <div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
144
+ <div class="kt-datatable-loading">
145
+ <svg class="animate-spin -ml-1 h-5 w-5 text-gray-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
146
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3"></circle>
147
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
148
+ </svg>
149
+ {content}
150
+ </div>
151
+ </div>
152
+ `,
153
+ /**
154
+ * Loading text to be displayed in the template
155
+ */
156
+ content: 'Loading...',
157
+ },
158
+ /**
159
+ * Selectors of the elements to be targeted
160
+ */
161
+ attributes: {
162
+ /**
163
+ * Data table element
164
+ */
165
+ table: 'table[data-kt-datatable-table="true"]',
166
+ /**
167
+ * Pagination info element
168
+ */
169
+ info: '[data-kt-datatable-info="true"]',
170
+ /**
171
+ * Page size dropdown element
172
+ */
173
+ size: '[data-kt-datatable-size="true"]',
174
+ /**
175
+ * Pagination element
176
+ */
177
+ pagination: '[data-kt-datatable-pagination="true"]',
178
+ /**
179
+ * Spinner element
180
+ */
181
+ spinner: '[data-kt-datatable-spinner="true"]',
182
+ /**
183
+ * Checkbox element
184
+ */
185
+ check: '[data-kt-datatable-check="true"]',
186
+ checkbox: '[data-kt-datatable-row-check="true"]',
187
+ },
188
+ /**
189
+ * Enable or disable state saving
190
+ */
191
+ stateSave: true,
192
+ checkbox: {
193
+ checkedClass: 'checked',
194
+ },
195
+ /**
196
+ * Table layout algorithm: 'auto' (default) or 'fixed' for consistent column widths
197
+ */
198
+ tableLayout: 'auto',
199
+ /**
200
+ * Private properties
201
+ */
202
+ _state: {} as KTDataTableStateInterface,
203
+ loadingClass: 'loading',
204
+ };
@@ -0,0 +1,459 @@
1
+ /**
2
+ * KTUI - Free & Open-Source Tailwind UI Components by Keenthemes
3
+ * Copyright 2025 by Keenthemes Inc
4
+ */
5
+
6
+ import {
7
+ KTDataTableConfigInterface,
8
+ KTDataTableLayoutPluginContextInterface,
9
+ KTDataTableLayoutPluginInterface,
10
+ } from './types';
11
+
12
+ type Edge = 'left' | 'right';
13
+
14
+ const LOCKED_CELL_CLASS = 'kt-datatable-locked-cell';
15
+ const LOCKED_HEADER_CLASS = 'kt-datatable-locked-header';
16
+ const LOCKED_TOP_ROW_CLASS = 'kt-datatable-locked-top-row';
17
+ const LOCKED_BOTTOM_ROW_CLASS = 'kt-datatable-locked-bottom-row';
18
+ const LOCKED_LEFT_CLASS = 'kt-datatable-locked-left';
19
+ const LOCKED_RIGHT_CLASS = 'kt-datatable-locked-right';
20
+ const LOCKED_LAYOUT_SEPARATE_CLASS = 'kt-datatable-locked-layout-separate';
21
+ const LOCKED_HEADER_SECTION_CLASS = 'kt-datatable-locked-header-section';
22
+
23
+ const HEADER_Z_INDEX = 40;
24
+ const ROW_Z_INDEX = 30;
25
+ const COLUMN_Z_INDEX = 35;
26
+ const INTERSECTION_Z_INDEX = 45;
27
+
28
+ const toPositiveInteger = (value: number | undefined): number => {
29
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
30
+ return 0;
31
+ }
32
+
33
+ return Math.max(0, Math.floor(value));
34
+ };
35
+
36
+ const hasStickyColumns = (config: KTDataTableConfigInterface): boolean => {
37
+ const lockedLayout = config.lockedLayout;
38
+ if (!lockedLayout?.stickyColumns) {
39
+ return false;
40
+ }
41
+
42
+ return (
43
+ (lockedLayout.stickyColumns.left?.length || 0) > 0 ||
44
+ (lockedLayout.stickyColumns.right?.length || 0) > 0
45
+ );
46
+ };
47
+
48
+ const hasLockedLayoutConfig = (config: KTDataTableConfigInterface): boolean => {
49
+ const lockedLayout = config.lockedLayout;
50
+ if (!lockedLayout) {
51
+ return false;
52
+ }
53
+
54
+ return (
55
+ lockedLayout.stickyHeader === true ||
56
+ toPositiveInteger(lockedLayout.stickyRows?.top) > 0 ||
57
+ toPositiveInteger(lockedLayout.stickyRows?.bottom) > 0 ||
58
+ (lockedLayout.stickyColumns?.left?.length || 0) > 0 ||
59
+ (lockedLayout.stickyColumns?.right?.length || 0) > 0
60
+ );
61
+ };
62
+
63
+ const getScrollContainer = (rootElement: HTMLElement): HTMLElement => {
64
+ return (
65
+ rootElement.closest<HTMLElement>('.kt-table-wrapper') ||
66
+ rootElement.querySelector<HTMLElement>('.kt-table-wrapper') ||
67
+ rootElement
68
+ );
69
+ };
70
+
71
+ const clearStickyStyles = (
72
+ tableElement: HTMLTableElement,
73
+ scrollContainer: HTMLElement,
74
+ ): void => {
75
+ tableElement.classList.remove(
76
+ 'kt-datatable-locked-layout',
77
+ LOCKED_LAYOUT_SEPARATE_CLASS,
78
+ );
79
+ scrollContainer.classList.remove('kt-datatable-locked-layout-host');
80
+ tableElement.style.borderCollapse = '';
81
+ tableElement.style.borderSpacing = '';
82
+
83
+ const theadElement = tableElement.tHead;
84
+ if (theadElement) {
85
+ theadElement.classList.remove(LOCKED_HEADER_SECTION_CLASS);
86
+ theadElement.style.position = '';
87
+ theadElement.style.top = '';
88
+ theadElement.style.zIndex = '';
89
+ }
90
+
91
+ const stickyElements = tableElement.querySelectorAll<HTMLElement>(
92
+ `.${LOCKED_CELL_CLASS}`,
93
+ );
94
+
95
+ stickyElements.forEach((element) => {
96
+ element.classList.remove(
97
+ LOCKED_CELL_CLASS,
98
+ LOCKED_HEADER_CLASS,
99
+ LOCKED_TOP_ROW_CLASS,
100
+ LOCKED_BOTTOM_ROW_CLASS,
101
+ LOCKED_LEFT_CLASS,
102
+ LOCKED_RIGHT_CLASS,
103
+ );
104
+ element.style.position = '';
105
+ element.style.top = '';
106
+ element.style.bottom = '';
107
+ element.style.left = '';
108
+ element.style.right = '';
109
+ element.style.zIndex = '';
110
+ element.style.backgroundColor = '';
111
+ });
112
+ };
113
+
114
+ const getDirection = (tableElement: HTMLTableElement): 'ltr' | 'rtl' => {
115
+ const scopedDir = tableElement
116
+ .closest<HTMLElement>('[dir]')
117
+ ?.getAttribute('dir');
118
+ const globalDir =
119
+ typeof document !== 'undefined'
120
+ ? document.documentElement.getAttribute('dir')
121
+ : null;
122
+ return scopedDir === 'rtl' || globalDir === 'rtl' ? 'rtl' : 'ltr';
123
+ };
124
+
125
+ const resolveEdgeProperty = (edge: Edge, direction: 'ltr' | 'rtl'): Edge => {
126
+ if (direction === 'rtl') {
127
+ return edge === 'left' ? 'right' : 'left';
128
+ }
129
+
130
+ return edge;
131
+ };
132
+
133
+ const setStickyEdge = (
134
+ element: HTMLElement,
135
+ edge: Edge,
136
+ offset: number,
137
+ direction: 'ltr' | 'rtl',
138
+ ): void => {
139
+ const resolvedEdge = resolveEdgeProperty(edge, direction);
140
+ if (resolvedEdge === 'left') {
141
+ element.style.left = `${offset}px`;
142
+ element.style.right = '';
143
+ } else {
144
+ element.style.right = `${offset}px`;
145
+ element.style.left = '';
146
+ }
147
+ };
148
+
149
+ const ensureStickyCell = (
150
+ element: HTMLElement,
151
+ className: string,
152
+ zIndex: number,
153
+ ): void => {
154
+ element.classList.add(LOCKED_CELL_CLASS, className);
155
+ element.style.position = 'sticky';
156
+ element.style.zIndex = String(zIndex);
157
+ };
158
+
159
+ const measureStickyHeaderHeight = (
160
+ theadElement: HTMLTableSectionElement,
161
+ ): number => Math.round(theadElement.offsetHeight);
162
+
163
+ /** Offset for top sticky body rows so they sit flush under a sticky header. */
164
+ const getStickyTopRowOffset = (
165
+ headerHeight: number,
166
+ useCollapsedBorders: boolean,
167
+ ): number => {
168
+ if (headerHeight <= 0) {
169
+ return 0;
170
+ }
171
+
172
+ // Collapsed row borders are shared between thead and the first tbody row.
173
+ return useCollapsedBorders ? headerHeight - 1 : headerHeight;
174
+ };
175
+
176
+ const markIntersectionZIndex = (element: HTMLElement): void => {
177
+ const isRowLocked =
178
+ element.classList.contains(LOCKED_HEADER_CLASS) ||
179
+ element.classList.contains(LOCKED_TOP_ROW_CLASS) ||
180
+ element.classList.contains(LOCKED_BOTTOM_ROW_CLASS);
181
+ const isColumnLocked =
182
+ element.classList.contains(LOCKED_LEFT_CLASS) ||
183
+ element.classList.contains(LOCKED_RIGHT_CLASS);
184
+
185
+ if (isRowLocked && isColumnLocked) {
186
+ element.style.zIndex = String(INTERSECTION_Z_INDEX);
187
+ }
188
+ };
189
+
190
+ const applyStickyHeader = (
191
+ theadElement: HTMLTableSectionElement,
192
+ enabled: boolean,
193
+ useSectionSticky: boolean,
194
+ ): number => {
195
+ if (!enabled) {
196
+ return 0;
197
+ }
198
+
199
+ if (useSectionSticky) {
200
+ theadElement.classList.add(LOCKED_HEADER_SECTION_CLASS);
201
+ theadElement.style.position = 'sticky';
202
+ theadElement.style.top = '0';
203
+ theadElement.style.zIndex = String(HEADER_Z_INDEX);
204
+
205
+ Array.from(theadElement.rows).forEach((row) => {
206
+ Array.from(row.cells).forEach((cell) => {
207
+ const headerCell = cell as HTMLTableCellElement;
208
+ headerCell.classList.add(LOCKED_CELL_CLASS, LOCKED_HEADER_CLASS);
209
+ });
210
+ });
211
+
212
+ return measureStickyHeaderHeight(theadElement);
213
+ }
214
+
215
+ let cumulativeTop = 0;
216
+ Array.from(theadElement.rows).forEach((row) => {
217
+ const rowTop = cumulativeTop;
218
+ Array.from(row.cells).forEach((cell) => {
219
+ const headerCell = cell as HTMLTableCellElement;
220
+ ensureStickyCell(headerCell, LOCKED_HEADER_CLASS, HEADER_Z_INDEX);
221
+ headerCell.style.top = `${rowTop}px`;
222
+ });
223
+ cumulativeTop += row.offsetHeight;
224
+ });
225
+
226
+ return cumulativeTop;
227
+ };
228
+
229
+ const applyStickyRows = (
230
+ tbodyElement: HTMLTableSectionElement,
231
+ headerHeight: number,
232
+ topCount: number,
233
+ bottomCount: number,
234
+ useCollapsedBorders: boolean,
235
+ ): void => {
236
+ const rows = Array.from(tbodyElement.rows);
237
+
238
+ let topOffset = getStickyTopRowOffset(headerHeight, useCollapsedBorders);
239
+ rows.slice(0, topCount).forEach((row) => {
240
+ const rowTop = topOffset;
241
+ Array.from(row.cells).forEach((cell) => {
242
+ const td = cell as HTMLTableCellElement;
243
+ ensureStickyCell(td, LOCKED_TOP_ROW_CLASS, ROW_Z_INDEX);
244
+ td.style.top = `${rowTop}px`;
245
+ });
246
+ topOffset += row.offsetHeight;
247
+ });
248
+
249
+ let bottomOffset = 0;
250
+ rows
251
+ .slice(Math.max(0, rows.length - bottomCount))
252
+ .reverse()
253
+ .forEach((row) => {
254
+ const rowBottom = bottomOffset;
255
+ Array.from(row.cells).forEach((cell) => {
256
+ const td = cell as HTMLTableCellElement;
257
+ ensureStickyCell(td, LOCKED_BOTTOM_ROW_CLASS, ROW_Z_INDEX);
258
+ td.style.bottom = `${rowBottom}px`;
259
+ });
260
+ bottomOffset += row.offsetHeight;
261
+ });
262
+ };
263
+
264
+ const getColumnIndexMap = (
265
+ theadElement: HTMLTableSectionElement,
266
+ config: KTDataTableConfigInterface,
267
+ ): Map<string, number> => {
268
+ const map = new Map<string, number>();
269
+ const typedHeaders = Array.from(
270
+ theadElement.querySelectorAll<HTMLTableCellElement>(
271
+ 'th[data-kt-datatable-column]',
272
+ ),
273
+ );
274
+
275
+ if (typedHeaders.length > 0) {
276
+ typedHeaders.forEach((th, index) => {
277
+ const column = th.getAttribute('data-kt-datatable-column');
278
+ if (column) {
279
+ map.set(column, index);
280
+ }
281
+ });
282
+ return map;
283
+ }
284
+
285
+ if (config.columns) {
286
+ Object.keys(config.columns).forEach((key, index) => {
287
+ map.set(key, index);
288
+ });
289
+ }
290
+
291
+ return map;
292
+ };
293
+
294
+ const getColumnCells = (
295
+ tableElement: HTMLTableElement,
296
+ columnIndex: number,
297
+ ): HTMLTableCellElement[] => {
298
+ const cells: HTMLTableCellElement[] = [];
299
+ tableElement.querySelectorAll('tr').forEach((row) => {
300
+ const cell = row.children.item(columnIndex);
301
+ if (cell instanceof HTMLTableCellElement) {
302
+ cells.push(cell);
303
+ }
304
+ });
305
+ return cells;
306
+ };
307
+
308
+ const applyStickyColumns = (
309
+ tableElement: HTMLTableElement,
310
+ theadElement: HTMLTableSectionElement,
311
+ config: KTDataTableConfigInterface,
312
+ ): void => {
313
+ const lockedColumns = config.lockedLayout?.stickyColumns;
314
+ if (!lockedColumns) {
315
+ return;
316
+ }
317
+
318
+ const direction = getDirection(tableElement);
319
+ const columnMap = getColumnIndexMap(theadElement, config);
320
+
321
+ let leftOffset = 0;
322
+ (lockedColumns.left || []).forEach((key) => {
323
+ const index = columnMap.get(key);
324
+ if (typeof index !== 'number') {
325
+ return;
326
+ }
327
+
328
+ const cells = getColumnCells(tableElement, index);
329
+ if (cells.length === 0) {
330
+ return;
331
+ }
332
+
333
+ const width = cells[0].getBoundingClientRect().width;
334
+ cells.forEach((cell) => {
335
+ ensureStickyCell(cell, LOCKED_LEFT_CLASS, COLUMN_Z_INDEX);
336
+ setStickyEdge(cell, 'left', leftOffset, direction);
337
+ });
338
+ leftOffset += width;
339
+ });
340
+
341
+ let rightOffset = 0;
342
+ [...(lockedColumns.right || [])].reverse().forEach((key) => {
343
+ const index = columnMap.get(key);
344
+ if (typeof index !== 'number') {
345
+ return;
346
+ }
347
+
348
+ const cells = getColumnCells(tableElement, index);
349
+ if (cells.length === 0) {
350
+ return;
351
+ }
352
+
353
+ const width = cells[0].getBoundingClientRect().width;
354
+ cells.forEach((cell) => {
355
+ ensureStickyCell(cell, LOCKED_RIGHT_CLASS, COLUMN_Z_INDEX);
356
+ setStickyEdge(cell, 'right', rightOffset, direction);
357
+ });
358
+ rightOffset += width;
359
+ });
360
+
361
+ tableElement
362
+ .querySelectorAll<HTMLElement>(`.${LOCKED_CELL_CLASS}`)
363
+ .forEach(markIntersectionZIndex);
364
+ };
365
+
366
+ export const createStickyLayoutPlugin =
367
+ (): KTDataTableLayoutPluginInterface => {
368
+ let resizeHandler: (() => void) | null = null;
369
+ let scrollContainerTarget: HTMLElement | null = null;
370
+ let isApplying = false;
371
+
372
+ const applyLayout = (
373
+ ctx: KTDataTableLayoutPluginContextInterface,
374
+ ): void => {
375
+ if (isApplying || !hasLockedLayoutConfig(ctx.config)) {
376
+ return;
377
+ }
378
+
379
+ isApplying = true;
380
+ try {
381
+ const scrollContainer = getScrollContainer(ctx.rootElement);
382
+ clearStickyStyles(ctx.tableElement, scrollContainer);
383
+ ctx.tableElement.classList.add('kt-datatable-locked-layout');
384
+ scrollContainer.classList.add('kt-datatable-locked-layout-host');
385
+
386
+ if (hasStickyColumns(ctx.config)) {
387
+ ctx.tableElement.classList.add(LOCKED_LAYOUT_SEPARATE_CLASS);
388
+ ctx.tableElement.style.borderCollapse = 'separate';
389
+ ctx.tableElement.style.borderSpacing = '0';
390
+ }
391
+
392
+ const lockedLayout = ctx.config.lockedLayout || {};
393
+ const useCollapsedBorders = !hasStickyColumns(ctx.config);
394
+ const headerHeight = applyStickyHeader(
395
+ ctx.theadElement,
396
+ lockedLayout.stickyHeader === true,
397
+ useCollapsedBorders,
398
+ );
399
+
400
+ applyStickyRows(
401
+ ctx.tbodyElement,
402
+ headerHeight,
403
+ toPositiveInteger(lockedLayout.stickyRows?.top),
404
+ toPositiveInteger(lockedLayout.stickyRows?.bottom),
405
+ useCollapsedBorders,
406
+ );
407
+
408
+ applyStickyColumns(ctx.tableElement, ctx.theadElement, ctx.config);
409
+ } finally {
410
+ isApplying = false;
411
+ }
412
+ };
413
+
414
+ const detachResizeListener = (): void => {
415
+ if (!resizeHandler) {
416
+ return;
417
+ }
418
+
419
+ window.removeEventListener('resize', resizeHandler);
420
+ if (scrollContainerTarget) {
421
+ scrollContainerTarget.removeEventListener('scroll', resizeHandler);
422
+ }
423
+
424
+ resizeHandler = null;
425
+ scrollContainerTarget = null;
426
+ };
427
+
428
+ return {
429
+ beforeDraw: (ctx) => {
430
+ const scrollContainer = getScrollContainer(ctx.rootElement);
431
+ clearStickyStyles(ctx.tableElement, scrollContainer);
432
+ },
433
+ afterDraw: (ctx) => {
434
+ detachResizeListener();
435
+ applyLayout(ctx);
436
+
437
+ // Throttle re-layout with rAF to avoid running on every resize/scroll event
438
+ let rafId = 0;
439
+ const throttledApply = () => {
440
+ if (rafId) return;
441
+ rafId = requestAnimationFrame(() => {
442
+ rafId = 0;
443
+ applyLayout(ctx);
444
+ });
445
+ };
446
+
447
+ const scrollContainer = getScrollContainer(ctx.rootElement);
448
+ resizeHandler = throttledApply;
449
+ window.addEventListener('resize', resizeHandler);
450
+ scrollContainerTarget = scrollContainer;
451
+ scrollContainer.addEventListener('scroll', resizeHandler);
452
+ },
453
+ dispose: (ctx) => {
454
+ detachResizeListener();
455
+ const scrollContainer = getScrollContainer(ctx.rootElement);
456
+ clearStickyStyles(ctx.tableElement, scrollContainer);
457
+ },
458
+ };
459
+ };