@keenthemes/ktui 1.0.3

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 (426) hide show
  1. package/CONTRIBUTING.md +88 -0
  2. package/LICENSE.md +21 -0
  3. package/README.md +124 -0
  4. package/dist/ktui.js +19201 -0
  5. package/dist/ktui.min.js +2 -0
  6. package/dist/ktui.min.js.map +1 -0
  7. package/lib/cjs/components/accordion/accordion.js +168 -0
  8. package/lib/cjs/components/accordion/accordion.js.map +1 -0
  9. package/lib/cjs/components/accordion/index.js +6 -0
  10. package/lib/cjs/components/accordion/index.js.map +1 -0
  11. package/lib/cjs/components/accordion/types.js +3 -0
  12. package/lib/cjs/components/accordion/types.js.map +1 -0
  13. package/lib/cjs/components/collapse/collapse.js +169 -0
  14. package/lib/cjs/components/collapse/collapse.js.map +1 -0
  15. package/lib/cjs/components/collapse/index.js +6 -0
  16. package/lib/cjs/components/collapse/index.js.map +1 -0
  17. package/lib/cjs/components/collapse/types.js +3 -0
  18. package/lib/cjs/components/collapse/types.js.map +1 -0
  19. package/lib/cjs/components/component.js +135 -0
  20. package/lib/cjs/components/component.js.map +1 -0
  21. package/lib/cjs/components/config.js +26 -0
  22. package/lib/cjs/components/config.js.map +1 -0
  23. package/lib/cjs/components/config.umd.js +23 -0
  24. package/lib/cjs/components/config.umd.js.map +1 -0
  25. package/lib/cjs/components/constants.js +15 -0
  26. package/lib/cjs/components/constants.js.map +1 -0
  27. package/lib/cjs/components/datatable/datatable.js +1464 -0
  28. package/lib/cjs/components/datatable/datatable.js.map +1 -0
  29. package/lib/cjs/components/datatable/index.js +6 -0
  30. package/lib/cjs/components/datatable/index.js.map +1 -0
  31. package/lib/cjs/components/datatable/types.js +3 -0
  32. package/lib/cjs/components/datatable/types.js.map +1 -0
  33. package/lib/cjs/components/dismiss/dismiss.js +131 -0
  34. package/lib/cjs/components/dismiss/dismiss.js.map +1 -0
  35. package/lib/cjs/components/dismiss/index.js +6 -0
  36. package/lib/cjs/components/dismiss/index.js.map +1 -0
  37. package/lib/cjs/components/dismiss/types.js +3 -0
  38. package/lib/cjs/components/dismiss/types.js.map +1 -0
  39. package/lib/cjs/components/drawer/drawer.js +347 -0
  40. package/lib/cjs/components/drawer/drawer.js.map +1 -0
  41. package/lib/cjs/components/drawer/index.js +6 -0
  42. package/lib/cjs/components/drawer/index.js.map +1 -0
  43. package/lib/cjs/components/drawer/types.js +3 -0
  44. package/lib/cjs/components/drawer/types.js.map +1 -0
  45. package/lib/cjs/components/dropdown/dropdown.js +403 -0
  46. package/lib/cjs/components/dropdown/dropdown.js.map +1 -0
  47. package/lib/cjs/components/dropdown/index.js +6 -0
  48. package/lib/cjs/components/dropdown/index.js.map +1 -0
  49. package/lib/cjs/components/dropdown/types.js +3 -0
  50. package/lib/cjs/components/dropdown/types.js.map +1 -0
  51. package/lib/cjs/components/image-input/image-input.js +191 -0
  52. package/lib/cjs/components/image-input/image-input.js.map +1 -0
  53. package/lib/cjs/components/image-input/index.js +6 -0
  54. package/lib/cjs/components/image-input/index.js.map +1 -0
  55. package/lib/cjs/components/image-input/types.js +3 -0
  56. package/lib/cjs/components/image-input/types.js.map +1 -0
  57. package/lib/cjs/components/menu/index.js +6 -0
  58. package/lib/cjs/components/menu/index.js.map +1 -0
  59. package/lib/cjs/components/menu/menu.js +1021 -0
  60. package/lib/cjs/components/menu/menu.js.map +1 -0
  61. package/lib/cjs/components/menu/types.js +3 -0
  62. package/lib/cjs/components/menu/types.js.map +1 -0
  63. package/lib/cjs/components/modal/index.js +6 -0
  64. package/lib/cjs/components/modal/index.js.map +1 -0
  65. package/lib/cjs/components/modal/modal.js +316 -0
  66. package/lib/cjs/components/modal/modal.js.map +1 -0
  67. package/lib/cjs/components/modal/types.js +3 -0
  68. package/lib/cjs/components/modal/types.js.map +1 -0
  69. package/lib/cjs/components/reparent/index.js +6 -0
  70. package/lib/cjs/components/reparent/index.js.map +1 -0
  71. package/lib/cjs/components/reparent/reparent.js +93 -0
  72. package/lib/cjs/components/reparent/reparent.js.map +1 -0
  73. package/lib/cjs/components/reparent/types.js +3 -0
  74. package/lib/cjs/components/reparent/types.js.map +1 -0
  75. package/lib/cjs/components/scrollable/index.js +6 -0
  76. package/lib/cjs/components/scrollable/index.js.map +1 -0
  77. package/lib/cjs/components/scrollable/scrollable.js +259 -0
  78. package/lib/cjs/components/scrollable/scrollable.js.map +1 -0
  79. package/lib/cjs/components/scrollable/types.js +3 -0
  80. package/lib/cjs/components/scrollable/types.js.map +1 -0
  81. package/lib/cjs/components/scrollspy/index.js +6 -0
  82. package/lib/cjs/components/scrollspy/index.js.map +1 -0
  83. package/lib/cjs/components/scrollspy/scrollspy.js +174 -0
  84. package/lib/cjs/components/scrollspy/scrollspy.js.map +1 -0
  85. package/lib/cjs/components/scrollspy/types.js +3 -0
  86. package/lib/cjs/components/scrollspy/types.js.map +1 -0
  87. package/lib/cjs/components/scrollto/index.js +6 -0
  88. package/lib/cjs/components/scrollto/index.js.map +1 -0
  89. package/lib/cjs/components/scrollto/scrollto.js +103 -0
  90. package/lib/cjs/components/scrollto/scrollto.js.map +1 -0
  91. package/lib/cjs/components/scrollto/types.js +3 -0
  92. package/lib/cjs/components/scrollto/types.js.map +1 -0
  93. package/lib/cjs/components/stepper/index.js +6 -0
  94. package/lib/cjs/components/stepper/index.js.map +1 -0
  95. package/lib/cjs/components/stepper/stepper.js +258 -0
  96. package/lib/cjs/components/stepper/stepper.js.map +1 -0
  97. package/lib/cjs/components/stepper/types.js +3 -0
  98. package/lib/cjs/components/stepper/types.js.map +1 -0
  99. package/lib/cjs/components/sticky/index.js +6 -0
  100. package/lib/cjs/components/sticky/index.js.map +1 -0
  101. package/lib/cjs/components/sticky/sticky.js +297 -0
  102. package/lib/cjs/components/sticky/sticky.js.map +1 -0
  103. package/lib/cjs/components/sticky/types.js +3 -0
  104. package/lib/cjs/components/sticky/types.js.map +1 -0
  105. package/lib/cjs/components/tabs/index.js +6 -0
  106. package/lib/cjs/components/tabs/index.js.map +1 -0
  107. package/lib/cjs/components/tabs/tabs.js +146 -0
  108. package/lib/cjs/components/tabs/tabs.js.map +1 -0
  109. package/lib/cjs/components/tabs/types.js +3 -0
  110. package/lib/cjs/components/tabs/types.js.map +1 -0
  111. package/lib/cjs/components/theme/index.js +6 -0
  112. package/lib/cjs/components/theme/index.js.map +1 -0
  113. package/lib/cjs/components/theme/theme.js +147 -0
  114. package/lib/cjs/components/theme/theme.js.map +1 -0
  115. package/lib/cjs/components/theme/types.js +3 -0
  116. package/lib/cjs/components/theme/types.js.map +1 -0
  117. package/lib/cjs/components/toggle/index.js +6 -0
  118. package/lib/cjs/components/toggle/index.js.map +1 -0
  119. package/lib/cjs/components/toggle/toggle.js +139 -0
  120. package/lib/cjs/components/toggle/toggle.js.map +1 -0
  121. package/lib/cjs/components/toggle/types.js +3 -0
  122. package/lib/cjs/components/toggle/types.js.map +1 -0
  123. package/lib/cjs/components/toggle-password/index.js +6 -0
  124. package/lib/cjs/components/toggle-password/index.js.map +1 -0
  125. package/lib/cjs/components/toggle-password/toggle-password.js +131 -0
  126. package/lib/cjs/components/toggle-password/toggle-password.js.map +1 -0
  127. package/lib/cjs/components/toggle-password/types.js +3 -0
  128. package/lib/cjs/components/toggle-password/types.js.map +1 -0
  129. package/lib/cjs/components/tooltip/index.js +6 -0
  130. package/lib/cjs/components/tooltip/index.js.map +1 -0
  131. package/lib/cjs/components/tooltip/tooltip.js +271 -0
  132. package/lib/cjs/components/tooltip/tooltip.js.map +1 -0
  133. package/lib/cjs/components/tooltip/types.js +3 -0
  134. package/lib/cjs/components/tooltip/types.js.map +1 -0
  135. package/lib/cjs/helpers/data.js +33 -0
  136. package/lib/cjs/helpers/data.js.map +1 -0
  137. package/lib/cjs/helpers/dom.js +297 -0
  138. package/lib/cjs/helpers/dom.js.map +1 -0
  139. package/lib/cjs/helpers/event-handler.js +36 -0
  140. package/lib/cjs/helpers/event-handler.js.map +1 -0
  141. package/lib/cjs/helpers/utils.js +94 -0
  142. package/lib/cjs/helpers/utils.js.map +1 -0
  143. package/lib/cjs/index.js +105 -0
  144. package/lib/cjs/index.js.map +1 -0
  145. package/lib/cjs/types.js +3 -0
  146. package/lib/cjs/types.js.map +1 -0
  147. package/lib/esm/components/accordion/accordion.js +165 -0
  148. package/lib/esm/components/accordion/accordion.js.map +1 -0
  149. package/lib/esm/components/accordion/index.js +2 -0
  150. package/lib/esm/components/accordion/index.js.map +1 -0
  151. package/lib/esm/components/accordion/types.js +2 -0
  152. package/lib/esm/components/accordion/types.js.map +1 -0
  153. package/lib/esm/components/collapse/collapse.js +166 -0
  154. package/lib/esm/components/collapse/collapse.js.map +1 -0
  155. package/lib/esm/components/collapse/index.js +2 -0
  156. package/lib/esm/components/collapse/index.js.map +1 -0
  157. package/lib/esm/components/collapse/types.js +2 -0
  158. package/lib/esm/components/collapse/types.js.map +1 -0
  159. package/lib/esm/components/component.js +133 -0
  160. package/lib/esm/components/component.js.map +1 -0
  161. package/lib/esm/components/config.js +24 -0
  162. package/lib/esm/components/config.js.map +1 -0
  163. package/lib/esm/components/config.umd.js +23 -0
  164. package/lib/esm/components/config.umd.js.map +1 -0
  165. package/lib/esm/components/constants.js +12 -0
  166. package/lib/esm/components/constants.js.map +1 -0
  167. package/lib/esm/components/datatable/datatable.js +1461 -0
  168. package/lib/esm/components/datatable/datatable.js.map +1 -0
  169. package/lib/esm/components/datatable/index.js +2 -0
  170. package/lib/esm/components/datatable/index.js.map +1 -0
  171. package/lib/esm/components/datatable/types.js +2 -0
  172. package/lib/esm/components/datatable/types.js.map +1 -0
  173. package/lib/esm/components/dismiss/dismiss.js +128 -0
  174. package/lib/esm/components/dismiss/dismiss.js.map +1 -0
  175. package/lib/esm/components/dismiss/index.js +2 -0
  176. package/lib/esm/components/dismiss/index.js.map +1 -0
  177. package/lib/esm/components/dismiss/types.js +2 -0
  178. package/lib/esm/components/dismiss/types.js.map +1 -0
  179. package/lib/esm/components/drawer/drawer.js +344 -0
  180. package/lib/esm/components/drawer/drawer.js.map +1 -0
  181. package/lib/esm/components/drawer/index.js +2 -0
  182. package/lib/esm/components/drawer/index.js.map +1 -0
  183. package/lib/esm/components/drawer/types.js +2 -0
  184. package/lib/esm/components/drawer/types.js.map +1 -0
  185. package/lib/esm/components/dropdown/dropdown.js +400 -0
  186. package/lib/esm/components/dropdown/dropdown.js.map +1 -0
  187. package/lib/esm/components/dropdown/index.js +2 -0
  188. package/lib/esm/components/dropdown/index.js.map +1 -0
  189. package/lib/esm/components/dropdown/types.js +2 -0
  190. package/lib/esm/components/dropdown/types.js.map +1 -0
  191. package/lib/esm/components/image-input/image-input.js +188 -0
  192. package/lib/esm/components/image-input/image-input.js.map +1 -0
  193. package/lib/esm/components/image-input/index.js +2 -0
  194. package/lib/esm/components/image-input/index.js.map +1 -0
  195. package/lib/esm/components/image-input/types.js +2 -0
  196. package/lib/esm/components/image-input/types.js.map +1 -0
  197. package/lib/esm/components/menu/index.js +2 -0
  198. package/lib/esm/components/menu/index.js.map +1 -0
  199. package/lib/esm/components/menu/menu.js +1018 -0
  200. package/lib/esm/components/menu/menu.js.map +1 -0
  201. package/lib/esm/components/menu/types.js +2 -0
  202. package/lib/esm/components/menu/types.js.map +1 -0
  203. package/lib/esm/components/modal/index.js +2 -0
  204. package/lib/esm/components/modal/index.js.map +1 -0
  205. package/lib/esm/components/modal/modal.js +313 -0
  206. package/lib/esm/components/modal/modal.js.map +1 -0
  207. package/lib/esm/components/modal/types.js +2 -0
  208. package/lib/esm/components/modal/types.js.map +1 -0
  209. package/lib/esm/components/reparent/index.js +2 -0
  210. package/lib/esm/components/reparent/index.js.map +1 -0
  211. package/lib/esm/components/reparent/reparent.js +90 -0
  212. package/lib/esm/components/reparent/reparent.js.map +1 -0
  213. package/lib/esm/components/reparent/types.js +2 -0
  214. package/lib/esm/components/reparent/types.js.map +1 -0
  215. package/lib/esm/components/scrollable/index.js +2 -0
  216. package/lib/esm/components/scrollable/index.js.map +1 -0
  217. package/lib/esm/components/scrollable/scrollable.js +256 -0
  218. package/lib/esm/components/scrollable/scrollable.js.map +1 -0
  219. package/lib/esm/components/scrollable/types.js +2 -0
  220. package/lib/esm/components/scrollable/types.js.map +1 -0
  221. package/lib/esm/components/scrollspy/index.js +2 -0
  222. package/lib/esm/components/scrollspy/index.js.map +1 -0
  223. package/lib/esm/components/scrollspy/scrollspy.js +171 -0
  224. package/lib/esm/components/scrollspy/scrollspy.js.map +1 -0
  225. package/lib/esm/components/scrollspy/types.js +2 -0
  226. package/lib/esm/components/scrollspy/types.js.map +1 -0
  227. package/lib/esm/components/scrollto/index.js +2 -0
  228. package/lib/esm/components/scrollto/index.js.map +1 -0
  229. package/lib/esm/components/scrollto/scrollto.js +100 -0
  230. package/lib/esm/components/scrollto/scrollto.js.map +1 -0
  231. package/lib/esm/components/scrollto/types.js +2 -0
  232. package/lib/esm/components/scrollto/types.js.map +1 -0
  233. package/lib/esm/components/stepper/index.js +2 -0
  234. package/lib/esm/components/stepper/index.js.map +1 -0
  235. package/lib/esm/components/stepper/stepper.js +255 -0
  236. package/lib/esm/components/stepper/stepper.js.map +1 -0
  237. package/lib/esm/components/stepper/types.js +2 -0
  238. package/lib/esm/components/stepper/types.js.map +1 -0
  239. package/lib/esm/components/sticky/index.js +2 -0
  240. package/lib/esm/components/sticky/index.js.map +1 -0
  241. package/lib/esm/components/sticky/sticky.js +294 -0
  242. package/lib/esm/components/sticky/sticky.js.map +1 -0
  243. package/lib/esm/components/sticky/types.js +2 -0
  244. package/lib/esm/components/sticky/types.js.map +1 -0
  245. package/lib/esm/components/tabs/index.js +2 -0
  246. package/lib/esm/components/tabs/index.js.map +1 -0
  247. package/lib/esm/components/tabs/tabs.js +143 -0
  248. package/lib/esm/components/tabs/tabs.js.map +1 -0
  249. package/lib/esm/components/tabs/types.js +2 -0
  250. package/lib/esm/components/tabs/types.js.map +1 -0
  251. package/lib/esm/components/theme/index.js +2 -0
  252. package/lib/esm/components/theme/index.js.map +1 -0
  253. package/lib/esm/components/theme/theme.js +144 -0
  254. package/lib/esm/components/theme/theme.js.map +1 -0
  255. package/lib/esm/components/theme/types.js +2 -0
  256. package/lib/esm/components/theme/types.js.map +1 -0
  257. package/lib/esm/components/toggle/index.js +2 -0
  258. package/lib/esm/components/toggle/index.js.map +1 -0
  259. package/lib/esm/components/toggle/toggle.js +136 -0
  260. package/lib/esm/components/toggle/toggle.js.map +1 -0
  261. package/lib/esm/components/toggle/types.js +2 -0
  262. package/lib/esm/components/toggle/types.js.map +1 -0
  263. package/lib/esm/components/toggle-password/index.js +2 -0
  264. package/lib/esm/components/toggle-password/index.js.map +1 -0
  265. package/lib/esm/components/toggle-password/toggle-password.js +128 -0
  266. package/lib/esm/components/toggle-password/toggle-password.js.map +1 -0
  267. package/lib/esm/components/toggle-password/types.js +2 -0
  268. package/lib/esm/components/toggle-password/types.js.map +1 -0
  269. package/lib/esm/components/tooltip/index.js +2 -0
  270. package/lib/esm/components/tooltip/index.js.map +1 -0
  271. package/lib/esm/components/tooltip/tooltip.js +268 -0
  272. package/lib/esm/components/tooltip/tooltip.js.map +1 -0
  273. package/lib/esm/components/tooltip/types.js +2 -0
  274. package/lib/esm/components/tooltip/types.js.map +1 -0
  275. package/lib/esm/helpers/data.js +31 -0
  276. package/lib/esm/helpers/data.js.map +1 -0
  277. package/lib/esm/helpers/dom.js +295 -0
  278. package/lib/esm/helpers/dom.js.map +1 -0
  279. package/lib/esm/helpers/event-handler.js +34 -0
  280. package/lib/esm/helpers/event-handler.js.map +1 -0
  281. package/lib/esm/helpers/utils.js +92 -0
  282. package/lib/esm/helpers/utils.js.map +1 -0
  283. package/lib/esm/index.js +79 -0
  284. package/lib/esm/index.js.map +1 -0
  285. package/lib/esm/types.js +2 -0
  286. package/lib/esm/types.js.map +1 -0
  287. package/package.json +85 -0
  288. package/prettier.config.js +9 -0
  289. package/src/components/accordion/accordion-menu.css +51 -0
  290. package/src/components/accordion/accordion.css +86 -0
  291. package/src/components/accordion/accordion.ts +221 -0
  292. package/src/components/accordion/index.ts +7 -0
  293. package/src/components/accordion/types.ts +16 -0
  294. package/src/components/alert/alert.css +282 -0
  295. package/src/components/avatar/avatar.css +46 -0
  296. package/src/components/badge/badge.css +176 -0
  297. package/src/components/breadcrumb/breadcrumb.css +38 -0
  298. package/src/components/btn/btn.css +227 -0
  299. package/src/components/card/card.css +158 -0
  300. package/src/components/checkbox/checkbox.css +74 -0
  301. package/src/components/collapse/collapse.css +14 -0
  302. package/src/components/collapse/collapse.ts +200 -0
  303. package/src/components/collapse/index.ts +7 -0
  304. package/src/components/collapse/types.ts +16 -0
  305. package/src/components/component.ts +132 -0
  306. package/src/components/constants.ts +16 -0
  307. package/src/components/datatable/datatable-checkbox.ts +236 -0
  308. package/src/components/datatable/datatable-sort.ts +154 -0
  309. package/src/components/datatable/datatable.css +110 -0
  310. package/src/components/datatable/datatable.ts +1657 -0
  311. package/src/components/datatable/index.ts +19 -0
  312. package/src/components/datatable/types.ts +203 -0
  313. package/src/components/datepicker/calendar.ts +1397 -0
  314. package/src/components/datepicker/config.ts +368 -0
  315. package/src/components/datepicker/datepicker.css +7 -0
  316. package/src/components/datepicker/datepicker.ts +1287 -0
  317. package/src/components/datepicker/dropdown.ts +757 -0
  318. package/src/components/datepicker/events.ts +149 -0
  319. package/src/components/datepicker/index.ts +10 -0
  320. package/src/components/datepicker/keyboard.ts +646 -0
  321. package/src/components/datepicker/locales.ts +80 -0
  322. package/src/components/datepicker/templates.ts +792 -0
  323. package/src/components/datepicker/types.ts +154 -0
  324. package/src/components/datepicker/utils.ts +631 -0
  325. package/src/components/dismiss/dismiss.css +10 -0
  326. package/src/components/dismiss/dismiss.ts +152 -0
  327. package/src/components/dismiss/index.ts +7 -0
  328. package/src/components/dismiss/types.ts +17 -0
  329. package/src/components/drawer/drawer.css +97 -0
  330. package/src/components/drawer/drawer.ts +437 -0
  331. package/src/components/drawer/index.ts +7 -0
  332. package/src/components/drawer/types.ts +25 -0
  333. package/src/components/dropdown/dropdown-menu.css +56 -0
  334. package/src/components/dropdown/dropdown.css +46 -0
  335. package/src/components/dropdown/dropdown.ts +549 -0
  336. package/src/components/dropdown/index.ts +7 -0
  337. package/src/components/dropdown/types.ts +28 -0
  338. package/src/components/form/form.css +54 -0
  339. package/src/components/image-input/image-input.css +56 -0
  340. package/src/components/image-input/image-input.ts +249 -0
  341. package/src/components/image-input/index.ts +10 -0
  342. package/src/components/image-input/types.ts +12 -0
  343. package/src/components/input/input-group.css +42 -0
  344. package/src/components/input/input.css +136 -0
  345. package/src/components/kbd/kbd.css +30 -0
  346. package/src/components/label/label.css +20 -0
  347. package/src/components/link/link.css +81 -0
  348. package/src/components/modal/index.ts +7 -0
  349. package/src/components/modal/modal.css +73 -0
  350. package/src/components/modal/modal.ts +382 -0
  351. package/src/components/modal/types.ts +21 -0
  352. package/src/components/pagination/pagination.css +26 -0
  353. package/src/components/popover/popover.css +22 -0
  354. package/src/components/progress/progress.css +51 -0
  355. package/src/components/radio/radio.css +51 -0
  356. package/src/components/reparent/index.ts +7 -0
  357. package/src/components/reparent/reparent.ts +109 -0
  358. package/src/components/reparent/types.ts +15 -0
  359. package/src/components/scrollable/index.ts +10 -0
  360. package/src/components/scrollable/scrollable.css +29 -0
  361. package/src/components/scrollable/scrollable.ts +297 -0
  362. package/src/components/scrollable/types.ts +16 -0
  363. package/src/components/scrollspy/index.ts +7 -0
  364. package/src/components/scrollspy/scrollspy.css +13 -0
  365. package/src/components/scrollspy/scrollspy.ts +224 -0
  366. package/src/components/scrollspy/types.ts +15 -0
  367. package/src/components/scrollto/index.ts +7 -0
  368. package/src/components/scrollto/scrollto.ts +127 -0
  369. package/src/components/scrollto/types.ts +15 -0
  370. package/src/components/select/combobox.ts +305 -0
  371. package/src/components/select/config.ts +324 -0
  372. package/src/components/select/dropdown.ts +510 -0
  373. package/src/components/select/index.ts +13 -0
  374. package/src/components/select/option.ts +43 -0
  375. package/src/components/select/remote.ts +477 -0
  376. package/src/components/select/search.ts +430 -0
  377. package/src/components/select/select.css +105 -0
  378. package/src/components/select/select.ts +1916 -0
  379. package/src/components/select/tags.ts +123 -0
  380. package/src/components/select/templates.ts +531 -0
  381. package/src/components/select/types.ts +36 -0
  382. package/src/components/select/utils.ts +747 -0
  383. package/src/components/select/variants.css +5 -0
  384. package/src/components/separator/separator.css +14 -0
  385. package/src/components/skeleton/skeleton.css +10 -0
  386. package/src/components/stepper/index.ts +7 -0
  387. package/src/components/stepper/stepper.css +49 -0
  388. package/src/components/stepper/stepper.ts +308 -0
  389. package/src/components/stepper/types.ts +13 -0
  390. package/src/components/sticky/index.ts +7 -0
  391. package/src/components/sticky/sticky.css +22 -0
  392. package/src/components/sticky/sticky.ts +381 -0
  393. package/src/components/sticky/types.ts +23 -0
  394. package/src/components/switch/switch.css +110 -0
  395. package/src/components/table/table.css +168 -0
  396. package/src/components/tabs/index.ts +7 -0
  397. package/src/components/tabs/tabs.css +40 -0
  398. package/src/components/tabs/tabs.ts +190 -0
  399. package/src/components/tabs/types.ts +13 -0
  400. package/src/components/textarea/textarea.css +35 -0
  401. package/src/components/theme-switch/index.ts +10 -0
  402. package/src/components/theme-switch/theme-switch.css +22 -0
  403. package/src/components/theme-switch/theme-switch.ts +176 -0
  404. package/src/components/theme-switch/types.ts +15 -0
  405. package/src/components/toggle/index.ts +7 -0
  406. package/src/components/toggle/toggle.css +13 -0
  407. package/src/components/toggle/toggle.ts +173 -0
  408. package/src/components/toggle/types.ts +18 -0
  409. package/src/components/toggle-group/toggle-group.css +55 -0
  410. package/src/components/toggle-password/index.ts +10 -0
  411. package/src/components/toggle-password/toggle-password.css +13 -0
  412. package/src/components/toggle-password/toggle-password.ts +159 -0
  413. package/src/components/toggle-password/types.ts +13 -0
  414. package/src/components/tooltip/index.ts +7 -0
  415. package/src/components/tooltip/tooltip.css +18 -0
  416. package/src/components/tooltip/tooltip.ts +361 -0
  417. package/src/components/tooltip/types.ts +28 -0
  418. package/src/helpers/data.ts +46 -0
  419. package/src/helpers/dom.ts +405 -0
  420. package/src/helpers/event-handler.ts +61 -0
  421. package/src/helpers/utils.ts +183 -0
  422. package/src/index.ts +113 -0
  423. package/src/types.ts +23 -0
  424. package/styles.css +48 -0
  425. package/tsconfig.json +17 -0
  426. package/webpack.config.js +113 -0
@@ -0,0 +1,1916 @@
1
+ /**
2
+ * KTUI - Free & Open-Source Tailwind UI Components by Keenthemes
3
+ * Copyright 2025 by Keenthemes Inc
4
+ * @version: 1.0.0
5
+ */
6
+ import KTData from '../../helpers/data';
7
+ import KTDom from '../../helpers/dom';
8
+ import KTComponent from '../component';
9
+ import {
10
+ KTSelectConfigInterface,
11
+ KTSelectState,
12
+ KTSelectOption as KTSelectOptionData,
13
+ } from './config';
14
+ import { KTSelectOption } from './option';
15
+ import { KTSelectRemote } from './remote';
16
+ import { KTSelectSearch } from './search';
17
+ import { defaultTemplates } from './templates';
18
+ import { KTSelectCombobox } from './combobox';
19
+ import { KTSelectDropdown } from './dropdown';
20
+ import {
21
+ handleDropdownKeyNavigation,
22
+ filterOptions,
23
+ FocusManager,
24
+ EventManager,
25
+ } from './utils';
26
+ import { KTSelectTags } from './tags';
27
+ import { SelectMode } from './types';
28
+
29
+ export class KTSelect extends KTComponent {
30
+ // Core properties
31
+ protected override readonly _name: string = 'select';
32
+ protected override readonly _dataOptionPrefix: string = 'kt-'; // Use 'kt-' prefix to support data-kt-select-option attributes
33
+ protected override readonly _config: KTSelectConfigInterface;
34
+ protected override _defaultConfig: KTSelectConfigInterface;
35
+
36
+ // DOM elements
37
+ private _wrapperElement: HTMLElement;
38
+ private _displayElement: HTMLElement;
39
+ private _dropdownContentElement: HTMLElement;
40
+ private _searchInputElement: HTMLInputElement | null;
41
+ private _valueDisplayElement: HTMLElement;
42
+ private _options: NodeListOf<HTMLElement>;
43
+
44
+ // State
45
+ private _dropdownIsOpen: boolean = false;
46
+ private _state: KTSelectState;
47
+ private _searchModule: KTSelectSearch;
48
+ private _remoteModule: KTSelectRemote;
49
+ private _comboboxModule: KTSelectCombobox | null = null;
50
+ private _tagsModule: KTSelectTags | null = null;
51
+ private _dropdownModule: KTSelectDropdown | null = null;
52
+ private _loadMoreIndicator: HTMLElement | null = null;
53
+ private _focusManager: FocusManager;
54
+ private _eventManager: EventManager;
55
+
56
+ /**
57
+ * Constructor: Initializes the select component
58
+ */
59
+ constructor(element: HTMLElement, config?: KTSelectConfigInterface) {
60
+ super();
61
+
62
+ if (KTData.has(element, this._name)) {
63
+ return;
64
+ }
65
+
66
+ this._init(element);
67
+ this._buildConfig(config);
68
+
69
+ this._state = new KTSelectState(this._config);
70
+ this._config = this._state.getConfig();
71
+
72
+ (element as any).instance = this;
73
+
74
+ // Initialize event manager
75
+ this._eventManager = new EventManager();
76
+
77
+ // Initialize remote module if remote data is enabled
78
+ if (this._config.remote) {
79
+ this._remoteModule = new KTSelectRemote(this._config, this._element);
80
+ this._initializeRemoteData();
81
+ } else {
82
+ this._state
83
+ .setItems()
84
+ .then(() => {
85
+ if (this._config.debug)
86
+ console.log('Setting up component after remote data is loaded');
87
+ this._setupComponent();
88
+ })
89
+ .catch((error) => {
90
+ console.error('Error setting items:', error);
91
+ // Handle the error, e.g., display an error message to the user
92
+ });
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Initialize remote data fetching
98
+ */
99
+ private _initializeRemoteData() {
100
+ if (!this._remoteModule || !this._config.remote) return;
101
+
102
+ if (this._config.debug)
103
+ console.log('Initializing remote data with URL:', this._config.dataUrl);
104
+
105
+ // Show loading state
106
+ this._renderLoadingState();
107
+
108
+ // Fetch remote data
109
+ this._remoteModule
110
+ .fetchData()
111
+ .then((items) => {
112
+ if (this._config.debug) console.log('Remote data fetched:', items);
113
+
114
+ // Remove placeholder/loading options before setting new items
115
+ this._clearExistingOptions();
116
+
117
+ // Update state with fetched items
118
+ this._state
119
+ .setItems(items)
120
+ .then(() => {
121
+ // Generate options from the fetched data
122
+ this._generateOptionsHtml(this._element);
123
+
124
+ if (this._config.debug)
125
+ console.log('Generating options HTML from remote data');
126
+ this._setupComponent();
127
+
128
+ // Add pagination "Load More" button if needed
129
+ if (this._config.pagination && this._remoteModule.hasMorePages()) {
130
+ this._addLoadMoreButton();
131
+ }
132
+ })
133
+ .catch((error) => {
134
+ console.error('Error setting items:', error);
135
+ this._renderErrorState(error.message || 'Failed to load data');
136
+ });
137
+ })
138
+ .catch((error) => {
139
+ console.error('Error fetching remote data:', error);
140
+ this._renderErrorState(
141
+ this._remoteModule.getErrorMessage() || 'Failed to load data',
142
+ );
143
+ });
144
+ }
145
+
146
+ /**
147
+ * Clear existing options from the select element
148
+ */
149
+ private _clearExistingOptions() {
150
+ // Keep only the empty/placeholder option and remove the rest
151
+ const options = Array.from(
152
+ this._element.querySelectorAll('option:not([value=""])'),
153
+ );
154
+ options.forEach((option) => option.remove());
155
+
156
+ // Ensure we have at least an empty option
157
+ if (this._element.querySelectorAll('option').length === 0) {
158
+ const emptyOption = defaultTemplates.emptyOption({
159
+ ...this._config,
160
+ placeholder: this._config.placeholder,
161
+ });
162
+ this._element.appendChild(emptyOption);
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Helper to show a dropdown message (error, loading, noResults)
168
+ */
169
+ private _showDropdownMessage(
170
+ type: 'error' | 'loading' | 'noResults',
171
+ message?: string,
172
+ ) {
173
+ if (!this._dropdownContentElement) return;
174
+ const optionsContainer = this._dropdownContentElement.querySelector(
175
+ '[data-kt-select-options-container]',
176
+ );
177
+ if (!optionsContainer) return;
178
+
179
+ switch (type) {
180
+ case 'error':
181
+ optionsContainer.innerHTML = defaultTemplates.error({
182
+ ...this._config,
183
+ errorMessage: message,
184
+ });
185
+ break;
186
+ case 'loading':
187
+ optionsContainer.innerHTML = defaultTemplates.loading(
188
+ this._config,
189
+ message || 'Loading...',
190
+ ).outerHTML;
191
+ break;
192
+ case 'noResults':
193
+ optionsContainer.innerHTML = '';
194
+ optionsContainer.appendChild(defaultTemplates.noResults(this._config));
195
+ break;
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Render loading state in dropdown
201
+ */
202
+ private _renderLoadingState() {
203
+ if (this._element.querySelectorAll('option').length <= 1) {
204
+ const existingLoadingOptions = this._element.querySelectorAll(
205
+ 'option[disabled][selected][value=""]',
206
+ );
207
+ existingLoadingOptions.forEach((option) => option.remove());
208
+ this._showDropdownMessage('loading', 'Loading options...');
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Render error state
214
+ * @param message Error message
215
+ */
216
+ private _renderErrorState(message: string) {
217
+ // Create error option if the select is empty
218
+ if (this._element.querySelectorAll('option').length <= 1) {
219
+ const loadingOptions = this._element.querySelectorAll(
220
+ 'option[disabled]:not([value])',
221
+ );
222
+ loadingOptions.forEach((option) => option.remove());
223
+
224
+ // Use template function for error option instead of hardcoded element
225
+ const errorOption = defaultTemplates.errorOption({
226
+ ...this._config,
227
+ errorMessage: message,
228
+ });
229
+ this._element.appendChild(errorOption);
230
+ }
231
+
232
+ // If dropdown is already created, show error message there
233
+ this._showDropdownMessage('error', message);
234
+
235
+ if (!this._wrapperElement) {
236
+ if (this._config.debug) console.log('Setting up component after error');
237
+ this._setupComponent();
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Add "Load More" button for pagination
243
+ */
244
+ private _addLoadMoreButton() {
245
+ if (!this._dropdownContentElement || !this._config.pagination) return;
246
+
247
+ // Remove existing button if any
248
+ if (this._loadMoreIndicator) {
249
+ this._loadMoreIndicator.remove();
250
+ this._loadMoreIndicator = null;
251
+ }
252
+
253
+ // Create load more button using template
254
+ this._loadMoreIndicator = defaultTemplates.loadMore(this._config);
255
+
256
+ // Add to dropdown
257
+ const optionsContainer = this._dropdownContentElement.querySelector(
258
+ '[data-kt-select-options-container]',
259
+ );
260
+ if (optionsContainer) {
261
+ optionsContainer.appendChild(this._loadMoreIndicator);
262
+ } else {
263
+ this._dropdownContentElement.appendChild(this._loadMoreIndicator);
264
+ }
265
+
266
+ // Add event listener
267
+ this._loadMoreIndicator.addEventListener(
268
+ 'click',
269
+ this._handleLoadMore.bind(this),
270
+ );
271
+ }
272
+
273
+ /**
274
+ * Handle load more button click
275
+ */
276
+ private _handleLoadMore() {
277
+ if (!this._remoteModule || !this._config.pagination) return;
278
+
279
+ // Show loading state
280
+ if (this._loadMoreIndicator) {
281
+ this._loadMoreIndicator.textContent = 'Loading...';
282
+ }
283
+
284
+ // Fetch next page
285
+ this._remoteModule
286
+ .loadNextPage()
287
+ .then((newItems) => {
288
+ // Get existing items
289
+ const existingItems = this._state.getItems();
290
+
291
+ // Combine new items with existing items
292
+ this._state
293
+ .setItems([...existingItems, ...newItems])
294
+ .then(() => {
295
+ // Update options in the dropdown
296
+ this._updateOptionsInDropdown(newItems);
297
+
298
+ // Check if there are more pages
299
+ if (this._remoteModule.hasMorePages()) {
300
+ // Reset load more button
301
+ if (this._loadMoreIndicator) {
302
+ this._loadMoreIndicator.textContent =
303
+ this._config.loadMoreText || 'Load more...';
304
+ }
305
+ } else {
306
+ // Remove load more button if no more pages
307
+ if (this._loadMoreIndicator) {
308
+ this._loadMoreIndicator.remove();
309
+ this._loadMoreIndicator = null;
310
+ }
311
+ }
312
+ })
313
+ .catch((error) => {
314
+ console.error('Error updating items:', error);
315
+
316
+ // Reset load more button
317
+ if (this._loadMoreIndicator) {
318
+ this._loadMoreIndicator.textContent = 'Error loading more items';
319
+ }
320
+ });
321
+ })
322
+ .catch((error) => {
323
+ console.error('Error loading more items:', error);
324
+
325
+ // Reset load more button
326
+ if (this._loadMoreIndicator) {
327
+ this._loadMoreIndicator.textContent = 'Error loading more items';
328
+ }
329
+ });
330
+ }
331
+
332
+ /**
333
+ * Update options in the dropdown
334
+ * @param newItems New items to add to the dropdown
335
+ */
336
+ private _updateOptionsInDropdown(newItems: KTSelectOptionData[]) {
337
+ if (!this._dropdownContentElement || !newItems.length) return;
338
+
339
+ const optionsContainer = this._dropdownContentElement.querySelector(
340
+ '[data-kt-select-options-container]',
341
+ );
342
+ if (!optionsContainer) return;
343
+
344
+ // Get the load more button
345
+ const loadMoreButton = optionsContainer.querySelector(
346
+ '[data-kt-select-load-more]',
347
+ );
348
+
349
+ // Process each new item
350
+ newItems.forEach((item) => {
351
+ // Create option for the original select
352
+ const selectOption = defaultTemplates.emptyOption({
353
+ ...this._config,
354
+ placeholder: item.title || 'Unnamed option',
355
+ });
356
+ selectOption.value = item.id || '';
357
+
358
+ // Add description and icon attributes if available and valid
359
+ if (
360
+ item.description &&
361
+ item.description !== 'null' &&
362
+ item.description !== 'undefined'
363
+ ) {
364
+ selectOption.setAttribute(
365
+ 'data-kt-select-option-description',
366
+ item.description,
367
+ );
368
+ }
369
+ if (item.icon && item.icon !== 'null' && item.icon !== 'undefined') {
370
+ selectOption.setAttribute('data-kt-select-option-icon', item.icon);
371
+ }
372
+
373
+ // Add the option to the original select element
374
+ this._element.appendChild(selectOption);
375
+
376
+ // Create option element for the dropdown using the KTSelectOption class
377
+ // This ensures consistent option rendering
378
+ const ktOption = new KTSelectOption(selectOption, this._config);
379
+ const renderedOption = ktOption.render();
380
+
381
+ // Add to dropdown container
382
+ if (loadMoreButton) {
383
+ // Insert before the load more button
384
+ optionsContainer.insertBefore(renderedOption, loadMoreButton);
385
+ } else {
386
+ // Append to the end
387
+ optionsContainer.appendChild(renderedOption);
388
+ }
389
+ });
390
+
391
+ // Update options NodeList to include the new options
392
+ this._options = this._wrapperElement.querySelectorAll(
393
+ `[data-kt-select-option]`,
394
+ ) as NodeListOf<HTMLElement>;
395
+
396
+ if (this._config.debug)
397
+ console.log(`Added ${newItems.length} more options to dropdown`);
398
+ }
399
+
400
+ /**
401
+ * ========================================================================
402
+ * INITIALIZATION METHODS
403
+ * ========================================================================
404
+ */
405
+
406
+ /**
407
+ * Set up the component after everything is initialized
408
+ */
409
+ private _setupComponent() {
410
+ // Setup HTML structure
411
+ this._createHtmlStructure();
412
+ this._setupElementReferences();
413
+ this._initZIndex();
414
+
415
+ // Initialize options
416
+ this._initializeOptionsHtml();
417
+ this._preSelectOptions(this._element);
418
+
419
+ // Apply disabled state if needed
420
+ this._applyInitialDisabledState();
421
+
422
+ // Initialize search if enabled
423
+ if (this._config.enableSearch) {
424
+ this._initializeSearchModule();
425
+ }
426
+
427
+ // Initialize combobox if enabled
428
+ if (this._config.mode === SelectMode.COMBOBOX) {
429
+ this._comboboxModule = new KTSelectCombobox(this);
430
+ }
431
+
432
+ // Initialize tags if enabled
433
+ if (this._config.mode === SelectMode.TAGS) {
434
+ this._tagsModule = new KTSelectTags(this);
435
+ }
436
+
437
+ // Initialize focus manager after dropdown element is created
438
+ this._focusManager = new FocusManager(
439
+ this._dropdownContentElement,
440
+ '[data-kt-select-option]',
441
+ this._config,
442
+ );
443
+
444
+ // Initialize dropdown module after all elements are created
445
+ this._dropdownModule = new KTSelectDropdown(
446
+ this._wrapperElement,
447
+ this._displayElement,
448
+ this._dropdownContentElement,
449
+ this._config,
450
+ );
451
+
452
+ // Update display and set ARIA attributes
453
+ this._updateDisplayAndAriaAttributes();
454
+ this.updateSelectedOptionDisplay();
455
+ this._setAriaAttributes();
456
+
457
+ // Attach event listeners after all modules are initialized
458
+ this._attachEventListeners();
459
+ }
460
+
461
+ /**
462
+ * Initialize options HTML from data
463
+ */
464
+ private _initializeOptionsHtml() {
465
+ this._generateOptionsHtml(this._element);
466
+ }
467
+
468
+ /**
469
+ * Creates the HTML structure for the select component
470
+ */
471
+ private _createHtmlStructure() {
472
+ const options = Array.from(this._element.querySelectorAll('option'));
473
+
474
+ // Create wrapper and display elements
475
+ const wrapperElement = defaultTemplates.main(this._config);
476
+ const displayElement = defaultTemplates.display(this._config);
477
+
478
+ // Add the display element to the wrapper
479
+ wrapperElement.appendChild(displayElement);
480
+
481
+ // Create an empty dropdown first (without options) using template
482
+ const dropdownElement = defaultTemplates.dropdownContent({
483
+ ...this._config,
484
+ zindex: this._config.dropdownZindex,
485
+ });
486
+
487
+ // Add search input if needed
488
+ const isCombobox = this._config.mode === SelectMode.COMBOBOX;
489
+ const hasSearch = this._config.enableSearch && !isCombobox;
490
+
491
+ if (hasSearch) {
492
+ const searchElement = defaultTemplates.search(this._config);
493
+ dropdownElement.appendChild(searchElement);
494
+ }
495
+
496
+ // Create options container using template
497
+ const optionsContainer = defaultTemplates.optionsContainer(this._config);
498
+
499
+ // Add each option directly to the container
500
+ options.forEach((optionElement) => {
501
+ // Skip empty placeholder options (only if BOTH value AND text are empty)
502
+ // This allows options with empty value but visible text to display in dropdown
503
+ if (
504
+ optionElement.value === '' &&
505
+ optionElement.textContent.trim() === ''
506
+ ) {
507
+ return;
508
+ }
509
+
510
+ // Create new KTSelectOption instance for each option
511
+ const selectOption = new KTSelectOption(optionElement, this._config);
512
+ const renderedOption = selectOption.render();
513
+
514
+ // Append directly to options container
515
+ optionsContainer.appendChild(renderedOption);
516
+ });
517
+
518
+ // Add options container to dropdown
519
+ dropdownElement.appendChild(optionsContainer);
520
+
521
+ // Add dropdown to wrapper
522
+ wrapperElement.appendChild(dropdownElement);
523
+
524
+ // Insert after the original element
525
+ this._element.after(wrapperElement);
526
+ this._element.style.display = 'none';
527
+ }
528
+
529
+ /**
530
+ * Setup all element references after DOM is created
531
+ */
532
+ private _setupElementReferences() {
533
+ this._wrapperElement = this._element.nextElementSibling as HTMLElement;
534
+
535
+ // Get display element
536
+ this._displayElement = this._wrapperElement.querySelector(
537
+ `[data-kt-select-display]`,
538
+ ) as HTMLElement;
539
+
540
+ // Get dropdown content element - this is critical for dropdown functionality
541
+ this._dropdownContentElement = this._wrapperElement.querySelector(
542
+ `[data-kt-select-dropdown-content]`,
543
+ ) as HTMLElement;
544
+
545
+ if (!this._dropdownContentElement) {
546
+ console.error('Dropdown content element not found', this._wrapperElement);
547
+ }
548
+
549
+ // Get search input element - this is used for the search functionality
550
+ // First check if it's in dropdown, then check if it's in display (for combobox)
551
+ this._searchInputElement = this._dropdownContentElement.querySelector(
552
+ `[data-kt-select-search]`,
553
+ ) as HTMLInputElement;
554
+
555
+ // If not found in dropdown, check if it's the display element itself (for combobox)
556
+ if (
557
+ !this._searchInputElement &&
558
+ this._config.mode === SelectMode.COMBOBOX
559
+ ) {
560
+ this._searchInputElement = this._displayElement as HTMLInputElement;
561
+ }
562
+
563
+ if (this._config.debug)
564
+ console.log(
565
+ 'Search input found:',
566
+ this._searchInputElement ? 'Yes' : 'No',
567
+ 'Mode:',
568
+ this._config.mode,
569
+ 'EnableSearch:',
570
+ this._config.enableSearch,
571
+ );
572
+
573
+ this._valueDisplayElement = this._wrapperElement.querySelector(
574
+ `[data-kt-select-value]`,
575
+ ) as HTMLElement;
576
+
577
+ this._options = this._wrapperElement.querySelectorAll(
578
+ `[data-kt-select-option]`,
579
+ ) as NodeListOf<HTMLElement>;
580
+ }
581
+
582
+ /**
583
+ * Attach all event listeners to elements
584
+ */
585
+ private _attachEventListeners() {
586
+ // Document level event listeners
587
+ document.addEventListener('click', this._handleDocumentClick.bind(this));
588
+ document.addEventListener('keydown', this._handleEscKey.bind(this));
589
+
590
+ // Dropdown option click events
591
+ this._eventManager.addListener(
592
+ this._dropdownContentElement,
593
+ 'click',
594
+ this._handleDropdownOptionClick.bind(this),
595
+ );
596
+
597
+ // Only attach click handler to display element
598
+ this._eventManager.addListener(
599
+ this._displayElement,
600
+ 'click',
601
+ this._handleDropdownClick.bind(this),
602
+ );
603
+
604
+ // Only attach keyboard navigation to display element if NOT in combobox mode
605
+ // This prevents conflicts with the combobox module's keyboard handler
606
+ if (this._config.mode !== SelectMode.COMBOBOX) {
607
+ if (this._config.debug)
608
+ console.log(
609
+ 'Attaching keyboard navigation to display element (non-combobox mode)',
610
+ );
611
+ this._eventManager.addListener(
612
+ this._displayElement,
613
+ 'keydown',
614
+ this._handleDropdownKeyDown.bind(this),
615
+ );
616
+ }
617
+ }
618
+
619
+ /**
620
+ * Initialize search module if search is enabled
621
+ */
622
+ private _initializeSearchModule() {
623
+ if (this._config.enableSearch) {
624
+ this._searchModule = new KTSelectSearch(this);
625
+ this._searchModule.init();
626
+
627
+ // If remote search is enabled, add event listener for search input
628
+ if (
629
+ this._config.remote &&
630
+ this._config.searchParam &&
631
+ this._searchInputElement
632
+ ) {
633
+ this._searchInputElement.addEventListener(
634
+ 'input',
635
+ this._handleRemoteSearch.bind(this),
636
+ );
637
+ }
638
+ }
639
+ }
640
+
641
+ /**
642
+ * Apply ARIA attributes and update display
643
+ */
644
+ private _updateDisplayAndAriaAttributes() {
645
+ this.updateSelectedOptionDisplay();
646
+ this._setAriaAttributes();
647
+ }
648
+
649
+ /**
650
+ * Apply initial disabled state if configured
651
+ */
652
+ private _applyInitialDisabledState() {
653
+ if (this._config.disabled) {
654
+ this.getElement().classList.add('disabled');
655
+ this.getElement().setAttribute('disabled', 'disabled');
656
+ this._wrapperElement.classList.add('disabled');
657
+ }
658
+ }
659
+
660
+ /**
661
+ * Generate options HTML from data items
662
+ */
663
+ private _generateOptionsHtml(element: HTMLElement) {
664
+ const items = this._state.getItems() || [];
665
+
666
+ if (this._config.debug)
667
+ console.log(`Generating options HTML from ${items.length} items`);
668
+
669
+ // Only modify options if we have items to replace them with
670
+ if (items && items.length > 0) {
671
+ // Clear existing options except the first empty one
672
+ const options = element.querySelectorAll('option:not(:first-child)');
673
+ options.forEach((option) => option.remove());
674
+
675
+ // Generate options from data
676
+ items.forEach((item) => {
677
+ const optionElement = document.createElement('option');
678
+
679
+ // Get value - use item.id directly if available, otherwise try dataValueField
680
+ let value = '';
681
+ if (item.id !== undefined) {
682
+ value = String(item.id);
683
+ } else if (this._config.dataValueField) {
684
+ const extractedValue = this._getValueByKey(
685
+ item,
686
+ this._config.dataValueField,
687
+ );
688
+ value = extractedValue !== null ? String(extractedValue) : '';
689
+ }
690
+
691
+ // Get label - use item.title directly if available, otherwise try dataFieldText
692
+ let label = '';
693
+ if (item.title !== undefined) {
694
+ label = String(item.title);
695
+ } else if (this._config.dataFieldText) {
696
+ const extractedLabel = this._getValueByKey(
697
+ item,
698
+ this._config.dataFieldText,
699
+ );
700
+ label =
701
+ extractedLabel !== null ? String(extractedLabel) : 'Unnamed option';
702
+ }
703
+
704
+ // Get description - skip if null, undefined, or "null" string
705
+ let description = null;
706
+ if (
707
+ item.description !== undefined &&
708
+ item.description !== null &&
709
+ String(item.description) !== 'null' &&
710
+ String(item.description) !== 'undefined'
711
+ ) {
712
+ description = String(item.description);
713
+ } else if (this._config.dataFieldDescription) {
714
+ const extractedDesc = this._getValueByKey(
715
+ item,
716
+ this._config.dataFieldDescription,
717
+ );
718
+ if (
719
+ extractedDesc !== null &&
720
+ extractedDesc !== undefined &&
721
+ String(extractedDesc) !== 'null' &&
722
+ String(extractedDesc) !== 'undefined'
723
+ ) {
724
+ description = String(extractedDesc);
725
+ }
726
+ }
727
+
728
+ // Get icon - skip if null, undefined, or "null" string
729
+ let icon = null;
730
+ if (
731
+ item.icon !== undefined &&
732
+ item.icon !== null &&
733
+ String(item.icon) !== 'null' &&
734
+ String(item.icon) !== 'undefined'
735
+ ) {
736
+ icon = String(item.icon);
737
+ } else if (this._config.dataFieldIcon) {
738
+ const extractedIcon = this._getValueByKey(
739
+ item,
740
+ this._config.dataFieldIcon,
741
+ );
742
+ if (
743
+ extractedIcon !== null &&
744
+ extractedIcon !== undefined &&
745
+ String(extractedIcon) !== 'null' &&
746
+ String(extractedIcon) !== 'undefined'
747
+ ) {
748
+ icon = String(extractedIcon);
749
+ }
750
+ }
751
+
752
+ // Log the extracted values for debugging
753
+ if (this._config.debug)
754
+ console.log(
755
+ `Option: value=${value}, label=${label}, desc=${description ? description : 'none'}, icon=${icon ? icon : 'none'}`,
756
+ );
757
+
758
+ // Set option attributes
759
+ optionElement.value = value;
760
+ optionElement.textContent = label || 'Unnamed option';
761
+
762
+ if (description) {
763
+ optionElement.setAttribute(
764
+ 'data-kt-select-option-description',
765
+ description,
766
+ );
767
+ }
768
+
769
+ if (icon) {
770
+ optionElement.setAttribute('data-kt-select-option-icon', icon);
771
+ }
772
+
773
+ if (item.selected) {
774
+ optionElement.setAttribute('selected', 'selected');
775
+ }
776
+
777
+ element.appendChild(optionElement);
778
+ });
779
+
780
+ if (this._config.debug)
781
+ console.log(`Added ${items.length} options to select element`);
782
+ } else {
783
+ if (this._config.debug) console.log('No items to generate options from');
784
+ }
785
+ }
786
+
787
+ /**
788
+ * Extract nested property value from object using dot notation
789
+ */
790
+ private _getValueByKey(obj: any, key: string): any {
791
+ if (!key || !obj) return null;
792
+
793
+ // Use reduce to walk through the object by splitting the key on dots
794
+ const result = key
795
+ .split('.')
796
+ .reduce((o, k) => (o && o[k] !== undefined ? o[k] : null), obj);
797
+
798
+ if (this._config.debug)
799
+ console.log(
800
+ `Extracting [${key}] from object => ${result !== null ? JSON.stringify(result) : 'null'}`,
801
+ );
802
+ return result;
803
+ }
804
+
805
+ /**
806
+ * Pre-select options that have the selected attribute
807
+ */
808
+ private _preSelectOptions(element: HTMLElement) {
809
+ // Handle options with selected attribute
810
+ Array.from(element.querySelectorAll('option[selected]')).forEach(
811
+ (option) => {
812
+ const value = (option as HTMLOptionElement).value;
813
+ this._selectOption(value);
814
+ },
815
+ );
816
+
817
+ // Handle data-kt-select-pre-selected attribute for React compatibility
818
+ const preSelectedValues = element.getAttribute(
819
+ 'data-kt-select-pre-selected',
820
+ );
821
+ if (preSelectedValues) {
822
+ const values = preSelectedValues.split(',').map((v) => v.trim());
823
+ values.forEach((value) => {
824
+ if (value) {
825
+ this._selectOption(value);
826
+ }
827
+ });
828
+ }
829
+ }
830
+
831
+ /**
832
+ * Set appropriate z-index for dropdown
833
+ */
834
+ private _initZIndex() {
835
+ let zindex: number = this._config.dropdownZindex as number;
836
+ if (
837
+ parseInt(KTDom.getCssProp(this._dropdownContentElement, 'z-index')) >
838
+ zindex
839
+ ) {
840
+ zindex = parseInt(
841
+ KTDom.getCssProp(this._dropdownContentElement, 'z-index'),
842
+ );
843
+ }
844
+ if (KTDom.getHighestZindex(this._wrapperElement) > zindex) {
845
+ zindex = KTDom.getHighestZindex(this._wrapperElement) + 1;
846
+ }
847
+ this._dropdownContentElement.style.zIndex = String(zindex);
848
+ }
849
+
850
+ /**
851
+ * ========================================================================
852
+ * DROPDOWN MANAGEMENT
853
+ * ========================================================================
854
+ */
855
+
856
+ /**
857
+ * Toggle dropdown visibility
858
+ */
859
+ public toggleDropdown() {
860
+ if (this._config.debug) console.log('toggleDropdown called');
861
+ if (this._dropdownModule) {
862
+ // Always use the dropdown module's state to determine whether to open or close
863
+ if (this._dropdownModule.isOpen()) {
864
+ if (this._config.debug) console.log('Dropdown is open, closing...');
865
+ this.closeDropdown();
866
+ } else {
867
+ if (this._config.debug) console.log('Dropdown is closed, opening...');
868
+ this.openDropdown();
869
+ }
870
+ }
871
+ }
872
+
873
+ /**
874
+ * Open the dropdown
875
+ */
876
+ public openDropdown() {
877
+ if (this._config.debug)
878
+ console.log(
879
+ 'openDropdown called, dropdownModule exists:',
880
+ !!this._dropdownModule,
881
+ );
882
+
883
+ if (!this._dropdownModule) {
884
+ if (this._config.debug)
885
+ console.log('Early return from openDropdown - module missing');
886
+ return;
887
+ }
888
+
889
+ // Don't open dropdown if the select is disabled
890
+ if (this._config.disabled) {
891
+ if (this._config.debug)
892
+ console.log('Early return from openDropdown - select is disabled');
893
+ return;
894
+ }
895
+
896
+ if (this._config.debug)
897
+ console.log('Opening dropdown via dropdownModule...');
898
+
899
+ // Set our internal flag to match what we're doing
900
+ this._dropdownIsOpen = true;
901
+
902
+ // Open the dropdown via the module
903
+ this._dropdownModule.open();
904
+
905
+ // Dispatch custom event
906
+ this._dispatchEvent('show');
907
+ this._fireEvent('show');
908
+
909
+ // Focus search input if configured and exists
910
+ if (
911
+ this._config.enableSearch &&
912
+ this._config.searchAutofocus &&
913
+ this._searchInputElement
914
+ ) {
915
+ setTimeout(() => {
916
+ this._searchInputElement.focus();
917
+ }, 50);
918
+ }
919
+
920
+ // Update ARIA states
921
+ this._setAriaAttributes();
922
+
923
+ // Focus the first selected option or first option if nothing selected
924
+ this._focusSelectedOption();
925
+ }
926
+
927
+ /**
928
+ * Close the dropdown
929
+ */
930
+ public closeDropdown() {
931
+ if (this._config.debug)
932
+ console.log(
933
+ 'closeDropdown called, dropdownModule exists:',
934
+ !!this._dropdownModule,
935
+ );
936
+
937
+ // Only check if dropdown module exists, not dropdownIsOpen flag
938
+ if (!this._dropdownModule) {
939
+ if (this._config.debug)
940
+ console.log('Early return from closeDropdown - module missing');
941
+ return;
942
+ }
943
+
944
+ // Always close by delegating to the dropdown module, which is the source of truth
945
+ if (this._config.debug)
946
+ console.log('Closing dropdown via dropdownModule...');
947
+
948
+ // Clear search input and highlights if the dropdown is closing
949
+ if (this._searchModule && this._searchInputElement) {
950
+ // Clear search input if configured to do so
951
+ if (this._config.clearSearchOnClose) {
952
+ this._searchInputElement.value = '';
953
+ }
954
+
955
+ // Always clear the highlights when dropdown closes
956
+ this._searchModule.clearSearchHighlights();
957
+ }
958
+
959
+ // Set our internal flag to match what we're doing
960
+ this._dropdownIsOpen = false;
961
+
962
+ // Call the dropdown module's close method
963
+ this._dropdownModule.close();
964
+
965
+ // Reset all focus states
966
+ if (this._focusManager) {
967
+ this._focusManager.resetFocus();
968
+ }
969
+
970
+ // Dispatch custom events
971
+ this._dispatchEvent('close');
972
+ this._fireEvent('close');
973
+
974
+ // Update ARIA states
975
+ this._setAriaAttributes();
976
+ if (this._config.debug) console.log('closeDropdown complete');
977
+ }
978
+
979
+ /**
980
+ * Update dropdown position
981
+ */
982
+ public updateDropdownPosition() {
983
+ if (this._dropdownModule) {
984
+ this._dropdownModule.updatePosition();
985
+ }
986
+ }
987
+
988
+ /**
989
+ * Focus on the first selected option if any exists in the dropdown
990
+ */
991
+ private _focusSelectedOption() {
992
+ // Get selected options
993
+ const selectedOptions = this.getSelectedOptions();
994
+ if (selectedOptions.length === 0) return;
995
+
996
+ // Get the first selected option element
997
+ const firstSelectedValue = selectedOptions[0];
998
+
999
+ // Use the FocusManager to focus on the option
1000
+ this._focusManager.focusOptionByValue(firstSelectedValue);
1001
+ }
1002
+
1003
+ /**
1004
+ * ========================================================================
1005
+ * SELECTION MANAGEMENT
1006
+ * ========================================================================
1007
+ */
1008
+
1009
+ /**
1010
+ * Select an option by value
1011
+ */
1012
+ private _selectOption(value: string) {
1013
+ // Get current selection state
1014
+ const isSelected = this._state.isSelected(value);
1015
+
1016
+ // Toggle selection in state
1017
+ if (this._config.multiple) {
1018
+ // Toggle in multiple mode
1019
+ this._state.toggleSelectedOptions(value);
1020
+ } else {
1021
+ // Set as only selection in single mode
1022
+ this._state.setSelectedOptions(value);
1023
+ }
1024
+
1025
+ // Update the original select element's option selected state
1026
+ const optionEl = Array.from(this._element.querySelectorAll('option')).find(
1027
+ (opt) => opt.value === value,
1028
+ ) as HTMLOptionElement;
1029
+
1030
+ if (optionEl) {
1031
+ if (this._config.multiple) {
1032
+ // Toggle the selection for multiple select
1033
+ optionEl.selected = !isSelected;
1034
+ } else {
1035
+ // Set as only selection for single select
1036
+ Array.from(this._element.querySelectorAll('option')).forEach((opt) => {
1037
+ (opt as HTMLOptionElement).selected = opt.value === value;
1038
+ });
1039
+ }
1040
+ }
1041
+
1042
+ // Update the visual display of selected options
1043
+ this.updateSelectedOptionDisplay();
1044
+
1045
+ // Update option classes without re-rendering the dropdown content
1046
+ this._updateSelectedOptionClass();
1047
+
1048
+ // Dispatch standard and custom change events
1049
+ this._dispatchEvent('change', {
1050
+ value: value,
1051
+ selected: !isSelected,
1052
+ selectedOptions: this.getSelectedOptions(),
1053
+ });
1054
+ this._fireEvent('change', {
1055
+ value: value,
1056
+ selected: !isSelected,
1057
+ selectedOptions: this.getSelectedOptions(),
1058
+ });
1059
+ }
1060
+
1061
+ /**
1062
+ * Update selected option display value
1063
+ */
1064
+ public updateSelectedOptionDisplay() {
1065
+ const selectedOptions = this.getSelectedOptions();
1066
+
1067
+ if (this._config.renderSelected) {
1068
+ // Use the custom renderSelected function if provided
1069
+ this._updateValueDisplay(this._config.renderSelected(selectedOptions));
1070
+ } else {
1071
+ if (selectedOptions.length === 0) {
1072
+ if (this._config.mode !== SelectMode.COMBOBOX) {
1073
+ this._updateValueDisplay(this._config.placeholder); // Use innerHTML for placeholder
1074
+ }
1075
+ } else if (this._config.multiple) {
1076
+ if (this._config.mode === SelectMode.TAGS) {
1077
+ // Use the tags module to render selected options as tags
1078
+ if (this._tagsModule) {
1079
+ this._tagsModule.updateTagsDisplay(selectedOptions);
1080
+ } else {
1081
+ // Fallback if tags module not initialized for some reason
1082
+ this._updateValueDisplay(selectedOptions.join(', '));
1083
+ }
1084
+ } else {
1085
+ // Render as comma-separated values
1086
+ const displayText = selectedOptions
1087
+ .map((option) => this._getOptionInnerHtml(option) || '')
1088
+ .join(', ');
1089
+ this._updateValueDisplay(displayText);
1090
+ }
1091
+ } else {
1092
+ const selectedOption = selectedOptions[0];
1093
+ if (selectedOption) {
1094
+ const selectedText = this._getOptionInnerHtml(selectedOption);
1095
+ this._updateValueDisplay(selectedText);
1096
+
1097
+ // Update combobox input value if in combobox mode
1098
+ if (
1099
+ this._config.mode === SelectMode.COMBOBOX &&
1100
+ this._comboboxModule
1101
+ ) {
1102
+ this._comboboxModule.updateSelectedValue(selectedText);
1103
+ }
1104
+ } else {
1105
+ this._updateValueDisplay(this._config.placeholder);
1106
+ }
1107
+ }
1108
+ }
1109
+
1110
+ // Update any debug display boxes if they exist
1111
+ this._updateDebugDisplays();
1112
+ }
1113
+
1114
+ /**
1115
+ * Update the value display element
1116
+ */
1117
+ private _updateValueDisplay(value: string) {
1118
+ if (this._config.mode === SelectMode.COMBOBOX) {
1119
+ // For combobox, we only update the hidden value element, not the input
1120
+ // The combobox module will handle updating the input value
1121
+ if (!this._comboboxModule) {
1122
+ (this._valueDisplayElement as HTMLInputElement).value = value;
1123
+ }
1124
+ } else {
1125
+ this._valueDisplayElement.innerHTML = value;
1126
+ }
1127
+ }
1128
+
1129
+ /**
1130
+ * Update debug displays if present
1131
+ */
1132
+ private _updateDebugDisplays() {
1133
+ // Check if we're in a test environment with debug boxes
1134
+ const selectId = this.getElement().id;
1135
+ if (selectId) {
1136
+ const debugElement = document.getElementById(`${selectId}-value`);
1137
+ if (debugElement) {
1138
+ const selectedOptions = this.getSelectedOptions();
1139
+
1140
+ // Format display based on selection mode
1141
+ if (this._config.multiple) {
1142
+ // For multiple selection, show comma-separated list
1143
+ debugElement.textContent =
1144
+ selectedOptions.length > 0 ? selectedOptions.join(', ') : 'None';
1145
+ } else {
1146
+ // For single selection, show just the one value
1147
+ debugElement.textContent =
1148
+ selectedOptions.length > 0 ? selectedOptions[0] : 'None';
1149
+ }
1150
+ }
1151
+ }
1152
+ }
1153
+
1154
+ /**
1155
+ * Get option inner HTML content by option value
1156
+ */
1157
+ private _getOptionInnerHtml(optionValue: string) {
1158
+ const option = Array.from(this._options).find(
1159
+ (opt) => opt.dataset.value === optionValue,
1160
+ );
1161
+ if (this._config.mode == SelectMode.COMBOBOX) {
1162
+ return option.textContent;
1163
+ }
1164
+ return option.innerHTML; // Get the entire HTML content of the option
1165
+ }
1166
+
1167
+ /**
1168
+ * Update CSS classes for selected options
1169
+ */
1170
+ private _updateSelectedOptionClass(): void {
1171
+ const allOptions = this._wrapperElement.querySelectorAll(
1172
+ `[data-kt-select-option]`,
1173
+ );
1174
+ const selectedValues = this._state.getSelectedOptions();
1175
+ const maxReached =
1176
+ typeof this._config.maxSelections === 'number' &&
1177
+ selectedValues.length >= this._config.maxSelections;
1178
+
1179
+ if (this._config.debug)
1180
+ console.log(
1181
+ 'Updating selected classes for options, selected values:',
1182
+ selectedValues,
1183
+ );
1184
+
1185
+ allOptions.forEach((option) => {
1186
+ const optionValue = option.getAttribute('data-value');
1187
+ if (!optionValue) return;
1188
+ const isSelected = selectedValues.includes(optionValue);
1189
+ if (isSelected) {
1190
+ option.classList.add('selected');
1191
+ option.setAttribute('aria-selected', 'true');
1192
+ option.classList.remove('hidden');
1193
+ option.classList.remove('disabled');
1194
+ option.removeAttribute('aria-disabled');
1195
+ } else {
1196
+ option.classList.remove('selected');
1197
+ option.setAttribute('aria-selected', 'false');
1198
+ if (maxReached) {
1199
+ option.classList.add('disabled');
1200
+ option.setAttribute('aria-disabled', 'true');
1201
+ } else {
1202
+ option.classList.remove('disabled');
1203
+ option.removeAttribute('aria-disabled');
1204
+ }
1205
+ }
1206
+ });
1207
+ }
1208
+
1209
+ /**
1210
+ * Clear all selected options
1211
+ */
1212
+ public clearSelection() {
1213
+ // Clear the current selection
1214
+ this._state.setSelectedOptions([]);
1215
+ this.updateSelectedOptionDisplay();
1216
+ this._updateSelectedOptionClass();
1217
+
1218
+ // For combobox, also clear the input value
1219
+ if (this._config.mode === SelectMode.COMBOBOX) {
1220
+ if (this._searchInputElement) {
1221
+ this._searchInputElement.value = '';
1222
+ }
1223
+
1224
+ // If combobox has a clear button, hide it
1225
+ if (this._comboboxModule) {
1226
+ // The combobox module will handle hiding the clear button
1227
+ this._comboboxModule.resetInputValueToSelection();
1228
+ }
1229
+ }
1230
+
1231
+ // Dispatch change event
1232
+ this._dispatchEvent('change');
1233
+ this._fireEvent('change');
1234
+ }
1235
+
1236
+ /**
1237
+ * Set selected options programmatically
1238
+ */
1239
+ public setSelectedOptions(options: HTMLOptionElement[]) {
1240
+ const values = Array.from(options).map((option) => option.value);
1241
+ this._state.setSelectedOptions(values);
1242
+ }
1243
+
1244
+ /**
1245
+ * ========================================================================
1246
+ * KEYBOARD NAVIGATION
1247
+ * ========================================================================
1248
+ */
1249
+
1250
+ /**
1251
+ * Handle dropdown key down events for keyboard navigation
1252
+ * Only used for standard (non-combobox) dropdowns
1253
+ */
1254
+ private _handleDropdownKeyDown(event: KeyboardEvent) {
1255
+ // Log event for debugging
1256
+ if (this._config.debug)
1257
+ console.log('Standard dropdown keydown:', event.key);
1258
+
1259
+ // Use the shared handler
1260
+ handleDropdownKeyNavigation(event, this, {
1261
+ multiple: this._config.multiple,
1262
+ closeOnSelect: this._config.closeOnSelect,
1263
+ });
1264
+ }
1265
+
1266
+ /**
1267
+ * Focus next option in dropdown
1268
+ */
1269
+ private _focusNextOption(): Element | null {
1270
+ return this._focusManager.focusNext();
1271
+ }
1272
+
1273
+ /**
1274
+ * Focus previous option in dropdown
1275
+ */
1276
+ private _focusPreviousOption(): Element | null {
1277
+ return this._focusManager.focusPrevious();
1278
+ }
1279
+
1280
+ /**
1281
+ * Apply hover/focus state to focused option
1282
+ */
1283
+ private _hoverFocusedOption(option: Element) {
1284
+ this._focusManager.applyFocus(option as HTMLElement);
1285
+ }
1286
+
1287
+ /**
1288
+ * Scroll option into view when navigating
1289
+ */
1290
+ private _scrollOptionIntoView(option: Element) {
1291
+ this._focusManager.scrollIntoView(option as HTMLElement);
1292
+ }
1293
+
1294
+ /**
1295
+ * Select the currently focused option
1296
+ */
1297
+ public selectFocusedOption() {
1298
+ const focusedOption = this._focusManager.getFocusedOption();
1299
+
1300
+ if (focusedOption) {
1301
+ const selectedValue = focusedOption.dataset.value;
1302
+
1303
+ // Extract just the title text, not including description
1304
+ let selectedText = '';
1305
+ const titleElement = focusedOption.querySelector(
1306
+ '[data-kt-option-title]',
1307
+ );
1308
+ if (titleElement) {
1309
+ // If it has a structured content with title element
1310
+ selectedText = titleElement.textContent?.trim() || '';
1311
+ } else {
1312
+ // Fallback to the whole text content
1313
+ selectedText = focusedOption.textContent?.trim() || '';
1314
+ }
1315
+
1316
+ // First trigger the selection to ensure state is updated properly
1317
+ if (selectedValue) {
1318
+ this._selectOption(selectedValue);
1319
+ }
1320
+
1321
+ // For combobox mode, update input value AFTER selection to ensure consistency
1322
+ if (this._config.mode === SelectMode.COMBOBOX && this._comboboxModule) {
1323
+ this._comboboxModule.updateSelectedValue(selectedText);
1324
+ // Also directly update the input value for immediate visual feedback
1325
+ if (this._searchInputElement) {
1326
+ this._searchInputElement.value = selectedText;
1327
+ }
1328
+ }
1329
+ }
1330
+ }
1331
+
1332
+ /**
1333
+ * ========================================================================
1334
+ * COMBOBOX SPECIFIC METHODS
1335
+ * ========================================================================
1336
+ */
1337
+
1338
+ /**
1339
+ * Handle combobox input events
1340
+ */
1341
+ private _handleComboboxInput(event: Event) {
1342
+ if (this._comboboxModule) {
1343
+ return;
1344
+ }
1345
+
1346
+ const inputElement = event.target as HTMLInputElement;
1347
+ const query = inputElement.value.toLowerCase();
1348
+
1349
+ // If dropdown isn't open, open it when user starts typing
1350
+ if (!this._dropdownIsOpen) {
1351
+ this.openDropdown();
1352
+ }
1353
+
1354
+ // Filter options based on input
1355
+ this._filterOptionsForCombobox(query);
1356
+ }
1357
+
1358
+ /**
1359
+ * Filter options for combobox based on input query
1360
+ * Uses the shared filterOptions function
1361
+ */
1362
+ private _filterOptionsForCombobox(query: string) {
1363
+ const options = Array.from(
1364
+ this._dropdownContentElement.querySelectorAll('[data-kt-select-option]'),
1365
+ ) as HTMLElement[];
1366
+
1367
+ filterOptions(options, query, this._config, this._dropdownContentElement);
1368
+ }
1369
+
1370
+ /**
1371
+ * ========================================================================
1372
+ * EVENT HANDLERS
1373
+ * ========================================================================
1374
+ */
1375
+
1376
+ /**
1377
+ * Handle display element click
1378
+ */
1379
+ private _handleDropdownClick(event: Event) {
1380
+ if (this._config.debug)
1381
+ console.log('Display element clicked', event.target);
1382
+ event.preventDefault();
1383
+ event.stopPropagation(); // Prevent event bubbling
1384
+ this.toggleDropdown();
1385
+ }
1386
+
1387
+ /**
1388
+ * Handle click within the dropdown
1389
+ */
1390
+ private _handleDropdownOptionClick(event: Event) {
1391
+ const optionElement = (event.target as HTMLElement).closest(
1392
+ `[data-kt-select-option]`,
1393
+ );
1394
+
1395
+ // If an option is clicked, handle the option click
1396
+ if (optionElement) {
1397
+ this._handleOptionClick(event);
1398
+ }
1399
+ }
1400
+
1401
+ /**
1402
+ * Handle clicking on an option in the dropdown
1403
+ */
1404
+ private _handleOptionClick(event: Event) {
1405
+ if (this._config.debug)
1406
+ console.log('_handleOptionClick called', event.target);
1407
+ event.preventDefault();
1408
+ event.stopPropagation();
1409
+
1410
+ // Find the clicked option element
1411
+ const clickedOption = (event.target as HTMLElement).closest(
1412
+ `[data-kt-select-option]`,
1413
+ ) as HTMLElement;
1414
+
1415
+ if (!clickedOption) {
1416
+ if (this._config.debug) console.log('No clicked option found');
1417
+ return;
1418
+ }
1419
+
1420
+ // Check if the option is disabled
1421
+ if (clickedOption.getAttribute('aria-disabled') === 'true') {
1422
+ if (this._config.debug) console.log('Option is disabled, ignoring click');
1423
+ return;
1424
+ }
1425
+
1426
+ // Use dataset.value to get the option value
1427
+ const optionValue = clickedOption.dataset.value;
1428
+ if (optionValue === undefined) {
1429
+ if (this._config.debug) console.log('Option value is undefined');
1430
+ return;
1431
+ }
1432
+
1433
+ if (this._config.debug) console.log('Option clicked:', optionValue);
1434
+
1435
+ // Use toggleSelection instead of _selectOption to prevent re-rendering
1436
+ this.toggleSelection(optionValue);
1437
+ }
1438
+
1439
+ /**
1440
+ * Handle document click for closing dropdown
1441
+ */
1442
+ private _handleDocumentClick(event: MouseEvent) {
1443
+ const targetElement = event.target as HTMLElement;
1444
+ // Check if the click is outside the dropdown and the display element
1445
+ if (!this._wrapperElement.contains(targetElement)) {
1446
+ this.closeDropdown();
1447
+ }
1448
+ }
1449
+
1450
+ /**
1451
+ * Handle escape key press
1452
+ */
1453
+ private _handleEscKey(event: KeyboardEvent) {
1454
+ if (event.key === 'Escape' && this._dropdownIsOpen) {
1455
+ this.closeDropdown();
1456
+ }
1457
+ }
1458
+
1459
+ /**
1460
+ * ========================================================================
1461
+ * ACCESSIBILITY METHODS
1462
+ * ========================================================================
1463
+ */
1464
+
1465
+ /**
1466
+ * Set ARIA attributes for accessibility
1467
+ */
1468
+ private _setAriaAttributes() {
1469
+ this._displayElement.setAttribute(
1470
+ 'aria-expanded',
1471
+ this._dropdownIsOpen.toString(),
1472
+ );
1473
+ }
1474
+
1475
+ /**
1476
+ * Handle focus events
1477
+ */
1478
+ private _handleFocus() {
1479
+ // Implementation pending
1480
+ }
1481
+
1482
+ /**
1483
+ * Handle blur events
1484
+ */
1485
+ private _handleBlur() {
1486
+ // Implementation pending
1487
+ }
1488
+
1489
+ /**
1490
+ * ========================================================================
1491
+ * PUBLIC API
1492
+ * ========================================================================
1493
+ */
1494
+
1495
+ /**
1496
+ * Get the search input element
1497
+ */
1498
+ public getSearchInput(): HTMLInputElement {
1499
+ return this._searchInputElement;
1500
+ }
1501
+
1502
+ /**
1503
+ * Get selected options
1504
+ */
1505
+ public getSelectedOptions() {
1506
+ return this._state.getSelectedOptions();
1507
+ }
1508
+
1509
+ /**
1510
+ * Get configuration
1511
+ */
1512
+ public getConfig(): KTSelectConfigInterface {
1513
+ return this._config;
1514
+ }
1515
+
1516
+ /**
1517
+ * Get option elements
1518
+ */
1519
+ public getOptionsElement(): NodeListOf<HTMLElement> {
1520
+ return this._options;
1521
+ }
1522
+
1523
+ /**
1524
+ * Get dropdown element
1525
+ */
1526
+ public getDropdownElement() {
1527
+ return this._dropdownContentElement;
1528
+ }
1529
+
1530
+ /**
1531
+ * Get value display element
1532
+ */
1533
+ public getValueDisplayElement() {
1534
+ return this._valueDisplayElement;
1535
+ }
1536
+
1537
+ /**
1538
+ * Show all options in the dropdown
1539
+ */
1540
+ public showAllOptions() {
1541
+ // Get all options in the dropdown
1542
+ const options = Array.from(
1543
+ this._wrapperElement.querySelectorAll(`[data-kt-select-option]`),
1544
+ );
1545
+
1546
+ // Show all options by removing the hidden class and any inline styles
1547
+ options.forEach((option) => {
1548
+ // Remove hidden class
1549
+ option.classList.remove('hidden');
1550
+
1551
+ // Clean up any existing inline styles for backward compatibility
1552
+ if (option.hasAttribute('style')) {
1553
+ const styleAttr = option.getAttribute('style');
1554
+
1555
+ if (styleAttr.includes('display:')) {
1556
+ // If style only contains display property, remove the entire attribute
1557
+ if (
1558
+ styleAttr.trim() === 'display: none;' ||
1559
+ styleAttr.trim() === 'display: block;'
1560
+ ) {
1561
+ option.removeAttribute('style');
1562
+ } else {
1563
+ // Otherwise, remove just the display property
1564
+ option.setAttribute(
1565
+ 'style',
1566
+ styleAttr.replace(/display:\s*[^;]+;?/gi, '').trim(),
1567
+ );
1568
+ }
1569
+ }
1570
+ }
1571
+ });
1572
+
1573
+ // If search input exists, clear it
1574
+ if (this._searchInputElement && this._config.mode !== SelectMode.COMBOBOX) {
1575
+ this._searchInputElement.value = '';
1576
+ // If we have a search module, clear any search filtering
1577
+ if (this._searchModule) {
1578
+ this._searchModule.clearSearchHighlights();
1579
+ }
1580
+ }
1581
+ }
1582
+
1583
+ /**
1584
+ * Toggle multi-select functionality
1585
+ */
1586
+ public enableMultiSelect() {
1587
+ this._state.modifyConfig({ multiple: true });
1588
+ }
1589
+
1590
+ /**
1591
+ * Disable multi-select functionality
1592
+ */
1593
+ public disableMultiSelect() {
1594
+ this._state.modifyConfig({ multiple: false });
1595
+ }
1596
+
1597
+ /**
1598
+ * Toggle the selection of an option
1599
+ */
1600
+ public toggleSelection(value: string): void {
1601
+ // Get current selection state
1602
+ const isSelected = this._state.isSelected(value);
1603
+ if (this._config.debug)
1604
+ console.log(
1605
+ `toggleSelection called for value: ${value}, isSelected: ${isSelected}, multiple: ${this._config.multiple}, closeOnSelect: ${this._config.closeOnSelect}`,
1606
+ );
1607
+
1608
+ // If already selected in single select mode, do nothing (can't deselect in single select)
1609
+ if (isSelected && !this._config.multiple) {
1610
+ if (this._config.debug)
1611
+ console.log(
1612
+ 'Early return from toggleSelection - already selected in single select mode',
1613
+ );
1614
+ return;
1615
+ }
1616
+
1617
+ if (this._config.debug)
1618
+ console.log(
1619
+ `Toggling selection for option: ${value}, currently selected: ${isSelected}`,
1620
+ );
1621
+
1622
+ // Ensure any search highlights are cleared when selection changes
1623
+ if (this._searchModule) {
1624
+ this._searchModule.clearSearchHighlights();
1625
+ }
1626
+
1627
+ // Toggle the selection in the state
1628
+ this._state.toggleSelectedOptions(value);
1629
+
1630
+ // Update the original select element's option selected state
1631
+ const optionEl = Array.from(this._element.querySelectorAll('option')).find(
1632
+ (opt) => opt.value === value,
1633
+ ) as HTMLOptionElement;
1634
+
1635
+ if (optionEl) {
1636
+ // For multiple select, toggle the 'selected' attribute
1637
+ if (this._config.multiple) {
1638
+ optionEl.selected = !isSelected;
1639
+ } else {
1640
+ // For single select, deselect all other options and select this one
1641
+ Array.from(this._element.querySelectorAll('option')).forEach((opt) => {
1642
+ (opt as HTMLOptionElement).selected = opt.value === value;
1643
+ });
1644
+ }
1645
+ }
1646
+
1647
+ // Update the display element value
1648
+ this.updateSelectedOptionDisplay();
1649
+
1650
+ // Update option classes without re-rendering the dropdown content
1651
+ this._updateSelectedOptionClass();
1652
+
1653
+ // For single select mode, always close the dropdown after selection
1654
+ // For multiple select mode, only close if closeOnSelect is true
1655
+ if (!this._config.multiple) {
1656
+ if (this._config.debug)
1657
+ console.log(
1658
+ 'About to call closeDropdown() for single select mode - always close after selection',
1659
+ );
1660
+ this.closeDropdown();
1661
+ } else if (this._config.closeOnSelect) {
1662
+ if (this._config.debug)
1663
+ console.log(
1664
+ 'About to call closeDropdown() for multiple select with closeOnSelect:true',
1665
+ );
1666
+ this.closeDropdown();
1667
+ }
1668
+
1669
+ // Dispatch custom change event with additional data
1670
+ this._dispatchEvent('change', {
1671
+ value: value,
1672
+ selected: !isSelected,
1673
+ selectedOptions: this.getSelectedOptions(),
1674
+ });
1675
+ this._fireEvent('change', {
1676
+ value: value,
1677
+ selected: !isSelected,
1678
+ selectedOptions: this.getSelectedOptions(),
1679
+ });
1680
+ }
1681
+
1682
+ /**
1683
+ * Clean up all resources when the component is destroyed
1684
+ * This overrides the parent dispose method
1685
+ */
1686
+ public override dispose(): void {
1687
+ // Clean up event listeners
1688
+ this._eventManager.removeAllListeners(null);
1689
+
1690
+ // Dispose modules
1691
+ if (this._dropdownModule) {
1692
+ this._dropdownModule.dispose();
1693
+ }
1694
+
1695
+ if (this._comboboxModule) {
1696
+ if (typeof this._comboboxModule.destroy === 'function') {
1697
+ this._comboboxModule.destroy();
1698
+ }
1699
+ }
1700
+
1701
+ if (this._tagsModule) {
1702
+ if (typeof this._tagsModule.destroy === 'function') {
1703
+ this._tagsModule.destroy();
1704
+ }
1705
+ }
1706
+
1707
+ if (this._searchModule) {
1708
+ if (typeof this._searchModule.destroy === 'function') {
1709
+ this._searchModule.destroy();
1710
+ }
1711
+ }
1712
+
1713
+ // Remove DOM elements
1714
+ if (this._wrapperElement && this._wrapperElement.parentNode) {
1715
+ this._wrapperElement.parentNode.removeChild(this._wrapperElement);
1716
+ }
1717
+
1718
+ // Call parent dispose to clean up data
1719
+ super.dispose();
1720
+ }
1721
+
1722
+ /**
1723
+ * ========================================================================
1724
+ * STATIC METHODS
1725
+ * ========================================================================
1726
+ */
1727
+
1728
+ private static readonly _instances = new Map<HTMLElement, KTSelect>();
1729
+
1730
+ /**
1731
+ * Create instances of KTSelect for all matching elements
1732
+ */
1733
+ public static createInstances(): void {
1734
+ const elements = document.querySelectorAll<HTMLElement>('[data-kt-select]');
1735
+
1736
+ elements.forEach((element) => {
1737
+ if (
1738
+ element.hasAttribute('data-kt-select') &&
1739
+ !element.classList.contains('data-kt-select-initialized')
1740
+ ) {
1741
+ const instance = new KTSelect(element);
1742
+ this._instances.set(element, instance);
1743
+ }
1744
+ });
1745
+ }
1746
+
1747
+ /**
1748
+ * Initialize all KTSelect instances
1749
+ */
1750
+ public static init(): void {
1751
+ KTSelect.createInstances();
1752
+ }
1753
+
1754
+ /**
1755
+ * Handle remote search
1756
+ * @param event Input event
1757
+ */
1758
+ private _handleRemoteSearch(event: Event) {
1759
+ if (
1760
+ !this._remoteModule ||
1761
+ !this._config.remote ||
1762
+ !this._config.searchParam
1763
+ )
1764
+ return;
1765
+
1766
+ const query = (event.target as HTMLInputElement).value;
1767
+
1768
+ // Check if the query is long enough
1769
+ if (query.length < (this._config.searchMinLength || 0)) {
1770
+ return;
1771
+ }
1772
+
1773
+ // Debounce the search
1774
+ if (this._searchDebounceTimeout) {
1775
+ clearTimeout(this._searchDebounceTimeout);
1776
+ }
1777
+
1778
+ this._searchDebounceTimeout = window.setTimeout(() => {
1779
+ // Show loading state
1780
+ this._renderSearchLoadingState();
1781
+
1782
+ // Fetch remote data with search query
1783
+ this._remoteModule
1784
+ .fetchData(query)
1785
+ .then((items) => {
1786
+ // Update state with fetched items
1787
+ this._state
1788
+ .setItems(items)
1789
+ .then(() => {
1790
+ // Update options in the dropdown
1791
+ this._updateSearchResults(items);
1792
+
1793
+ // Refresh the search module's option cache if search is enabled
1794
+ if (this._searchModule && this._config.enableSearch) {
1795
+ this._searchModule.refreshOptionCache();
1796
+ }
1797
+ })
1798
+ .catch((error) => {
1799
+ console.error('Error updating search results:', error);
1800
+ this._renderSearchErrorState(
1801
+ error.message || 'Failed to load search results',
1802
+ );
1803
+ });
1804
+ })
1805
+ .catch((error) => {
1806
+ console.error('Error fetching search results:', error);
1807
+ this._renderSearchErrorState(
1808
+ this._remoteModule.getErrorMessage() ||
1809
+ 'Failed to load search results',
1810
+ );
1811
+ });
1812
+ }, this._config.searchDebounce || 300);
1813
+ }
1814
+
1815
+ // Search debounce timeout
1816
+ private _searchDebounceTimeout: number | null = null;
1817
+
1818
+ /**
1819
+ * Render loading state for search
1820
+ */
1821
+ private _renderSearchLoadingState() {
1822
+ if (!this._originalOptionsHtml && this._dropdownContentElement) {
1823
+ const optionsContainer = this._dropdownContentElement.querySelector(
1824
+ '[data-kt-select-options-container]',
1825
+ );
1826
+ if (optionsContainer) {
1827
+ this._originalOptionsHtml = optionsContainer.innerHTML;
1828
+ }
1829
+ }
1830
+ this._showDropdownMessage('loading', 'Searching...');
1831
+ }
1832
+
1833
+ // Store original options HTML for restoring after search
1834
+ private _originalOptionsHtml: string | null = null;
1835
+
1836
+ /**
1837
+ * Render error state for search
1838
+ * @param message Error message
1839
+ */
1840
+ private _renderSearchErrorState(message: string) {
1841
+ this._showDropdownMessage('error', message);
1842
+ }
1843
+
1844
+ /**
1845
+ * Update search results in the dropdown
1846
+ * @param items Search result items
1847
+ */
1848
+ private _updateSearchResults(items: KTSelectOptionData[]) {
1849
+ if (!this._dropdownContentElement) return;
1850
+
1851
+ const optionsContainer = this._dropdownContentElement.querySelector(
1852
+ '[data-kt-select-options-container]',
1853
+ );
1854
+ if (!optionsContainer) return;
1855
+
1856
+ // Clear current options
1857
+ optionsContainer.innerHTML = '';
1858
+
1859
+ if (items.length === 0) {
1860
+ // Show no results message using template for consistency and customization
1861
+ const noResultsElement = defaultTemplates.noResults(this._config);
1862
+ optionsContainer.appendChild(noResultsElement);
1863
+ return;
1864
+ }
1865
+
1866
+ // Process each item individually to create options
1867
+ items.forEach((item) => {
1868
+ // Create option for the original select
1869
+ const selectOption = defaultTemplates.emptyOption({
1870
+ ...this._config,
1871
+ placeholder: item.title,
1872
+ });
1873
+ selectOption.value = item.id;
1874
+ if (item.description) {
1875
+ selectOption.setAttribute(
1876
+ 'data-kt-select-option-description',
1877
+ item.description,
1878
+ );
1879
+ }
1880
+ if (item.icon) {
1881
+ selectOption.setAttribute('data-kt-select-option-icon', item.icon);
1882
+ }
1883
+
1884
+ // Create option element for the dropdown
1885
+ const ktOption = new KTSelectOption(selectOption, this._config);
1886
+ const renderedOption = ktOption.render();
1887
+
1888
+ // Add to dropdown container
1889
+ optionsContainer.appendChild(renderedOption);
1890
+ });
1891
+
1892
+ // Add pagination "Load More" button if needed
1893
+ if (this._config.pagination && this._remoteModule.hasMorePages()) {
1894
+ this._addLoadMoreButton();
1895
+ }
1896
+
1897
+ // Update options NodeList
1898
+ this._options = this._wrapperElement.querySelectorAll(
1899
+ `[data-kt-select-option]`,
1900
+ ) as NodeListOf<HTMLElement>;
1901
+ }
1902
+
1903
+ /**
1904
+ * Filter options by query
1905
+ */
1906
+ public filterOptions(query: string): void {
1907
+ this._filterOptionsForCombobox(query);
1908
+ }
1909
+
1910
+ /**
1911
+ * Check if dropdown is open
1912
+ */
1913
+ public isDropdownOpen(): boolean {
1914
+ return this._dropdownIsOpen;
1915
+ }
1916
+ }