@keenthemes/ktui 1.0.29 → 1.1.0

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 (243) hide show
  1. package/README.md +27 -0
  2. package/dist/ktui.js +8780 -6199
  3. package/dist/ktui.min.js +1 -1
  4. package/dist/ktui.min.js.map +1 -1
  5. package/dist/styles.css +2744 -1367
  6. package/lib/cjs/components/alert/alert.js +1025 -0
  7. package/lib/cjs/components/alert/alert.js.map +1 -0
  8. package/lib/cjs/components/alert/index.js +20 -0
  9. package/lib/cjs/components/alert/index.js.map +1 -0
  10. package/lib/cjs/components/alert/templates.js +120 -0
  11. package/lib/cjs/components/alert/templates.js.map +1 -0
  12. package/lib/cjs/components/alert/types.js +7 -0
  13. package/lib/cjs/components/alert/types.js.map +1 -0
  14. package/lib/cjs/components/datepicker/config/config.js +42 -0
  15. package/lib/cjs/components/datepicker/config/config.js.map +1 -0
  16. package/lib/cjs/components/datepicker/config/index.js +24 -0
  17. package/lib/cjs/components/datepicker/config/index.js.map +1 -0
  18. package/lib/cjs/components/datepicker/config/interfaces.js +7 -0
  19. package/lib/cjs/components/datepicker/config/interfaces.js.map +1 -0
  20. package/lib/cjs/components/datepicker/config/types.js +7 -0
  21. package/lib/cjs/components/datepicker/config/types.js.map +1 -0
  22. package/lib/cjs/components/datepicker/core/event-manager.js +135 -0
  23. package/lib/cjs/components/datepicker/core/event-manager.js.map +1 -0
  24. package/lib/cjs/components/datepicker/core/focus-manager.js +167 -0
  25. package/lib/cjs/components/datepicker/core/focus-manager.js.map +1 -0
  26. package/lib/cjs/components/datepicker/core/helpers.js +219 -0
  27. package/lib/cjs/components/datepicker/core/helpers.js.map +1 -0
  28. package/lib/cjs/components/datepicker/core/index.js +25 -0
  29. package/lib/cjs/components/datepicker/core/index.js.map +1 -0
  30. package/lib/cjs/components/datepicker/core/unified-state-manager.js +394 -0
  31. package/lib/cjs/components/datepicker/core/unified-state-manager.js.map +1 -0
  32. package/lib/cjs/components/datepicker/datepicker.js +2066 -763
  33. package/lib/cjs/components/datepicker/datepicker.js.map +1 -1
  34. package/lib/cjs/components/datepicker/index.js +19 -8
  35. package/lib/cjs/components/datepicker/index.js.map +1 -1
  36. package/lib/cjs/components/datepicker/ui/index.js +23 -0
  37. package/lib/cjs/components/datepicker/ui/index.js.map +1 -0
  38. package/lib/cjs/components/datepicker/ui/input/dropdown.js +489 -0
  39. package/lib/cjs/components/datepicker/ui/input/dropdown.js.map +1 -0
  40. package/lib/cjs/components/datepicker/ui/input/index.js +23 -0
  41. package/lib/cjs/components/datepicker/ui/input/index.js.map +1 -0
  42. package/lib/cjs/components/datepicker/ui/input/segmented-input.js +640 -0
  43. package/lib/cjs/components/datepicker/ui/input/segmented-input.js.map +1 -0
  44. package/lib/cjs/components/datepicker/ui/renderers/calendar.js +446 -0
  45. package/lib/cjs/components/datepicker/ui/renderers/calendar.js.map +1 -0
  46. package/lib/cjs/components/datepicker/ui/renderers/footer.js +42 -0
  47. package/lib/cjs/components/datepicker/ui/renderers/footer.js.map +1 -0
  48. package/lib/cjs/components/datepicker/ui/renderers/header.js +32 -0
  49. package/lib/cjs/components/datepicker/ui/renderers/header.js.map +1 -0
  50. package/lib/cjs/components/datepicker/ui/renderers/index.js +25 -0
  51. package/lib/cjs/components/datepicker/ui/renderers/index.js.map +1 -0
  52. package/lib/cjs/components/datepicker/ui/renderers/time-picker.js +384 -0
  53. package/lib/cjs/components/datepicker/ui/renderers/time-picker.js.map +1 -0
  54. package/lib/cjs/components/datepicker/ui/templates/index.js +22 -0
  55. package/lib/cjs/components/datepicker/ui/templates/index.js.map +1 -0
  56. package/lib/cjs/components/datepicker/ui/templates/templates.js +253 -0
  57. package/lib/cjs/components/datepicker/ui/templates/templates.js.map +1 -0
  58. package/lib/cjs/components/datepicker/utils/date-formatters.js +88 -0
  59. package/lib/cjs/components/datepicker/utils/date-formatters.js.map +1 -0
  60. package/lib/cjs/components/datepicker/utils/date-utils.js +194 -0
  61. package/lib/cjs/components/datepicker/utils/date-utils.js.map +1 -0
  62. package/lib/cjs/components/datepicker/utils/index.js +24 -0
  63. package/lib/cjs/components/datepicker/utils/index.js.map +1 -0
  64. package/lib/cjs/components/datepicker/utils/time-utils.js +213 -0
  65. package/lib/cjs/components/datepicker/utils/time-utils.js.map +1 -0
  66. package/lib/cjs/index.js +6 -1
  67. package/lib/cjs/index.js.map +1 -1
  68. package/lib/esm/components/alert/alert.js +1022 -0
  69. package/lib/esm/components/alert/alert.js.map +1 -0
  70. package/lib/esm/components/alert/index.js +4 -0
  71. package/lib/esm/components/alert/index.js.map +1 -0
  72. package/lib/esm/components/alert/templates.js +112 -0
  73. package/lib/esm/components/alert/templates.js.map +1 -0
  74. package/lib/esm/components/alert/types.js +6 -0
  75. package/lib/esm/components/alert/types.js.map +1 -0
  76. package/lib/esm/components/datepicker/config/config.js +39 -0
  77. package/lib/esm/components/datepicker/config/config.js.map +1 -0
  78. package/lib/esm/components/datepicker/config/index.js +8 -0
  79. package/lib/esm/components/datepicker/config/index.js.map +1 -0
  80. package/lib/esm/components/datepicker/config/interfaces.js +6 -0
  81. package/lib/esm/components/datepicker/config/interfaces.js.map +1 -0
  82. package/lib/esm/components/datepicker/config/types.js +6 -0
  83. package/lib/esm/components/datepicker/config/types.js.map +1 -0
  84. package/lib/esm/components/datepicker/core/event-manager.js +133 -0
  85. package/lib/esm/components/datepicker/core/event-manager.js.map +1 -0
  86. package/lib/esm/components/datepicker/core/focus-manager.js +164 -0
  87. package/lib/esm/components/datepicker/core/focus-manager.js.map +1 -0
  88. package/lib/esm/components/datepicker/core/helpers.js +211 -0
  89. package/lib/esm/components/datepicker/core/helpers.js.map +1 -0
  90. package/lib/esm/components/datepicker/core/index.js +9 -0
  91. package/lib/esm/components/datepicker/core/index.js.map +1 -0
  92. package/lib/esm/components/datepicker/core/unified-state-manager.js +391 -0
  93. package/lib/esm/components/datepicker/core/unified-state-manager.js.map +1 -0
  94. package/lib/esm/components/datepicker/datepicker.js +2065 -763
  95. package/lib/esm/components/datepicker/datepicker.js.map +1 -1
  96. package/lib/esm/components/datepicker/index.js +6 -8
  97. package/lib/esm/components/datepicker/index.js.map +1 -1
  98. package/lib/esm/components/datepicker/ui/index.js +7 -0
  99. package/lib/esm/components/datepicker/ui/index.js.map +1 -0
  100. package/lib/esm/components/datepicker/ui/input/dropdown.js +486 -0
  101. package/lib/esm/components/datepicker/ui/input/dropdown.js.map +1 -0
  102. package/lib/esm/components/datepicker/ui/input/index.js +7 -0
  103. package/lib/esm/components/datepicker/ui/input/index.js.map +1 -0
  104. package/lib/esm/components/datepicker/ui/input/segmented-input.js +637 -0
  105. package/lib/esm/components/datepicker/ui/input/segmented-input.js.map +1 -0
  106. package/lib/esm/components/datepicker/ui/renderers/calendar.js +443 -0
  107. package/lib/esm/components/datepicker/ui/renderers/calendar.js.map +1 -0
  108. package/lib/esm/components/datepicker/ui/renderers/footer.js +39 -0
  109. package/lib/esm/components/datepicker/ui/renderers/footer.js.map +1 -0
  110. package/lib/esm/components/datepicker/ui/renderers/header.js +29 -0
  111. package/lib/esm/components/datepicker/ui/renderers/header.js.map +1 -0
  112. package/lib/esm/components/datepicker/ui/renderers/index.js +9 -0
  113. package/lib/esm/components/datepicker/ui/renderers/index.js.map +1 -0
  114. package/lib/esm/components/datepicker/ui/renderers/time-picker.js +381 -0
  115. package/lib/esm/components/datepicker/ui/renderers/time-picker.js.map +1 -0
  116. package/lib/esm/components/datepicker/ui/templates/index.js +6 -0
  117. package/lib/esm/components/datepicker/ui/templates/index.js.map +1 -0
  118. package/lib/esm/components/datepicker/ui/templates/templates.js +242 -0
  119. package/lib/esm/components/datepicker/ui/templates/templates.js.map +1 -0
  120. package/lib/esm/components/datepicker/utils/date-formatters.js +83 -0
  121. package/lib/esm/components/datepicker/utils/date-formatters.js.map +1 -0
  122. package/lib/esm/components/datepicker/utils/date-utils.js +184 -0
  123. package/lib/esm/components/datepicker/utils/date-utils.js.map +1 -0
  124. package/lib/esm/components/datepicker/utils/index.js +8 -0
  125. package/lib/esm/components/datepicker/utils/index.js.map +1 -0
  126. package/lib/esm/components/datepicker/utils/time-utils.js +201 -0
  127. package/lib/esm/components/datepicker/utils/time-utils.js.map +1 -0
  128. package/lib/esm/index.js +4 -0
  129. package/lib/esm/index.js.map +1 -1
  130. package/package.json +12 -3
  131. package/src/components/alert/alert.css +429 -188
  132. package/src/components/alert/alert.ts +990 -0
  133. package/src/components/alert/index.ts +4 -0
  134. package/src/components/alert/templates.ts +110 -0
  135. package/src/components/alert/tests/accessibility/aria-roles.test.ts +19 -0
  136. package/src/components/alert/tests/accessibility/focus-management.test.ts +19 -0
  137. package/src/components/alert/tests/accessibility/keyboard-nav.test.ts +22 -0
  138. package/src/components/alert/tests/actions/confirm-cancel.test.ts +122 -0
  139. package/src/components/alert/tests/actions/input-field.test.ts +180 -0
  140. package/src/components/alert/tests/alert.basic.test.ts +126 -0
  141. package/src/components/alert/tests/alert.config.test.ts +75 -0
  142. package/src/components/alert/tests/alert.templates.test.ts +17 -0
  143. package/src/components/alert/tests/config/attribute-config.test.ts +94 -0
  144. package/src/components/alert/tests/config/json-config.test.ts +119 -0
  145. package/src/components/alert/tests/config/merging.test.ts +89 -0
  146. package/src/components/alert/tests/dismissal/auto-dismiss.test.ts +96 -0
  147. package/src/components/alert/tests/dismissal/escape-key-dismiss.test.ts +105 -0
  148. package/src/components/alert/tests/dismissal/manual-dismiss.test.ts +90 -0
  149. package/src/components/alert/tests/dismissal/outside-click-dismiss.test.ts +91 -0
  150. package/src/components/alert/tests/edge-cases/invalid-config.test.ts +19 -0
  151. package/src/components/alert/tests/edge-cases/multiple-alerts.test.ts +19 -0
  152. package/src/components/alert/tests/rendering/custom-content.test.ts +81 -0
  153. package/src/components/alert/tests/rendering/info-alert.test.ts +84 -0
  154. package/src/components/alert/tests/rendering/success-alert.test.ts +100 -0
  155. package/src/components/alert/tests/templates/default-templates.test.ts +16 -0
  156. package/src/components/alert/tests/templates/user-templates.test.ts +16 -0
  157. package/src/components/alert/types.ts +145 -0
  158. package/src/components/datepicker/__tests__/datepicker-events.test.ts +356 -0
  159. package/src/components/datepicker/__tests__/datepicker-init.test.ts +343 -0
  160. package/src/components/datepicker/__tests__/datepicker-integration.test.ts +435 -0
  161. package/src/components/datepicker/__tests__/datepicker-timezone.test.ts +220 -0
  162. package/src/components/datepicker/__tests__/segmented-input-focus.test.ts +380 -0
  163. package/src/components/datepicker/__tests__/selective-state-updates.test.ts +400 -0
  164. package/src/components/datepicker/__tests__/state-manager.test.ts +421 -0
  165. package/src/components/datepicker/__tests__/time-preservation.test.ts +387 -0
  166. package/src/components/datepicker/config/config.ts +40 -0
  167. package/src/components/datepicker/config/index.ts +8 -0
  168. package/src/components/datepicker/config/interfaces.ts +82 -0
  169. package/src/components/datepicker/config/types.ts +188 -0
  170. package/src/components/datepicker/core/event-manager.ts +159 -0
  171. package/src/components/datepicker/core/focus-manager.ts +201 -0
  172. package/src/components/datepicker/core/helpers.ts +231 -0
  173. package/src/components/datepicker/core/index.ts +9 -0
  174. package/src/components/datepicker/core/unified-state-manager.ts +459 -0
  175. package/src/components/datepicker/datepicker.css +429 -1
  176. package/src/components/datepicker/datepicker.ts +2538 -1277
  177. package/src/components/datepicker/index.ts +6 -8
  178. package/src/components/datepicker/ui/index.ts +7 -0
  179. package/src/components/datepicker/ui/input/dropdown.ts +552 -0
  180. package/src/components/datepicker/ui/input/index.ts +7 -0
  181. package/src/components/datepicker/ui/input/segmented-input.ts +638 -0
  182. package/src/components/datepicker/ui/renderers/__tests__/calendar-optimizations.test.ts +611 -0
  183. package/src/components/datepicker/ui/renderers/calendar.ts +530 -0
  184. package/src/components/datepicker/ui/renderers/footer.ts +43 -0
  185. package/src/components/datepicker/ui/renderers/header.ts +33 -0
  186. package/src/components/datepicker/ui/renderers/index.ts +9 -0
  187. package/src/components/datepicker/ui/renderers/time-picker.ts +438 -0
  188. package/src/components/datepicker/ui/templates/index.ts +6 -0
  189. package/src/components/datepicker/ui/templates/templates.ts +306 -0
  190. package/src/components/datepicker/utils/__tests__/date-formatters.test.ts +160 -0
  191. package/src/components/datepicker/utils/__tests__/date-utils-keys.test.ts +86 -0
  192. package/src/components/datepicker/utils/__tests__/date-utils-timezone.test.ts +215 -0
  193. package/src/components/datepicker/utils/date-formatters.ts +85 -0
  194. package/src/components/datepicker/utils/date-utils.ts +172 -0
  195. package/src/components/datepicker/utils/index.ts +8 -0
  196. package/src/components/datepicker/utils/time-utils.ts +221 -0
  197. package/src/index.ts +7 -1
  198. package/lib/cjs/components/datepicker/calendar.js +0 -1061
  199. package/lib/cjs/components/datepicker/calendar.js.map +0 -1
  200. package/lib/cjs/components/datepicker/config.js +0 -332
  201. package/lib/cjs/components/datepicker/config.js.map +0 -1
  202. package/lib/cjs/components/datepicker/dropdown.js +0 -635
  203. package/lib/cjs/components/datepicker/dropdown.js.map +0 -1
  204. package/lib/cjs/components/datepicker/events.js +0 -129
  205. package/lib/cjs/components/datepicker/events.js.map +0 -1
  206. package/lib/cjs/components/datepicker/keyboard.js +0 -536
  207. package/lib/cjs/components/datepicker/keyboard.js.map +0 -1
  208. package/lib/cjs/components/datepicker/locales.js +0 -78
  209. package/lib/cjs/components/datepicker/locales.js.map +0 -1
  210. package/lib/cjs/components/datepicker/templates.js +0 -403
  211. package/lib/cjs/components/datepicker/templates.js.map +0 -1
  212. package/lib/cjs/components/datepicker/types.js +0 -23
  213. package/lib/cjs/components/datepicker/types.js.map +0 -1
  214. package/lib/cjs/components/datepicker/utils.js +0 -524
  215. package/lib/cjs/components/datepicker/utils.js.map +0 -1
  216. package/lib/esm/components/datepicker/calendar.js +0 -1058
  217. package/lib/esm/components/datepicker/calendar.js.map +0 -1
  218. package/lib/esm/components/datepicker/config.js +0 -329
  219. package/lib/esm/components/datepicker/config.js.map +0 -1
  220. package/lib/esm/components/datepicker/dropdown.js +0 -632
  221. package/lib/esm/components/datepicker/dropdown.js.map +0 -1
  222. package/lib/esm/components/datepicker/events.js +0 -126
  223. package/lib/esm/components/datepicker/events.js.map +0 -1
  224. package/lib/esm/components/datepicker/keyboard.js +0 -533
  225. package/lib/esm/components/datepicker/keyboard.js.map +0 -1
  226. package/lib/esm/components/datepicker/locales.js +0 -74
  227. package/lib/esm/components/datepicker/locales.js.map +0 -1
  228. package/lib/esm/components/datepicker/templates.js +0 -390
  229. package/lib/esm/components/datepicker/templates.js.map +0 -1
  230. package/lib/esm/components/datepicker/types.js +0 -20
  231. package/lib/esm/components/datepicker/types.js.map +0 -1
  232. package/lib/esm/components/datepicker/utils.js +0 -508
  233. package/lib/esm/components/datepicker/utils.js.map +0 -1
  234. package/src/components/datepicker/calendar.ts +0 -1397
  235. package/src/components/datepicker/config.ts +0 -368
  236. package/src/components/datepicker/dropdown.ts +0 -757
  237. package/src/components/datepicker/events.ts +0 -149
  238. package/src/components/datepicker/keyboard.ts +0 -646
  239. package/src/components/datepicker/locales.ts +0 -80
  240. package/src/components/datepicker/templates.ts +0 -792
  241. package/src/components/datepicker/types.ts +0 -154
  242. package/src/components/datepicker/utils.ts +0 -631
  243. package/src/components/select/variants.css +0 -4
@@ -1,1287 +1,2548 @@
1
- /**
2
- * KTUI - Free & Open-Source Tailwind UI Components by Keenthemes
3
- * Copyright 2025 by Keenthemes Inc
1
+ /*
2
+ * datepicker.ts - Main implementation for KTDatepicker component
3
+ * Provides single, range, and multi-date selection with segmented input UI.
4
+ * Modular rendering and state helpers are imported from datepicker-helpers.ts.
4
5
  */
5
6
 
6
7
  import KTComponent from '../component';
7
- import { KTDatepickerCalendar } from './calendar';
8
- import { KTDatepickerStateManager } from './config';
9
- import { KTDatepickerKeyboard } from './keyboard';
10
- import { DateRangeInterface, KTDatepickerConfigInterface } from './types';
11
- import { formatDate, parseDate, isValidDate, isDateDisabled } from './utils';
8
+ import { KTDatepickerConfig, KTDatepickerState } from './config/types';
12
9
  import {
13
- datepickerContainerTemplate,
14
- inputWrapperTemplate,
15
- segmentedDateInputTemplate,
16
- segmentedDateRangeInputTemplate,
17
- placeholderTemplate,
18
- } from './templates';
19
- import { KTDatepickerEventManager, KTDatepickerEventName } from './events';
20
-
21
- // Helper function to replace stringToElement
22
- function createElement(html: string): HTMLElement {
23
- const template = document.createElement('template');
24
- template.innerHTML = html.trim();
25
- return template.content.firstChild as HTMLElement;
26
- }
10
+ getTemplateStrings,
11
+ defaultTemplates,
12
+ createTemplateRenderer,
13
+ TemplateRenderer,
14
+ renderTemplateString,
15
+ mergeClassData} from './ui/templates/templates';
16
+ import { defaultDatepickerConfig } from './config/config';
17
+ import { renderHeader } from './ui/renderers/header';
18
+ import { renderCalendar } from './ui/renderers/calendar';
19
+ import { renderFooter } from './ui/renderers/footer';
20
+ import { renderTimePicker } from './ui/renderers/time-picker';
21
+ import { EventManager } from './core/event-manager';
22
+ import { FocusManager } from './core/focus-manager';
23
+ import { KTDatepickerDropdown } from './ui/input/dropdown';
24
+
25
+ import { KTDatepickerUnifiedStateManager, StateObserver } from './core/unified-state-manager';
26
+ import { DropdownState } from './config/types';
27
+ import { formatDateToLocalString } from './utils/date-utils';
28
+ import { formatDate, isSameDay, normalizeDateToMidnight } from './utils/date-formatters';
29
+ import { dateToTimeState, applyTimeToDate, validateTime } from './utils/time-utils';
30
+ import { TimeState } from './config/types';
31
+ import {
32
+ renderSingleSegmentedInputUI,
33
+ renderRangeSegmentedInputUI,
34
+ instantiateSingleSegmentedInput,
35
+ instantiateRangeSegmentedInputs,
36
+ updateRangeSelection
37
+ } from './core/helpers';
27
38
 
28
39
  /**
29
- * KTDatepicker - Main datepicker component class
30
- * Manages the datepicker functionality and integration with input elements
40
+ * KTDatepicker
41
+ *
42
+ * Datepicker component for selecting single, range, or multiple dates.
43
+ *
44
+ * Features:
45
+ * - Opens on input focus or calendar button click (configurable)
46
+ * - Supports single, range, and multi-date modes
47
+ * - Customizable via templates and data attributes
48
+ * - Keyboard navigation and accessibility support
31
49
  */
32
- export class KTDatepicker extends KTComponent {
33
- protected override readonly _name: string = 'datepicker';
34
- protected override readonly _config: KTDatepickerConfigInterface;
35
-
36
- private _state: KTDatepickerStateManager;
37
- private _calendar: KTDatepickerCalendar;
38
- private _keyboard: KTDatepickerKeyboard;
39
- private _eventManager: KTDatepickerEventManager;
40
-
41
- private _dateInputElement: HTMLInputElement | null = null;
42
- private _startDateInputElement: HTMLInputElement | null = null;
43
- private _endDateInputElement: HTMLInputElement | null = null;
44
- private _displayElement: HTMLElement | null = null;
45
- private _useSegmentedDisplay = false;
46
- private _displayWrapper: HTMLElement | null = null;
47
- private _displayText: HTMLElement | null = null;
48
-
49
- private _currentDate: Date | null = null;
50
- private _currentRange: DateRangeInterface | null = null;
51
- private _segmentFocused:
52
- | 'day'
53
- | 'month'
54
- | 'year'
55
- | 'start-day'
56
- | 'start-month'
57
- | 'start-year'
58
- | 'end-day'
59
- | 'end-month'
60
- | 'end-year'
61
- | null = null;
62
-
63
- /**
64
- * Constructor for the KTDatepicker class.
65
- */
66
- constructor(element: HTMLElement, config?: KTDatepickerConfigInterface) {
67
- super();
68
-
69
- // Check if the element already has a datepicker instance attached to it
70
- if (element.getAttribute('data-kt-datepicker-initialized') === 'true') {
71
- return;
72
- }
73
-
74
- // Initialize the datepicker with the provided element
75
- this._init(element);
76
-
77
- // Build the configuration object by merging the default config with the provided config
78
- this._buildConfig(config);
79
-
80
- // Store the instance of the datepicker directly on the element
81
- (element as any).instance = this;
82
-
83
- // Ensure the element is focusable
84
- this._element.setAttribute('tabindex', '0');
85
- this._element.classList.add(
86
- 'kt-datepicker',
87
- 'relative',
88
- 'focus:outline-none',
89
- );
90
-
91
- // Mark as initialized
92
- this._element.setAttribute('data-kt-datepicker-initialized', 'true');
93
-
94
- // Find input elements
95
- this._initializeInputElements();
96
-
97
- // Create display element if needed
98
- this._createDisplayElement();
99
-
100
- // Create state manager first
101
- this._state = new KTDatepickerStateManager(this._element, this._config);
102
- this._config = this._state.getConfig();
103
-
104
- // Initialize the calendar and keyboard after creating the state manager
105
- this._calendar = new KTDatepickerCalendar(this._element, this._state);
106
- this._keyboard = new KTDatepickerKeyboard(this._element, this._state);
107
-
108
- // Initialize event manager
109
- this._eventManager = this._state.getEventManager();
110
-
111
- // Set up event listeners
112
- this._setupEventListeners();
113
-
114
- // Initialize with any default values
115
- this._initializeDefaultValues();
116
- }
117
-
118
- /**
119
- * Initialize input elements
120
- */
121
- private _initializeInputElements(): void {
122
- // Get main input element - will be hidden
123
- this._dateInputElement = this._element.querySelector(
124
- '[data-kt-datepicker-input]',
125
- );
126
-
127
- // Hide the input element and make it only for data storage
128
- if (this._dateInputElement) {
129
- this._dateInputElement.classList.add('hidden', 'sr-only');
130
- this._dateInputElement.setAttribute('aria-hidden', 'true');
131
- this._dateInputElement.tabIndex = -1;
132
- }
133
-
134
- // Get range input elements if applicable
135
- this._startDateInputElement = this._element.querySelector(
136
- '[data-kt-datepicker-start]',
137
- );
138
- this._endDateInputElement = this._element.querySelector(
139
- '[data-kt-datepicker-end]',
140
- );
141
-
142
- // Get display element if exists
143
- this._displayElement = this._element.querySelector(
144
- '[data-kt-datepicker-display]',
145
- );
146
-
147
- // Check if we should use segmented display
148
- this._useSegmentedDisplay =
149
- this._element.hasAttribute('data-kt-datepicker-segmented') ||
150
- this._element.hasAttribute('data-kt-datepicker-segmented-input');
151
- }
152
-
153
- /**
154
- * Create display element for datepicker
155
- */
156
- private _createDisplayElement(): void {
157
- // Skip if already created
158
- if (this._displayElement) {
159
- return;
160
- }
161
-
162
- // Get format from config or use default
163
- const format = this._config.format || 'mm/dd/yyyy';
164
- const placeholder =
165
- this._dateInputElement?.getAttribute('placeholder') || format;
166
-
167
- // Create wrapper for display element
168
- this._displayWrapper = document.createElement('div');
169
- this._displayWrapper.className =
170
- 'kt-datepicker-display-wrapper kt-datepicker-display-segment';
171
- this._displayWrapper.setAttribute('role', 'combobox');
172
- this._displayWrapper.setAttribute('aria-haspopup', 'dialog');
173
- this._displayWrapper.setAttribute('aria-expanded', 'false');
174
- this._element.appendChild(this._displayWrapper);
175
-
176
- if (this._useSegmentedDisplay) {
177
- // Create segmented display for better date part selection
178
- const displayContainer = document.createElement('div');
179
- displayContainer.className = 'kt-datepicker-display-element';
180
- displayContainer.setAttribute('tabindex', '0');
181
- displayContainer.setAttribute('role', 'textbox');
182
- displayContainer.setAttribute('aria-label', placeholder);
183
- displayContainer.setAttribute('data-kt-datepicker-display', '');
184
-
185
- // Add segmented template based on range mode
186
- if (this._config.range) {
187
- displayContainer.innerHTML = segmentedDateRangeInputTemplate(
188
- this._config.format || 'mm/dd/yyyy',
189
- );
190
- } else {
191
- displayContainer.innerHTML = segmentedDateInputTemplate(
192
- this._config.format || 'mm/dd/yyyy',
193
- );
194
- }
195
-
196
- this._displayElement = displayContainer;
197
- this._displayWrapper.appendChild(this._displayElement);
198
-
199
- // Add click handlers for segments
200
- const segments = this._displayElement.querySelectorAll('[data-segment]');
201
- segments.forEach((segment) => {
202
- segment.addEventListener('click', (e) => {
203
- e.stopPropagation();
204
- const segmentType = segment.getAttribute('data-segment');
205
- this._handleSegmentClick(segmentType);
206
- });
207
- });
208
- } else {
209
- // Create simple display element
210
- this._displayElement = document.createElement('div');
211
- this._displayElement.className = 'kt-datepicker-display-element';
212
- this._displayElement.setAttribute('tabindex', '0');
213
- this._displayElement.setAttribute('role', 'textbox');
214
- this._displayElement.setAttribute('aria-label', placeholder);
215
- this._displayElement.setAttribute('data-placeholder', placeholder);
216
- this._displayElement.setAttribute('data-kt-datepicker-display', '');
217
-
218
- // Create display text element
219
- this._displayText = document.createElement('span');
220
- this._displayText.className = 'kt-datepicker-display-text';
221
- this._displayText.textContent = placeholder;
222
- this._displayText.classList.add('text-gray-400');
223
-
224
- this._displayElement.appendChild(this._displayText);
225
- this._displayWrapper.appendChild(this._displayElement);
226
- }
227
-
228
- // Add click event to display element
229
- this._displayElement.addEventListener('click', (e) => {
230
- e.preventDefault();
231
- if (!this._state.getState().isOpen) {
232
- this._state.setOpen(true);
233
- }
234
- });
235
-
236
- // Enhanced keyboard event handling for display element
237
- this._displayElement.addEventListener('keydown', (e) => {
238
- if (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown') {
239
- e.preventDefault();
240
- e.stopPropagation();
241
-
242
- // If not already open, open the dropdown
243
- if (!this._state.getState().isOpen) {
244
- this._state.setOpen(true);
245
-
246
- // Dispatch a custom event to notify about the keyboard open
247
- this._eventManager.dispatchKeyboardOpenEvent();
248
- }
249
- }
250
- });
251
- }
252
-
253
- /**
254
- * Handle segment click to focus and open appropriate view
255
- *
256
- * @param segmentType - Type of segment clicked
257
- */
258
- private _handleSegmentClick(segmentType: string | null): void {
259
- if (!segmentType) return;
260
-
261
- // Store the focused segment
262
- this._segmentFocused = segmentType as any;
263
-
264
- // Remove highlight from all segments
265
- this._removeSegmentHighlights();
266
-
267
- // Add highlight to clicked segment
268
- if (this._displayElement) {
269
- const segment = this._displayElement.querySelector(
270
- `[data-segment="${segmentType}"]`,
271
- );
272
- if (segment) {
273
- segment.classList.add('kt-datepicker-segment-focused');
274
- }
275
- }
276
-
277
- // Set the appropriate view mode based on segment type
278
- if (segmentType.includes('day')) {
279
- // Day segment - open in days view (default)
280
- this._state.setViewMode('days');
281
- this._state.setOpen(true);
282
- } else if (segmentType.includes('month')) {
283
- // Month segment - open in months view
284
- this._state.setViewMode('months');
285
- this._state.setOpen(true);
286
- } else if (segmentType.includes('year')) {
287
- // Year segment - open in years view
288
- this._state.setViewMode('years');
289
- this._state.setOpen(true);
290
- }
291
- }
292
-
293
- /**
294
- * Set up event listeners
295
- */
296
- private _setupEventListeners(): void {
297
- // Listen for state changes
298
- this._eventManager.addEventListener(
299
- KTDatepickerEventName.STATE_CHANGE,
300
- (e: CustomEvent) => {
301
- const { state } = e.detail;
302
-
303
- // Update ARIA attributes based on open state
304
- if (this._displayWrapper) {
305
- this._displayWrapper.setAttribute(
306
- 'aria-expanded',
307
- state.isOpen.toString(),
308
- );
309
- }
310
-
311
- // Update display when closing
312
- if (!state.isOpen && state.prevIsOpen) {
313
- this._syncDisplayWithSelectedDate();
314
- }
315
- },
316
- );
317
-
318
- // Set up change event listener to update input values
319
- this._eventManager.addEventListener(
320
- KTDatepickerEventName.DATE_CHANGE,
321
- this._handleDateChange.bind(this),
322
- );
323
-
324
- // Add keyboard events to the root element
325
- this._element.addEventListener('keydown', (e) => {
326
- if (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown') {
327
- const state = this._state.getState();
328
- if (!state.isOpen) {
329
- e.preventDefault();
330
- this._state.setOpen(true);
331
- }
332
- }
333
- });
334
-
335
- // Add keyboard navigation for segments
336
- if (this._displayElement && this._useSegmentedDisplay) {
337
- this._displayElement.addEventListener(
338
- 'keydown',
339
- this._handleSegmentKeydown.bind(this),
340
- );
341
- }
342
- }
343
-
344
- /**
345
- * Handle keyboard navigation between segments
346
- *
347
- * @param e - Keyboard event
348
- */
349
- private _handleSegmentKeydown(e: KeyboardEvent): void {
350
- // Only handle if we have a focused segment
351
- if (!this._segmentFocused) return;
352
-
353
- const target = e.target as HTMLElement;
354
- const segmentType = target.getAttribute('data-segment');
355
- if (!segmentType) return;
356
-
357
- // Handle keyboard navigation
358
- switch (e.key) {
359
- case 'ArrowLeft':
360
- e.preventDefault();
361
- this._navigateSegments('prev', segmentType);
362
- break;
363
- case 'ArrowRight':
364
- e.preventDefault();
365
- this._navigateSegments('next', segmentType);
366
- break;
367
- case 'Tab':
368
- // Let default tab behavior work, but update focus segment when tabbing
369
- this._segmentFocused = segmentType as any;
370
- // Remove highlight from all segments
371
- this._removeSegmentHighlights();
372
- // Add highlight to current segment
373
- target.classList.add('segment-focused');
374
- break;
375
- case 'Enter':
376
- case ' ':
377
- e.preventDefault();
378
- this._handleSegmentClick(segmentType);
379
- break;
380
- }
381
- }
382
-
383
- /**
384
- * Navigate between segments with keyboard
385
- *
386
- * @param direction - 'prev' or 'next'
387
- * @param currentSegment - Current segment identifier
388
- */
389
- private _navigateSegments(
390
- direction: 'prev' | 'next',
391
- currentSegment: string,
392
- ): void {
393
- if (!this._displayElement) return;
394
-
395
- // Define segment order
396
- let segments: string[];
397
- if (this._config.range) {
398
- segments = [
399
- 'start-month',
400
- 'start-day',
401
- 'start-year',
402
- 'end-month',
403
- 'end-day',
404
- 'end-year',
405
- ];
406
- } else {
407
- segments = ['month', 'day', 'year'];
408
- }
409
-
410
- // Find current index
411
- const currentIndex = segments.indexOf(currentSegment);
412
- if (currentIndex === -1) return;
413
-
414
- // Calculate new index
415
- let newIndex;
416
- if (direction === 'prev') {
417
- newIndex = currentIndex === 0 ? segments.length - 1 : currentIndex - 1;
418
- } else {
419
- newIndex = currentIndex === segments.length - 1 ? 0 : currentIndex + 1;
420
- }
421
-
422
- // Find new segment element
423
- const newSegment = this._displayElement.querySelector(
424
- `[data-segment="${segments[newIndex]}"]`,
425
- ) as HTMLElement;
426
- if (!newSegment) return;
427
-
428
- // Update focus
429
- newSegment.focus();
430
- this._segmentFocused = segments[newIndex] as any;
431
-
432
- // Remove highlight from all segments
433
- this._removeSegmentHighlights();
434
-
435
- // Add highlight to new segment
436
- newSegment.classList.add('segment-focused');
437
- }
438
-
439
- /**
440
- * Remove highlight from all segments
441
- */
442
- private _removeSegmentHighlights(): void {
443
- if (!this._displayElement) return;
444
-
445
- const segments = this._displayElement.querySelectorAll('.segment-part');
446
- segments.forEach((segment) => {
447
- segment.classList.remove('segment-focused');
448
- });
449
- }
450
-
451
- /**
452
- * Sync display element with the selected date
453
- */
454
- private _syncDisplayWithSelectedDate(): void {
455
- if (!this._displayElement) return;
456
-
457
- const state = this._state.getState();
458
- const selectedDate = state.selectedDate;
459
- const selectedDateRange = state.selectedDateRange;
460
-
461
- if (this._useSegmentedDisplay) {
462
- // Update segmented display elements
463
- if (selectedDate) {
464
- // Single date
465
- const daySegment = this._displayElement.querySelector(
466
- '[data-segment="day"]',
467
- );
468
- const monthSegment = this._displayElement.querySelector(
469
- '[data-segment="month"]',
470
- );
471
- const yearSegment = this._displayElement.querySelector(
472
- '[data-segment="year"]',
473
- );
474
-
475
- if (daySegment) {
476
- daySegment.textContent = selectedDate
477
- .getDate()
478
- .toString()
479
- .padStart(2, '0');
480
- }
481
- if (monthSegment) {
482
- monthSegment.textContent = (selectedDate.getMonth() + 1)
483
- .toString()
484
- .padStart(2, '0');
485
- }
486
- if (yearSegment) {
487
- yearSegment.textContent = selectedDate.getFullYear().toString();
488
- }
489
- } else if (selectedDateRange && selectedDateRange.startDate) {
490
- // Range selection
491
- const startDay = this._displayElement.querySelector(
492
- '[data-segment="start-day"]',
493
- );
494
- const startMonth = this._displayElement.querySelector(
495
- '[data-segment="start-month"]',
496
- );
497
- const startYear = this._displayElement.querySelector(
498
- '[data-segment="start-year"]',
499
- );
500
-
501
- if (startDay) {
502
- startDay.textContent = selectedDateRange.startDate
503
- .getDate()
504
- .toString()
505
- .padStart(2, '0');
506
- }
507
- if (startMonth) {
508
- startMonth.textContent = (selectedDateRange.startDate.getMonth() + 1)
509
- .toString()
510
- .padStart(2, '0');
511
- }
512
- if (startYear) {
513
- startYear.textContent = selectedDateRange.startDate
514
- .getFullYear()
515
- .toString();
516
- }
517
-
518
- if (selectedDateRange.endDate) {
519
- const endDay = this._displayElement.querySelector(
520
- '[data-segment="end-day"]',
521
- );
522
- const endMonth = this._displayElement.querySelector(
523
- '[data-segment="end-month"]',
524
- );
525
- const endYear = this._displayElement.querySelector(
526
- '[data-segment="end-year"]',
527
- );
528
-
529
- if (endDay) {
530
- endDay.textContent = selectedDateRange.endDate
531
- .getDate()
532
- .toString()
533
- .padStart(2, '0');
534
- }
535
- if (endMonth) {
536
- endMonth.textContent = (selectedDateRange.endDate.getMonth() + 1)
537
- .toString()
538
- .padStart(2, '0');
539
- }
540
- if (endYear) {
541
- endYear.textContent = selectedDateRange.endDate
542
- .getFullYear()
543
- .toString();
544
- }
545
- }
546
- }
547
- } else if (this._displayText) {
548
- // Simple display
549
- if (selectedDate) {
550
- // Clear placeholder styling
551
- this._displayText.classList.remove('placeholder');
552
-
553
- // Format date(s) based on config
554
- if (
555
- this._config.range &&
556
- selectedDateRange &&
557
- selectedDateRange.startDate &&
558
- selectedDateRange.endDate
559
- ) {
560
- this._displayText.textContent = `${formatDate(
561
- selectedDateRange.startDate,
562
- this._config.format,
563
- this._config,
564
- )} - ${formatDate(
565
- selectedDateRange.endDate,
566
- this._config.format,
567
- this._config,
568
- )}`;
569
- } else {
570
- this._displayText.textContent = formatDate(
571
- selectedDate,
572
- this._config.format,
573
- this._config,
574
- );
575
- }
576
- } else {
577
- // No date selected, show format as placeholder
578
- const placeholder =
579
- this._displayElement?.getAttribute('data-placeholder') ||
580
- this._config.format;
581
- this._displayText.textContent = placeholder;
582
- this._displayText.classList.add('placeholder');
583
- }
584
- }
585
- }
586
-
587
- /**
588
- * Handle date change events
589
- *
590
- * @param e - Custom event with date change details
591
- */
592
- private _handleDateChange(e: CustomEvent): void {
593
- const detail = e.detail?.payload;
594
- if (!detail) return;
595
-
596
- // Handle single date selection
597
- if (detail.selectedDate) {
598
- const formattedDate = formatDate(
599
- detail.selectedDate,
600
- this._config.format,
601
- this._config,
602
- );
603
-
604
- // Update hidden input value
605
- if (this._dateInputElement) {
606
- this._dateInputElement.value = formattedDate;
607
- // Dispatch change event on input to trigger form validation
608
- this._dateInputElement.dispatchEvent(
609
- new Event('change', { bubbles: true }),
610
- );
611
- }
612
-
613
- // Update display element
614
- this._updateDisplayElement(detail.selectedDate);
615
- }
616
-
617
- // Handle date range selection
618
- if (detail.selectedDateRange && this._config.range) {
619
- const { startDate, endDate } = detail.selectedDateRange;
620
-
621
- // Format the range for the hidden input
622
- if (this._dateInputElement) {
623
- let displayValue = '';
624
-
625
- if (startDate) {
626
- displayValue = formatDate(
627
- startDate,
628
- this._config.format,
629
- this._config,
630
- );
631
-
632
- if (endDate) {
633
- const endFormatted = formatDate(
634
- endDate,
635
- this._config.format,
636
- this._config,
637
- );
638
- displayValue += `${this._config.rangeSeparator}${endFormatted}`;
639
- }
640
- }
641
-
642
- this._dateInputElement.value = displayValue;
643
- // Dispatch change event on input
644
- this._dateInputElement.dispatchEvent(
645
- new Event('change', { bubbles: true }),
646
- );
647
- }
648
-
649
- // Update individual start/end inputs if they exist
650
- if (this._startDateInputElement && startDate) {
651
- this._startDateInputElement.value = formatDate(
652
- startDate,
653
- this._config.format,
654
- this._config,
655
- );
656
- this._startDateInputElement.dispatchEvent(
657
- new Event('change', { bubbles: true }),
658
- );
659
- }
660
-
661
- if (this._endDateInputElement && endDate) {
662
- this._endDateInputElement.value = formatDate(
663
- endDate,
664
- this._config.format,
665
- this._config,
666
- );
667
- this._endDateInputElement.dispatchEvent(
668
- new Event('change', { bubbles: true }),
669
- );
670
- }
671
-
672
- // Update display element for range
673
- this._updateRangeDisplayElement(startDate, endDate);
674
- }
675
- }
50
+ export class KTDatepicker extends KTComponent implements StateObserver {
51
+ protected override readonly _name: string = 'datepicker';
52
+ protected override _defaultConfig: KTDatepickerConfig = defaultDatepickerConfig;
53
+ protected override _config: KTDatepickerConfig = defaultDatepickerConfig;
54
+
55
+ /**
56
+ * Initialize the datepicker components after configuration is set
57
+ */
58
+ private _initializeDatepicker(): void {
59
+ // Set up templates
60
+ this._templateSet = getTemplateStrings(this._config);
61
+ this._templateRenderer = createTemplateRenderer(this._templateSet);
62
+
63
+ // Initialize state manager
64
+ this._unifiedStateManager = new KTDatepickerUnifiedStateManager({
65
+ enableValidation: true,
66
+ enableDebugging: this._config.debug || false,
67
+ enableUpdateBatching: true,
68
+ batchDelay: 16
69
+ });
70
+
71
+ // Set initial state
72
+ this._unifiedStateManager.updateState(this._getInitialState(), 'initialization', true);
73
+
74
+ // Subscribe to state changes
75
+ this._unsubscribeFromState = this._unifiedStateManager.subscribe(this);
76
+
77
+ // Initialize event manager
78
+ this._eventManager = new EventManager();
79
+
80
+ // Set up instance ID for debugging
81
+ this._instanceId = `datepicker-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
82
+ this._element.setAttribute('data-kt-datepicker-instance-id', this._instanceId);
83
+
84
+ // Set placeholder from config if available
85
+ if (this._input && this._config.placeholder) {
86
+ this._input.setAttribute('placeholder', this._config.placeholder);
87
+ }
88
+
89
+ // Set disabled state from config if available
90
+ if (this._input && this._config.disabled) {
91
+ this._input.setAttribute('disabled', 'true');
92
+ // Also set disabled state in unified state manager
93
+ this._unifiedStateManager.setDropdownDisabled(true, 'config');
94
+ }
95
+
96
+ // --- Time initialization ---
97
+ if (this._config.enableTime) {
98
+ this._unifiedStateManager.updateState({
99
+ timeGranularity: this._config.timeGranularity || 'minute'
100
+ }, 'config');
101
+
102
+ // Initialize time from selected date or current time
103
+ const baseDate = this._unifiedStateManager.getState().selectedDate || this._unifiedStateManager.getState().currentDate || new Date();
104
+ this._unifiedStateManager.setSelectedTime(dateToTimeState(baseDate), 'config');
105
+ }
106
+
107
+ // --- Mode-specific initialization ---
108
+ if (this._config.range && this._config.valueRange) {
109
+ this._initRangeFromConfig();
110
+ } else if (this._config.multiDate && Array.isArray(this._config.values)) {
111
+ this._initMultiDateFromConfig();
112
+ } else if (this._config.value) {
113
+ this._initSingleDateFromConfig();
114
+ }
115
+
116
+ // --- Input focus event for showOnFocus ---
117
+ if (this._input) {
118
+ this._eventManager.addListener(this._input, 'focus', this._onInputFocus);
119
+ }
120
+
121
+ // --- Document click event for outside click detection ---
122
+ this._eventManager.addListener(
123
+ document as unknown as HTMLElement,
124
+ 'click',
125
+ this._handleDocumentClick
126
+ );
127
+
128
+ (this._element as any).instance = this;
129
+
130
+ // Initial render
131
+ this._render();
132
+ }
133
+
134
+ /**
135
+ * Get the initial state for the datepicker
136
+ */
137
+ private _getInitialState(): KTDatepickerState {
138
+ const now = new Date();
139
+ const selectedDate = this._config.value ? new Date(this._config.value) : null;
140
+ const selectedTime = selectedDate && this._config.enableTime ? dateToTimeState(selectedDate) : null;
141
+
142
+ return {
143
+ currentDate: selectedDate || now,
144
+ selectedDate,
145
+ selectedRange: null,
146
+ selectedDates: [],
147
+ selectedTime,
148
+ timeGranularity: this._config.timeGranularity || 'minute',
149
+ viewMode: 'days',
150
+ isOpen: false,
151
+ isFocused: false,
152
+ isTransitioning: false,
153
+ isDisabled: !!this._config.disabled,
154
+ validationErrors: [],
155
+ isValid: true,
156
+ dropdownState: {
157
+ isOpen: false,
158
+ isTransitioning: false,
159
+ isDisabled: !!this._config.disabled,
160
+ isFocused: false,
161
+ },
162
+ };
163
+ }
164
+
165
+ protected _templateSet: ReturnType<typeof getTemplateStrings>;
166
+ private _userTemplates: Record<string, string | ((data: any) => string)> = {};
167
+ private _templateRenderer: TemplateRenderer;
168
+
169
+ private _container: HTMLElement;
170
+ private _input: HTMLInputElement | null = null;
171
+ private _eventManager: EventManager;
172
+ private _focusManager: FocusManager | null = null;
173
+ private _dropdownModule: KTDatepickerDropdown | null = null;
174
+ private _timePickerRenderer: { cleanup: () => void; update: (newTime: TimeState) => void } | null = null;
175
+
176
+ // Unified state manager
177
+ private _unifiedStateManager: KTDatepickerUnifiedStateManager;
178
+ private _unsubscribeFromState: (() => void) | null = null;
179
+
180
+
181
+ // Dynamic element detection
182
+ private _elementObserver: MutationObserver | null = null;
183
+ private _instanceId: string;
184
+
185
+ // DOM element cache for performance optimization
186
+ private _cachedElements: {
187
+ calendarElement: HTMLElement | null;
188
+ timePickerElement: HTMLElement | null;
189
+ startContainer: HTMLElement | null;
190
+ endContainer: HTMLElement | null;
191
+ yearElement: HTMLElement | null;
192
+ monthElement: HTMLElement | null;
193
+ dayElement: HTMLElement | null;
194
+ hourElement: HTMLElement | null;
195
+ minuteElement: HTMLElement | null;
196
+ secondElement: HTMLElement | null;
197
+ ampmElement: HTMLElement | null;
198
+ monthYearElement: HTMLElement | null;
199
+ timeDisplay: HTMLElement | null;
200
+ } = {
201
+ calendarElement: null,
202
+ timePickerElement: null,
203
+ startContainer: null,
204
+ endContainer: null,
205
+ yearElement: null,
206
+ monthElement: null,
207
+ dayElement: null,
208
+ hourElement: null,
209
+ minuteElement: null,
210
+ secondElement: null,
211
+ ampmElement: null,
212
+ monthYearElement: null,
213
+ timeDisplay: null
214
+ };
215
+
216
+
217
+ /**
218
+ * Initialize DOM element cache for performance optimization
219
+ */
220
+ private _initializeElementCache(): void {
221
+ // Cache main container elements
222
+ this._cachedElements.calendarElement = this._element.querySelector('[data-kt-datepicker-calendar-table]') as HTMLElement;
223
+ this._cachedElements.timePickerElement = this._element.querySelector('[data-kt-datepicker-time-container]') as HTMLElement;
224
+
225
+ // Cache range mode containers
226
+ this._cachedElements.startContainer = this._element.querySelector('[data-kt-datepicker-start-container]') as HTMLElement;
227
+ this._cachedElements.endContainer = this._element.querySelector('[data-kt-datepicker-end-container]') as HTMLElement;
228
+
229
+ // Cache segmented input elements
230
+ this._cachedElements.yearElement = this._element.querySelector('[data-segment="year"]') as HTMLElement;
231
+ this._cachedElements.monthElement = this._element.querySelector('[data-segment="month"]') as HTMLElement;
232
+ this._cachedElements.dayElement = this._element.querySelector('[data-segment="day"]') as HTMLElement;
233
+ this._cachedElements.hourElement = this._element.querySelector('[data-segment="hour"]') as HTMLElement;
234
+ this._cachedElements.minuteElement = this._element.querySelector('[data-segment="minute"]') as HTMLElement;
235
+ this._cachedElements.secondElement = this._element.querySelector('[data-segment="second"]') as HTMLElement;
236
+ this._cachedElements.ampmElement = this._element.querySelector('[data-segment="ampm"]') as HTMLElement;
237
+
238
+ // Cache navigation and display elements
239
+ this._cachedElements.monthYearElement = this._cachedElements.calendarElement?.querySelector('[data-kt-datepicker-month-year]') as HTMLElement;
240
+ this._cachedElements.timeDisplay = this._cachedElements.timePickerElement?.querySelector('[data-kt-datepicker-time-value]') as HTMLElement;
241
+ }
242
+
243
+ /**
244
+ * Refresh DOM element cache when structure changes
245
+ */
246
+ private _refreshElementCache(): void {
247
+ this._initializeElementCache();
248
+ }
249
+
250
+ /**
251
+ * Update input field with current state
252
+ */
253
+ private _updateInput(state: KTDatepickerState): void {
254
+ if (!this._input) return;
255
+
256
+ // Update input value
257
+ let value = '';
258
+ if (this._config.range && state.selectedRange) {
259
+ value = this._formatRange(state.selectedRange.start, state.selectedRange.end);
260
+ } else if (this._config.multiDate && state.selectedDates.length > 0) {
261
+ value = this._formatMultiDate(state.selectedDates);
262
+ } else if (state.selectedDate) {
263
+ value = this._formatSingleDate(state.selectedDate);
264
+ }
265
+
266
+ if (this._input.value !== value) {
267
+ this._input.value = value;
268
+ }
269
+
270
+ // Update disabled state
271
+ if (state.isDisabled) {
272
+ this._input.setAttribute('disabled', 'true');
273
+ } else {
274
+ this._input.removeAttribute('disabled');
275
+ }
276
+
277
+ // Update placeholder
278
+ if (value) {
279
+ this._input.removeAttribute('placeholder');
280
+ } else {
281
+ this._input.setAttribute('placeholder', 'Select date...');
282
+ }
283
+ }
284
+
285
+
286
+ /**
287
+ * Update single date segmented input
288
+ */
289
+ private _updateSingleSegmentedInput(state: KTDatepickerState): void {
290
+ const dateToUse = state.selectedDate || state.currentDate;
291
+ if (dateToUse) {
292
+ this._updateDateSegments(dateToUse);
293
+ }
294
+
295
+ // Update time segments if time is enabled
296
+ if (state.selectedTime) {
297
+ this._updateTimeSegments(state.selectedTime);
298
+ }
299
+ }
300
+
301
+ /**
302
+ * Update date segments in a specific container
303
+ */
304
+ private _updateDateSegmentsInContainer(date: Date, container: HTMLElement): void {
305
+ // For range mode, we need to query within the specific container
306
+ const yearElement = container.querySelector('[data-segment="year"]') as HTMLElement;
307
+ const monthElement = container.querySelector('[data-segment="month"]') as HTMLElement;
308
+ const dayElement = container.querySelector('[data-segment="day"]') as HTMLElement;
309
+
310
+ if (yearElement) {
311
+ yearElement.textContent = date.getFullYear().toString();
312
+ }
313
+ if (monthElement) {
314
+ monthElement.textContent = (date.getMonth() + 1).toString().padStart(2, '0');
315
+ }
316
+ if (dayElement) {
317
+ dayElement.textContent = date.getDate().toString().padStart(2, '0');
318
+ }
319
+ }
320
+
321
+ /**
322
+ * Update date segments (year, month, day)
323
+ */
324
+ private _updateDateSegments(date: Date): void {
325
+ this._updateDateSegmentsInContainer(date, this._element);
326
+ }
327
+
328
+ /**
329
+ * Update time segments
330
+ */
331
+ private _updateTimeSegments(time: TimeState): void {
332
+ if (this._cachedElements.hourElement) {
333
+ this._cachedElements.hourElement.textContent = time.hour.toString().padStart(2, '0');
334
+ }
335
+ if (this._cachedElements.minuteElement) {
336
+ this._cachedElements.minuteElement.textContent = time.minute.toString().padStart(2, '0');
337
+ }
338
+ if (this._cachedElements.secondElement) {
339
+ this._cachedElements.secondElement.textContent = time.second.toString().padStart(2, '0');
340
+ }
341
+ if (this._cachedElements.ampmElement) {
342
+ this._cachedElements.ampmElement.textContent = time.hour >= 12 ? 'PM' : 'AM';
343
+ }
344
+ }
345
+
346
+ /**
347
+ * Update calendar display
348
+ */
349
+ private _updateCalendar(state: KTDatepickerState): void {
350
+ // Try to find dropdown first
351
+ let dropdownEl: HTMLElement | null = this._element.querySelector('[data-kt-datepicker-dropdown]') as HTMLElement;
352
+
353
+ if (!dropdownEl && this._instanceId) {
354
+ dropdownEl = document.querySelector(`[data-kt-datepicker-dropdown][data-kt-datepicker-instance-id="${this._instanceId}"]`) as HTMLElement;
355
+ }
356
+
357
+ if (!dropdownEl) {
358
+ return;
359
+ }
360
+
361
+ // Find ALL calendar tables (for multi-month view support)
362
+ const calendarElements = dropdownEl.querySelectorAll('[data-kt-datepicker-calendar-table]') as NodeListOf<HTMLElement>;
363
+
364
+ // If no calendars found, return early
365
+ if (calendarElements.length === 0) {
366
+ return;
367
+ }
368
+
369
+ // Update cache with first calendar (for backward compatibility)
370
+ if (calendarElements.length > 0) {
371
+ this._cachedElements.calendarElement = calendarElements[0];
372
+ }
373
+
374
+ // Update range state on ALL calendar elements for hover handlers to access dynamically
375
+ if (this._config.range && state.selectedRange) {
376
+ calendarElements.forEach((calendar) => {
377
+ if (state.selectedRange.start) {
378
+ calendar.setAttribute('data-kt-range-start', formatDateToLocalString(state.selectedRange.start));
379
+ } else {
380
+ calendar.removeAttribute('data-kt-range-start');
381
+ }
382
+ if (state.selectedRange.end) {
383
+ calendar.setAttribute('data-kt-range-end', formatDateToLocalString(state.selectedRange.end));
384
+ } else {
385
+ calendar.removeAttribute('data-kt-range-end');
386
+ }
387
+ });
388
+ } else if (this._config.range) {
389
+ // No range selected, clear attributes from all calendars
390
+ calendarElements.forEach((calendar) => {
391
+ calendar.removeAttribute('data-kt-range-start');
392
+ calendar.removeAttribute('data-kt-range-end');
393
+ });
394
+ }
395
+
396
+ // Update date selection highlighting for ALL calendars
397
+ calendarElements.forEach((calendarElement) => {
398
+ this._updateDateSelection(state, calendarElement);
399
+ });
400
+
401
+ // Update navigation (month/year display) - only update first calendar for navigation
402
+ if (calendarElements.length > 0) {
403
+ this._updateNavigation(state, calendarElements[0]);
404
+ }
405
+ }
406
+
407
+ /**
408
+ * Update date selection highlighting
409
+ */
410
+ private _updateDateSelection(state: KTDatepickerState, calendarElement: HTMLElement): void {
411
+ // Clear previous selections
412
+ const selectedCells = calendarElement.querySelectorAll('[data-kt-selected]');
413
+ selectedCells.forEach(cell => {
414
+ cell.removeAttribute('data-kt-selected');
415
+ cell.removeAttribute('aria-selected');
416
+ cell.classList.remove('active'); // Remove active class if present (legacy support)
417
+ });
418
+
419
+ // Clear previous range highlighting (only if range is complete, not during hover preview)
420
+ // Check if we're in hover preview mode by checking if range has start but no end
421
+ if (state.selectedRange?.start && state.selectedRange?.end) {
422
+ // Range is complete, clear all hover-range attributes to re-apply for completed range
423
+ const hoverRangeCells = calendarElement.querySelectorAll('[data-kt-hover-range]');
424
+ hoverRangeCells.forEach(cell => {
425
+ (cell as HTMLElement).removeAttribute('data-kt-hover-range');
426
+ });
427
+ }
428
+
429
+ // Highlight selected date(s)
430
+ if (state.selectedDate) {
431
+ this._highlightDate(state.selectedDate, calendarElement);
432
+ }
433
+ // Highlight range start/end dates and dates in between
434
+ if (state.selectedRange) {
435
+ this._highlightDateRange(state.selectedRange, calendarElement);
436
+ }
437
+ // Highlight multi-date selections
438
+ if (state.selectedDates.length > 0) {
439
+ state.selectedDates.forEach(date => this._highlightDate(date, calendarElement));
440
+ }
441
+ }
442
+
443
+ /**
444
+ * Highlight a specific date
445
+ * Uses data-kt-selected attribute (class="active" removed for consistency)
446
+ */
447
+ private _highlightDate(date: Date, calendarElement: HTMLElement): void {
448
+ const cell = this._findDayCell(date, calendarElement);
449
+ if (cell) {
450
+ cell.setAttribute('data-kt-selected', 'true');
451
+ cell.setAttribute('aria-selected', 'true');
452
+ // Note: class="active" is redundant - CSS already targets data-kt-selected
453
+ }
454
+ }
455
+
456
+ /**
457
+ * Highlight a date range
458
+ * Uses data-kt-hover-range for both hover preview and completed ranges (consolidated)
459
+ */
460
+ private _highlightDateRange(range: { start: Date | null; end: Date | null }, calendarElement: HTMLElement): void {
461
+ if (!range.start || !range.end) {
462
+ // If only start or end is set, just highlight that date (hover preview handles the rest)
463
+ if (range.start) {
464
+ this._highlightDate(range.start, calendarElement);
465
+ }
466
+ if (range.end) {
467
+ this._highlightDate(range.end, calendarElement);
468
+ }
469
+ return;
470
+ }
471
+
472
+ // Normalize dates to local midnight for accurate comparison
473
+ const start = new Date(range.start);
474
+ start.setHours(0, 0, 0, 0);
475
+ const end = new Date(range.end);
476
+ end.setHours(0, 0, 0, 0);
477
+
478
+ // Determine actual start and end (handle backward selection)
479
+ const actualStart = start <= end ? start : end;
480
+ const actualEnd = start <= end ? end : start;
481
+
482
+ // Highlight start and end dates
483
+ this._highlightDate(actualStart, calendarElement);
484
+ this._highlightDate(actualEnd, calendarElement);
485
+
486
+ // Find all calendars in multi-month view to highlight range across all visible months
487
+ const dropdownEl = calendarElement.closest('[data-kt-datepicker-dropdown]') as HTMLElement;
488
+ const allCalendars = dropdownEl
489
+ ? Array.from(dropdownEl.querySelectorAll('[data-kt-datepicker-calendar-table]')) as HTMLElement[]
490
+ : [calendarElement];
491
+
492
+ // Highlight all dates in between using data-kt-hover-range (same as hover preview)
493
+ const current = new Date(actualStart);
494
+ current.setDate(current.getDate() + 1); // Start from day after start
495
+
496
+ while (current < actualEnd) {
497
+ const dateLocal = formatDateToLocalString(current);
498
+
499
+ // Search across all calendars to find the cell
500
+ for (const calendar of allCalendars) {
501
+ const cell = calendar.querySelector(`td[data-kt-datepicker-day][data-date="${dateLocal}"]`) as HTMLElement;
502
+ if (cell) {
503
+ // Use data-kt-hover-range for completed ranges (consolidated with hover preview)
504
+ cell.setAttribute('data-kt-hover-range', 'true');
505
+ break; // Found in this calendar, no need to search others
506
+ }
507
+ }
508
+ current.setDate(current.getDate() + 1);
509
+ }
510
+ }
511
+
512
+ /**
513
+ * Find day cell for a specific date
514
+ * Uses data-date attribute (YYYY-MM-DD format) for accurate date matching across months
515
+ * Searches within the provided calendar element, or across all calendars in multi-month view
516
+ */
517
+ private _findDayCell(date: Date, calendarElement: HTMLElement): HTMLElement | null {
518
+ const dateLocal = formatDateToLocalString(date); // Use local timezone date string for accurate matching (YYYY-MM-DD)
519
+
520
+ // First, try to find in the provided calendar element
521
+ const cells = calendarElement.querySelectorAll('td[data-kt-datepicker-day]');
522
+ for (const cell of Array.from(cells)) {
523
+ const cellDate = cell.getAttribute('data-date');
524
+ if (cellDate === dateLocal) {
525
+ return cell as HTMLElement;
526
+ }
527
+ }
528
+
529
+ // If not found in provided calendar, search across all calendars in multi-month view
530
+ // This handles cases where the date might be in a different visible month
531
+ const dropdownEl = calendarElement.closest('[data-kt-datepicker-dropdown]') as HTMLElement;
532
+ if (dropdownEl) {
533
+ const allCalendars = dropdownEl.querySelectorAll('[data-kt-datepicker-calendar-table]');
534
+ for (const calendar of Array.from(allCalendars)) {
535
+ const allCells = calendar.querySelectorAll('td[data-kt-datepicker-day]');
536
+ for (const cell of Array.from(allCells)) {
537
+ const cellDate = cell.getAttribute('data-date');
538
+ if (cellDate === dateLocal) {
539
+ return cell as HTMLElement;
540
+ }
541
+ }
542
+ }
543
+ }
544
+
545
+ // No fallback - if date not found by data-date, it's not in any visible calendar view
546
+ // This prevents incorrect matches (e.g., matching Oct 20 when looking for Nov 20)
547
+ return null;
548
+ }
549
+
550
+ /**
551
+ * Update navigation display
552
+ */
553
+ private _updateNavigation(state: KTDatepickerState, calendarElement: HTMLElement): void {
554
+ if (this._cachedElements.monthYearElement) {
555
+ const month = state.currentDate.toLocaleDateString('en-US', { month: 'long' });
556
+ const year = state.currentDate.getFullYear();
557
+ this._cachedElements.monthYearElement.textContent = `${month} ${year}`;
558
+ }
559
+ }
560
+
561
+ /**
562
+ * Update time picker display
563
+ */
564
+ private _updateTimePicker(state: KTDatepickerState): void {
565
+ if (!this._cachedElements.timePickerElement || !state.selectedTime) return;
566
+
567
+ if (this._cachedElements.timeDisplay) {
568
+ const timeString = `${state.selectedTime.hour.toString().padStart(2, '0')}:${state.selectedTime.minute.toString().padStart(2, '0')}:${state.selectedTime.second.toString().padStart(2, '0')}`;
569
+ this._cachedElements.timeDisplay.textContent = timeString;
570
+ }
571
+ }
572
+
573
+
574
+ /**
575
+ * Fire events based on state changes using the centralized event system
576
+ */
577
+ private _fireEvents(newState: KTDatepickerState, oldState: KTDatepickerState): void {
578
+ // Fire onChange when selected date changes
579
+ if (newState.selectedDate !== oldState.selectedDate ||
580
+ newState.selectedRange?.start !== oldState.selectedRange?.start ||
581
+ newState.selectedRange?.end !== oldState.selectedRange?.end ||
582
+ JSON.stringify(newState.selectedDates) !== JSON.stringify(oldState.selectedDates)) {
583
+
584
+ let selectedValue: Date | null = null;
585
+
586
+ if (this._config.range && newState.selectedRange) {
587
+ // For range mode, pass the end date if both are selected, otherwise null
588
+ selectedValue = newState.selectedRange.end || newState.selectedRange.start;
589
+ } else if (this._config.multiDate && newState.selectedDates.length > 0) {
590
+ // For multi-date mode, pass the last selected date
591
+ selectedValue = newState.selectedDates[newState.selectedDates.length - 1];
592
+ } else {
593
+ // For single date mode
594
+ selectedValue = newState.selectedDate;
595
+ }
596
+
597
+ this._fireDatepickerEvent('onChange', selectedValue, this);
598
+ }
599
+
600
+ // Fire onOpen when dropdown opens
601
+ if (newState.isOpen && !oldState.isOpen) {
602
+ this._fireDatepickerEvent('onOpen', this);
603
+ }
604
+
605
+ // Fire onClose when dropdown closes
606
+ if (!newState.isOpen && oldState.isOpen) {
607
+ this._fireDatepickerEvent('onClose', this);
608
+ }
609
+ }
610
+
611
+ /**
612
+ * Centralized event firing system - safely dispatches events with error handling
613
+ */
614
+ private _fireDatepickerEvent(eventName: keyof KTDatepickerConfig, ...args: any[]): void {
615
+ try {
616
+ const eventHandler = this._config[eventName] as Function;
617
+ if (typeof eventHandler === 'function') {
618
+ eventHandler(...args);
619
+ }
620
+ } catch (error) {
621
+ // Don't let event handler errors break the datepicker
622
+ }
623
+ }
624
+
625
+ // --- Mode-specific helpers ---
626
+ /** Initialize single date from config */
627
+ private _initSingleDateFromConfig() {
628
+ if (this._config.value) {
629
+ const date = new Date(this._config.value);
630
+ this._unifiedStateManager.setSelectedDate(date, 'config');
631
+ this._unifiedStateManager.setCurrentDate(date, 'config');
632
+ }
633
+ }
634
+
635
+ /** Initialize range from config */
636
+ private _initRangeFromConfig() {
637
+ if (this._config.valueRange) {
638
+ const start = this._config.valueRange.start ? new Date(this._config.valueRange.start) : null;
639
+ const end = this._config.valueRange.end ? new Date(this._config.valueRange.end) : null;
640
+ this._unifiedStateManager.setSelectedRange({ start, end }, 'config');
641
+ }
642
+ }
643
+
644
+ /** Initialize multi-date from config */
645
+ private _initMultiDateFromConfig() {
646
+ if (Array.isArray(this._config.values)) {
647
+ const dates = this._config.values.map((v: any) => new Date(v));
648
+ this._unifiedStateManager.setSelectedDates(dates, 'config');
649
+ }
650
+ }
651
+
652
+ /** Format single date for input */
653
+ private _formatSingleDate(date: Date): string {
654
+ if (!date) return '';
655
+
656
+ // If time is enabled, include time in the format
657
+ if (this._config.enableTime) {
658
+ if (this._config.format && typeof this._config.format === 'string') {
659
+ // Check if format already includes time tokens
660
+ const hasTimeTokens = /[Hhms]/.test(this._config.format);
661
+ if (hasTimeTokens) {
662
+ return formatDate(date, this._config.format);
663
+ } else {
664
+ // Add time format based on granularity and format
665
+ const timeFormat = this._getTimeFormat();
666
+ const dateFormat = this._config.format;
667
+ return formatDate(date, `${dateFormat} ${timeFormat}`);
668
+ }
669
+ } else {
670
+ // Default format with time
671
+ const timeFormat = this._getTimeFormat();
672
+ return `${date.toLocaleDateString(this._config.locale || 'en-US')} ${formatDate(date, timeFormat)}`;
673
+ }
674
+ } else {
675
+ // Time not enabled, use original logic
676
+ if (this._config.format && typeof this._config.format === 'string') {
677
+ return formatDate(date, this._config.format);
678
+ } else if (this._config.locale) {
679
+ return date.toLocaleDateString(this._config.locale);
680
+ } else {
681
+ return date.toLocaleDateString();
682
+ }
683
+ }
684
+ }
685
+
686
+ /**
687
+ * Get time format string based on granularity and time format
688
+ * @returns Time format string
689
+ */
690
+ private _getTimeFormat(): string {
691
+ const granularity = this._unifiedStateManager.getState().timeGranularity || 'minute';
692
+ const timeFormat = this._config.timeFormat || '24h';
693
+
694
+ switch (granularity) {
695
+ case 'hour':
696
+ return timeFormat === '12h' ? 'HH a' : 'HH';
697
+ case 'minute':
698
+ return timeFormat === '12h' ? 'HH:mm a' : 'HH:mm';
699
+ case 'second':
700
+ return timeFormat === '12h' ? 'HH:mm:ss a' : 'HH:mm:ss';
701
+ default:
702
+ return timeFormat === '12h' ? 'HH:mm a' : 'HH:mm';
703
+ }
704
+ }
705
+
706
+ /** Format range for input */
707
+ private _formatRange(start: Date | null, end: Date | null): string {
708
+ if (start && end) {
709
+ return `${this._formatSingleDate(start)} – ${this._formatSingleDate(end)}`;
710
+ } else if (start) {
711
+ return this._formatSingleDate(start);
712
+ }
713
+ return '';
714
+ }
715
+
716
+ /** Format multi-date for input */
717
+ private _formatMultiDate(dates: Date[]): string {
718
+ return dates.map((d) => this._formatSingleDate(d)).join(', ');
719
+ }
720
+
721
+
722
+
723
+ /** Select a single date */
724
+ private _selectSingleDate(date: Date) {
725
+ // Preserve time if time is enabled
726
+ if (this._config.enableTime) {
727
+ let timeToUse = this._unifiedStateManager.getState().selectedTime;
728
+
729
+ // If no selectedTime, try to extract from current selectedDate
730
+ if (!timeToUse) {
731
+ const currentSelectedDate = this._unifiedStateManager.getState().selectedDate;
732
+ if (currentSelectedDate) {
733
+ timeToUse = dateToTimeState(currentSelectedDate);
734
+ }
735
+ }
736
+
737
+ // If still no time, default to current time
738
+ if (!timeToUse) {
739
+ timeToUse = dateToTimeState(new Date());
740
+ }
741
+
742
+ const dateWithTime = applyTimeToDate(date, timeToUse);
743
+ this._unifiedStateManager.setSelectedDate(dateWithTime, 'calendar');
744
+ } else {
745
+ this._unifiedStateManager.setSelectedDate(date, 'calendar');
746
+ }
747
+
748
+ // Dispatch change event
749
+ if (this._input) {
750
+ const evt = new Event('change', { bubbles: true });
751
+ this._input.dispatchEvent(evt);
752
+ }
753
+ }
754
+
755
+ /**
756
+ * Select a range date (calendar click or segmented input change)
757
+ * Updates both segmented inputs and internal state.
758
+ */
759
+ private _selectRangeDate(date: Date) {
760
+ const currentState = this._unifiedStateManager.getState();
761
+
762
+ // First, let updateRangeSelection handle the date logic (it normalizes to midnight for comparison)
763
+ const newRange = updateRangeSelection(currentState.selectedRange, date);
764
+
765
+ // Then, if time is enabled, apply time to both start and end dates
766
+ if (this._config.enableTime) {
767
+ let timeToUse = currentState.selectedTime;
768
+
769
+ // If no selectedTime, try to extract from current selectedDate or range dates
770
+ if (!timeToUse) {
771
+ // Check if we have a start date with time
772
+ if (currentState.selectedRange?.start) {
773
+ timeToUse = dateToTimeState(currentState.selectedRange.start);
774
+ } else if (currentState.selectedDate) {
775
+ timeToUse = dateToTimeState(currentState.selectedDate);
776
+ }
777
+ }
778
+
779
+ // If still no time, default to current time
780
+ if (!timeToUse) {
781
+ timeToUse = dateToTimeState(new Date());
782
+ }
783
+
784
+ // Apply time to both start and end dates
785
+ const rangeWithTime = {
786
+ start: newRange.start ? applyTimeToDate(newRange.start, timeToUse) : null,
787
+ end: newRange.end ? applyTimeToDate(newRange.end, timeToUse) : null
788
+ };
789
+
790
+ this._unifiedStateManager.setSelectedRange(rangeWithTime, 'calendar');
791
+ } else {
792
+ this._unifiedStateManager.setSelectedRange(newRange, 'calendar');
793
+ }
794
+
795
+ if (this._input) {
796
+ const evt = new Event('change', { bubbles: true });
797
+ this._input.dispatchEvent(evt);
798
+ }
799
+ }
800
+
801
+ /** Select a multi-date */
802
+ private _selectMultiDate(date: Date) {
803
+ const currentState = this._unifiedStateManager.getState();
804
+ const currentDates = currentState.selectedDates || [];
805
+
806
+ // Preserve time if time is enabled
807
+ let dateToSelect = date;
808
+ if (this._config.enableTime) {
809
+ let timeToUse = currentState.selectedTime;
810
+
811
+ // If no selectedTime, try to extract from existing selected dates
812
+ if (!timeToUse && currentDates.length > 0) {
813
+ timeToUse = dateToTimeState(currentDates[0]);
814
+ } else if (!timeToUse && currentState.selectedDate) {
815
+ timeToUse = dateToTimeState(currentState.selectedDate);
816
+ }
817
+
818
+ // If still no time, default to current time
819
+ if (!timeToUse) {
820
+ timeToUse = dateToTimeState(new Date());
821
+ }
822
+
823
+ dateToSelect = applyTimeToDate(date, timeToUse);
824
+ }
825
+
826
+ const exists = currentDates.some((d) => d.getTime() === dateToSelect.getTime());
827
+ let newDates: Date[];
828
+
829
+ if (exists) {
830
+ newDates = currentDates.filter((d) => d.getTime() !== dateToSelect.getTime());
831
+ } else {
832
+ newDates = [...currentDates, dateToSelect];
833
+ }
834
+
835
+ this._unifiedStateManager.setSelectedDates(newDates, 'calendar');
836
+
837
+ if (this._input) {
838
+ const evt = new Event('change', { bubbles: true });
839
+ this._input.dispatchEvent(evt);
840
+ }
841
+ }
842
+
843
+ /** Handler for Apply button in multi-date mode */
844
+ private _onApplyMultiDate = (e: Event) => {
845
+ // Apply button clicked in multi-date mode
846
+ };
847
+
848
+ private _onToday = (e: Event) => {
849
+ e.preventDefault();
850
+ const today = new Date();
851
+ this.setDate(today);
852
+ };
853
+
854
+ // Stub for _onClear (to be implemented next)
855
+ private _onClear = (e: Event) => {
856
+ e.preventDefault();
857
+ // Clear all selection states using unified state manager
858
+ this._unifiedStateManager.updateState({
859
+ selectedDate: null,
860
+ selectedRange: { start: null, end: null },
861
+ selectedDates: [],
862
+ selectedTime: null
863
+ }, 'clear');
864
+
865
+ if (this._input) {
866
+ const evt = new Event('change', { bubbles: true });
867
+ this._input.dispatchEvent(evt);
868
+ }
869
+ };
870
+
871
+ /**
872
+ * Handler for Apply button - confirms selection and closes dropdown
873
+ * Used in range and multi-date modes
874
+ */
875
+ private _onApply = (e: Event) => {
876
+ e.preventDefault();
877
+ e.stopPropagation();
878
+
879
+ const currentState = this._unifiedStateManager.getState();
880
+
881
+ // Ensure input value is updated with current selection
882
+ this._updateInput(currentState);
883
+
884
+ // Fire onChange event if there's a selection
885
+ if (this._config.range && currentState.selectedRange) {
886
+ if (currentState.selectedRange.start || currentState.selectedRange.end) {
887
+ this._fireDatepickerEvent('onChange', currentState.selectedRange, this);
888
+ }
889
+ } else if (this._config.multiDate && currentState.selectedDates.length > 0) {
890
+ this._fireDatepickerEvent('onChange', currentState.selectedDates, this);
891
+ } else if (currentState.selectedDate) {
892
+ this._fireDatepickerEvent('onChange', currentState.selectedDate, this);
893
+ }
894
+
895
+ // Trigger input change event
896
+ if (this._input) {
897
+ const evt = new Event('change', { bubbles: true });
898
+ this._input.dispatchEvent(evt);
899
+ }
900
+
901
+ // Close the dropdown
902
+ this._unifiedStateManager.setDropdownOpen(false, 'apply-button');
903
+ };
904
+
905
+ /**
906
+ * Centralized keyboard event handler for all datepicker keyboard interactions.
907
+ * Handles navigation, selection, and closing for input, calendar, and popover.
908
+ * Covers: Tab, Shift+Tab, Arrow keys, Enter, Space, Escape, Home, End, PageUp, PageDown.
909
+ */
910
+ private _onKeyDown = (e: KeyboardEvent) => {
911
+ if (!this._unifiedStateManager.isDropdownOpen()) return;
912
+ const target = e.target as HTMLElement;
913
+
914
+ // Check if segmented input is focused - let it handle its own keyboard events
915
+ if (target.closest('[data-segment]')) {
916
+ return; // Let segmented input handle its own keyboard events
917
+ }
918
+
919
+ // Handle Escape key
920
+ if (e.key === 'Escape') {
921
+ e.preventDefault();
922
+ return;
923
+ }
924
+ // Handle Tab/Shift+Tab: allow normal tabbing, but trap focus within dropdown if needed
925
+ if (e.key === 'Tab') {
926
+ // Optionally implement focus trap if required
927
+ return;
928
+ }
929
+ // Handle Arrow keys, Home/End, PageUp/PageDown for calendar grid navigation
930
+ const isCalendarGrid = target.closest('[data-kt-datepicker-calendar-grid]');
931
+ if (isCalendarGrid) {
932
+ // Find all day buttons
933
+ const dayButtons = Array.from(isCalendarGrid.querySelectorAll('button[data-day]')) as HTMLButtonElement[];
934
+ const currentIndex = dayButtons.findIndex(btn => btn === target);
935
+ let nextIndex = currentIndex;
936
+ if (e.key === 'ArrowRight') nextIndex = Math.min(dayButtons.length - 1, currentIndex + 1);
937
+ if (e.key === 'ArrowLeft') nextIndex = Math.max(0, currentIndex - 1);
938
+ if (e.key === 'ArrowDown') nextIndex = Math.min(dayButtons.length - 1, currentIndex + 7);
939
+ if (e.key === 'ArrowUp') nextIndex = Math.max(0, currentIndex - 7);
940
+ if (e.key === 'Home') nextIndex = Math.floor(currentIndex / 7) * 7;
941
+ if (e.key === 'End') nextIndex = Math.min(dayButtons.length - 1, Math.floor(currentIndex / 7) * 7 + 6);
942
+ if (e.key === 'PageUp' || e.key === 'PageDown') {
943
+ // Change month and focus first day
944
+ this._changeMonth(e.key === 'PageUp' ? -1 : 1);
945
+ setTimeout(() => {
946
+ const newGrid = this._element.querySelector('[data-kt-datepicker-calendar-grid]');
947
+ if (newGrid) {
948
+ const newButtons = Array.from(newGrid.querySelectorAll('button[data-day]')) as HTMLButtonElement[];
949
+ if (newButtons.length > 0) {
950
+ // Set roving tabindex
951
+ newButtons.forEach((btn, idx) => btn.tabIndex = idx === 0 ? 0 : -1);
952
+ newButtons[0].focus();
953
+ }
954
+ }
955
+ }, 0);
956
+ e.preventDefault();
957
+ return;
958
+ }
959
+ if (nextIndex !== currentIndex && dayButtons[nextIndex]) {
960
+ // Set roving tabindex
961
+ dayButtons.forEach((btn, idx) => btn.tabIndex = idx === nextIndex ? 0 : -1);
962
+ dayButtons[nextIndex].focus();
963
+ e.preventDefault();
964
+ return;
965
+ }
966
+ // Enter/Space: select date
967
+ if (e.key === 'Enter' || e.key === ' ') {
968
+ dayButtons[currentIndex]?.click();
969
+ // Optionally announce selection to screen reader
970
+ const liveRegion = this._element.querySelector('[data-kt-datepicker-live]');
971
+ if (liveRegion && dayButtons[currentIndex]) {
972
+ liveRegion.textContent = `Selected ${dayButtons[currentIndex].getAttribute('aria-label')}`;
973
+ }
974
+ e.preventDefault();
975
+ return;
976
+ }
977
+ }
978
+ // Handle navigation for header buttons (prev/next month)
979
+ if (target.hasAttribute('data-kt-datepicker-prev') || target.hasAttribute('data-kt-datepicker-next')) {
980
+ if (e.key === 'Enter' || e.key === ' ') {
981
+ target.dispatchEvent(new MouseEvent('click', { bubbles: true }));
982
+ // Optionally announce navigation to screen reader
983
+ const liveRegion = this._element.querySelector('[data-kt-datepicker-live]');
984
+ if (liveRegion) {
985
+ liveRegion.textContent = target.hasAttribute('data-kt-datepicker-prev') ? 'Previous month' : 'Next month';
986
+ }
987
+ e.preventDefault();
988
+ return;
989
+ }
990
+ }
991
+ // Handle footer buttons (today, clear, apply)
992
+ if (target.hasAttribute('data-kt-datepicker-today') || target.hasAttribute('data-kt-datepicker-clear') || target.hasAttribute('data-kt-datepicker-apply')) {
993
+ if (e.key === 'Enter' || e.key === ' ') {
994
+ target.dispatchEvent(new MouseEvent('click', { bubbles: true }));
995
+ // Optionally announce action to screen reader
996
+ const liveRegion = this._element.querySelector('[data-kt-datepicker-live]');
997
+ if (liveRegion) {
998
+ if (target.hasAttribute('data-kt-datepicker-today')) liveRegion.textContent = 'Today selected';
999
+ if (target.hasAttribute('data-kt-datepicker-clear')) liveRegion.textContent = 'Selection cleared';
1000
+ if (target.hasAttribute('data-kt-datepicker-apply')) liveRegion.textContent = 'Selection applied';
1001
+ }
1002
+ e.preventDefault();
1003
+ return;
1004
+ }
1005
+ }
1006
+ };
1007
+
1008
+ /**
1009
+ * Constructor: Initializes the datepicker component
1010
+ */
1011
+ constructor(element: HTMLElement, config?: KTDatepickerConfig) {
1012
+ super();
1013
+
1014
+ // Generate unique instance ID
1015
+ this._instanceId = `datepicker-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
1016
+
1017
+ // Add instance ID to element for debugging
1018
+ element.setAttribute('data-kt-datepicker-instance-id', this._instanceId);
1019
+
1020
+ this._init(element);
1021
+
1022
+ // Build config using the standard KTComponent approach
1023
+ this._buildConfig(config);
1024
+ this._templateSet = getTemplateStrings(this._config);
1025
+ this._templateRenderer = createTemplateRenderer(this._templateSet);
1026
+
1027
+ // Initialize unified state manager
1028
+ this._unifiedStateManager = new KTDatepickerUnifiedStateManager({
1029
+ enableValidation: true,
1030
+ enableDebugging: this._config.debug || false,
1031
+ enableUpdateBatching: true,
1032
+ batchDelay: 16
1033
+ });
1034
+
1035
+ // Subscribe to state changes
1036
+ this._unsubscribeFromState = this._unifiedStateManager.subscribe(this);
1037
+
1038
+
1039
+
1040
+
1041
+
1042
+ // Set placeholder from config if available
1043
+ if (this._input && this._config.placeholder) {
1044
+ this._input.setAttribute('placeholder', this._config.placeholder);
1045
+ }
1046
+ // Set disabled state from config if available
1047
+ if (this._input && this._config.disabled) {
1048
+ this._input.setAttribute('disabled', 'true');
1049
+
1050
+ // Also set disabled state in unified state manager
1051
+ this._unifiedStateManager.setDropdownDisabled(true, 'config');
1052
+ }
1053
+ // --- Time initialization ---
1054
+ if (this._config.enableTime) {
1055
+ this._unifiedStateManager.updateState({
1056
+ timeGranularity: this._config.timeGranularity || 'minute'
1057
+ }, 'config');
1058
+
1059
+ // Initialize time from selected date or current time
1060
+ const baseDate = this._unifiedStateManager.getState().selectedDate || this._unifiedStateManager.getState().currentDate || new Date();
1061
+ this._unifiedStateManager.setSelectedTime(dateToTimeState(baseDate), 'config');
1062
+ }
1063
+
1064
+ // --- Mode-specific initialization ---
1065
+ if (this._config.range && this._config.valueRange) {
1066
+ this._initRangeFromConfig();
1067
+ } else if (this._config.multiDate && Array.isArray(this._config.values)) {
1068
+ this._initMultiDateFromConfig();
1069
+ } else if (this._config.value) {
1070
+ this._initSingleDateFromConfig();
1071
+ }
1072
+ // --- Input focus event for showOnFocus ---
1073
+ if (this._input) {
1074
+ this._eventManager.addListener(this._input, 'focus', this._onInputFocus);
1075
+ }
1076
+
1077
+ // --- Document click event for outside click detection ---
1078
+ this._eventManager.addListener(
1079
+ document as unknown as HTMLElement,
1080
+ 'click',
1081
+ this._handleDocumentClick
1082
+ );
1083
+
1084
+ (element as any).instance = this;
1085
+ this._render();
1086
+ }
1087
+
1088
+ /**
1089
+ * Handler for input focus event, opens the datepicker if showOnFocus is true and input is not disabled/readonly
1090
+ */
1091
+ private _onInputFocus = (e: FocusEvent) => {
1092
+ if (!this._input) return;
1093
+ if (this._input.hasAttribute('disabled') || this._input.hasAttribute('readonly')) return;
1094
+ if (this._config.showOnFocus) {
1095
+ this.open();
1096
+ }
1097
+ };
1098
+
1099
+ /**
1100
+ * Handler for document click event, closes the datepicker if click is outside the component
1101
+ */
1102
+ private _handleDocumentClick = (e: MouseEvent): void => {
1103
+ // Skip if click-outside is disabled
1104
+ if (!this._config.closeOnOutsideClick) return;
1105
+
1106
+ // Skip if dropdown is not open
1107
+ if (!this._unifiedStateManager.isDropdownOpen()) return;
1108
+
1109
+ const targetElement = e.target as HTMLElement;
1110
+
1111
+ // Find the dropdown element (it's rendered in body, not inside _element)
1112
+ const dropdownElement = document.querySelector(`[data-kt-datepicker-dropdown][data-kt-datepicker-instance-id="${this._instanceId}"]`) as HTMLElement;
1113
+
1114
+ // Check if click is outside both the datepicker element and the dropdown
1115
+ const isInsideDatepicker = this._element.contains(targetElement);
1116
+ const isInsideDropdown = dropdownElement && dropdownElement.contains(targetElement);
1117
+
1118
+ if (!isInsideDatepicker && !isInsideDropdown) {
1119
+ this.close();
1120
+ }
1121
+ };
1122
+
1123
+ protected _init(element: HTMLElement) {
1124
+ this._element = element;
1125
+ // Find or assign the input
1126
+ this._input = this._element.querySelector('input[data-kt-datepicker-input]');
1127
+ if (!this._input) {
1128
+ // Fallback: find the first input and add the attribute
1129
+ const firstInput = this._element.querySelector('input');
1130
+ if (firstInput) {
1131
+ firstInput.setAttribute('data-kt-datepicker-input', '');
1132
+ this._input = firstInput;
1133
+ } else {
1134
+ // If no input exists, create one and append
1135
+ const newInput = document.createElement('input');
1136
+ newInput.setAttribute('data-kt-datepicker-input', '');
1137
+ this._element.appendChild(newInput);
1138
+ this._input = newInput;
1139
+ }
1140
+ }
1141
+ }
1142
+
1143
+ /**
1144
+ * Build config by merging defaults and user config
1145
+ */
1146
+ protected override _buildConfig(config?: KTDatepickerConfig) {
1147
+ // First call parent to read data attributes
1148
+ super._buildConfig(config);
1149
+
1150
+ // Merge templates separately to ensure correct type
1151
+ const mergedTemplates = {
1152
+ ...defaultTemplates,
1153
+ ...(this._config.templates || {}),
1154
+ ...(this._userTemplates || {})
1155
+ };
1156
+ // Determine closeOnSelect default based on mode and requirements
1157
+ let closeOnSelect: boolean;
1158
+ if (typeof this._config.closeOnSelect !== 'undefined') {
1159
+ // User explicitly set closeOnSelect, respect their choice
1160
+ closeOnSelect = this._config.closeOnSelect!;
1161
+ } else if (this._config.enableTime) {
1162
+ // Time-enabled: never close on date selection
1163
+ closeOnSelect = false;
1164
+ } else if (this._config.range) {
1165
+ // Range mode: handle clicks inside dropdown
1166
+ closeOnSelect = false;
1167
+ } else if (this._config.multiDate) {
1168
+ // Multi-date mode: don't close on individual selections
1169
+ closeOnSelect = false;
1170
+ } else {
1171
+ // Single date only: close on date click
1172
+ closeOnSelect = true;
1173
+ }
1174
+
1175
+ // Merge with data attributes and passed config
1176
+ // Important: data attributes (this._config) must come after defaults to override them
1177
+ this._config = {
1178
+ ...defaultDatepickerConfig,
1179
+ ...this._config, // Data attributes override defaults
1180
+ ...(config || {}),
1181
+ templates: mergedTemplates,
1182
+ closeOnSelect,
1183
+ };
1184
+
1185
+ // Initialize event manager
1186
+ this._eventManager = new EventManager();
1187
+
1188
+ // Initialize focus manager for keyboard navigation
1189
+ this._focusManager = new FocusManager({
1190
+ enableFocusTrapping: true,
1191
+ enableFocusRestoration: true,
1192
+ enableKeyboardNavigation: true,
1193
+ enableDebugging: this._config.debug || false
1194
+ });
1195
+ }
1196
+
1197
+ /**
1198
+ * Public method to set/override templates at runtime (supports string or function)
1199
+ */
1200
+ public setTemplates(templates: Record<string, string | ((data: any) => string)>) {
1201
+ this._userTemplates = { ...this._userTemplates, ...templates };
1202
+ this._templateSet = getTemplateStrings(this._config);
1203
+ this._templateRenderer.updateTemplates(this._templateSet);
1204
+ this._render();
1205
+ }
1206
+
1207
+ /**
1208
+ * Render the main container and set this._container
1209
+ */
1210
+ private _renderContainer(): HTMLElement {
1211
+ const containerEl = this._templateRenderer.renderTemplateToElement({
1212
+ templateKey: 'container',
1213
+ data: {},
1214
+ configClasses: this._config.classes
1215
+ });
1216
+ this._container = containerEl;
1217
+ return containerEl;
1218
+ }
1219
+
1220
+ /**
1221
+ * Render the input wrapper and calendar button, move/create input element
1222
+ * In range mode, renders two segmented inputs (start/end) using the segmentedDateRangeInput template.
1223
+ */
1224
+ private _renderInputWrapper(calendarButtonHtml: string): HTMLElement {
1225
+ const inputWrapperTpl = this._templateSet.inputWrapper || defaultTemplates.inputWrapper;
1226
+ // Set input to hidden instead of removing it, so it remains in DOM for form submission
1227
+ if (this._input) {
1228
+ this._input.type = 'hidden';
1229
+ // Remove any visible styling classes that might interfere
1230
+ this._input.classList.remove('hidden');
1231
+ }
1232
+ if (this._config.range) {
1233
+ const rangeTpl = this._templateSet.segmentedDateRangeInput || defaultTemplates.segmentedDateRangeInput;
1234
+ const { inputWrapperEl, startContainer, endContainer } = renderRangeSegmentedInputUI(inputWrapperTpl, rangeTpl, calendarButtonHtml, this._config);
1235
+ instantiateRangeSegmentedInputs(
1236
+ startContainer,
1237
+ endContainer,
1238
+ this._unifiedStateManager.getState(),
1239
+ this._config,
1240
+ (date: Date) => {
1241
+ const end = this._unifiedStateManager.getState().selectedRange?.end || null;
1242
+ let newEnd = end;
1243
+ if (end && date > end) newEnd = null;
1244
+ this._unifiedStateManager.updateState({ selectedRange: { start: date, end: newEnd } }, 'range-selection');
1245
+ this._render();
1246
+ },
1247
+ (date: Date) => {
1248
+ const start = this._unifiedStateManager.getState().selectedRange?.start || null;
1249
+ let newStart = start;
1250
+ if (start && date < start) newStart = null;
1251
+ this._unifiedStateManager.updateState({ selectedRange: { start: newStart, end: date } }, 'range-selection');
1252
+ this._render();
1253
+ }
1254
+ );
1255
+ return inputWrapperEl;
1256
+ }
1257
+ // Single-date mode
1258
+ const inputWrapperEl = renderSingleSegmentedInputUI(inputWrapperTpl, calendarButtonHtml, this._config);
1259
+
1260
+ // Find the segmented input container that was rendered by the template system
1261
+ const segmentedInputContainer = inputWrapperEl.querySelector('[data-kt-datepicker-segmented-input]') as HTMLElement;
1262
+
1263
+ instantiateSingleSegmentedInput(segmentedInputContainer as HTMLElement, this._unifiedStateManager.getState(), this._config, (date: Date) => {
1264
+ this.setDate(date);
1265
+ });
1266
+ return inputWrapperEl;
1267
+ }
1268
+
1269
+ /**
1270
+ * Bind event listener to the calendar button and input wrapper
1271
+ */
1272
+ private _bindCalendarButtonEvent(inputWrapperEl: HTMLElement) {
1273
+ const buttonEl = inputWrapperEl.querySelector('button[data-kt-datepicker-calendar-btn]');
1274
+ if (buttonEl && buttonEl instanceof HTMLButtonElement) {
1275
+ buttonEl.type = 'button';
1276
+ buttonEl.setAttribute('aria-label', this._config.calendarButtonAriaLabel || 'Open calendar');
1277
+ buttonEl.addEventListener('click', (e) => {
1278
+ e.preventDefault();
1279
+ e.stopPropagation();
1280
+ if (this._config.disabled || buttonEl.hasAttribute('disabled')) {
1281
+ return;
1282
+ }
1283
+ this.toggle();
1284
+ });
1285
+ }
1286
+
1287
+ // Add click handler to entire input wrapper for better UX
1288
+ inputWrapperEl.addEventListener('click', (e) => {
1289
+ // Don't handle if disabled
1290
+ if (this._config.disabled) {
1291
+ return;
1292
+ }
1293
+
1294
+ // Don't handle if target is a focusable input element (segmented input)
1295
+ const target = e.target as HTMLElement;
1296
+ if (target.hasAttribute('contenteditable') || target.hasAttribute('data-segment')) {
1297
+ return;
1298
+ }
1299
+
1300
+ // Don't handle if already handled by button click
1301
+ if (target === buttonEl || buttonEl?.contains(target)) {
1302
+ return;
1303
+ }
1304
+
1305
+ // Open the datepicker
1306
+ this.open();
1307
+ });
1308
+ }
1309
+
1310
+ /**
1311
+ * Render the dropdown container from template
1312
+ */
1313
+ private _renderDropdown(): HTMLElement {
1314
+ const dropdownEl = this._templateRenderer.renderTemplateToElement({
1315
+ templateKey: 'dropdown',
1316
+ data: {},
1317
+ configClasses: this._config.classes
1318
+ });
1319
+ dropdownEl.setAttribute('data-kt-datepicker-dropdown', '');
1320
+
1321
+ // Add instance association for better identification
1322
+ if (this._instanceId) {
1323
+ dropdownEl.setAttribute('data-kt-datepicker-instance-id', this._instanceId);
1324
+ }
1325
+
1326
+ if (!this._unifiedStateManager.isDropdownOpen()) {
1327
+ dropdownEl.classList.add('hidden');
1328
+ }
1329
+ return dropdownEl;
1330
+ }
1331
+
1332
+ /**
1333
+ * Render header, calendar, and footer into the dropdown element
1334
+ */
1335
+ private _renderDropdownContent(dropdownEl: HTMLElement) {
1336
+ const visibleMonths = this._config.visibleMonths ?? 1;
1337
+
1338
+ if (visibleMonths === 1) {
1339
+ this._renderSingleMonth(dropdownEl);
1340
+ } else {
1341
+ this._renderMultiMonth(dropdownEl, visibleMonths);
1342
+ }
1343
+
1344
+ // --- Render footer using template-driven buttons (conditional by mode) ---
1345
+ const isRange = !!this._config.range;
1346
+ const isMultiDate = !!this._config.multiDate;
1347
+ const showFooter = isRange || isMultiDate;
1348
+ if (showFooter) {
1349
+ let todayButtonHtml: string | undefined = undefined;
1350
+ let clearButtonHtml: string | undefined = undefined;
1351
+ let applyButtonHtml: string | undefined = undefined;
1352
+ const todayButtonTpl = this._templateSet.todayButton || defaultTemplates.todayButton;
1353
+ const clearButtonTpl = this._templateSet.clearButton || defaultTemplates.clearButton;
1354
+ const applyButtonTpl = this._templateSet.applyButton || defaultTemplates.applyButton;
1355
+ // Only show Today/Clear if explicitly enabled in config/templates
1356
+ if (this._config.showTodayButton) {
1357
+ todayButtonHtml = typeof todayButtonTpl === 'function' ? todayButtonTpl({}) : todayButtonTpl;
1358
+ }
1359
+ if (this._config.showClearButton) {
1360
+ clearButtonHtml = typeof clearButtonTpl === 'function' ? clearButtonTpl({}) : clearButtonTpl;
1361
+ }
1362
+ // Always show Apply in range/multi-date mode
1363
+ applyButtonHtml = typeof applyButtonTpl === 'function' ? applyButtonTpl({}) : applyButtonTpl;
1364
+ const footer = renderFooter(
1365
+ this._templateSet.footer,
1366
+ { todayButton: todayButtonHtml, clearButton: clearButtonHtml, applyButton: applyButtonHtml },
1367
+ this._onToday,
1368
+ this._onClear,
1369
+ this._onApply
1370
+ );
1371
+ dropdownEl.appendChild(footer);
1372
+ }
1373
+
1374
+ // --- Render time picker if enabled ---
1375
+ const currentState = this._unifiedStateManager.getState();
1376
+ if (this._config.enableTime && currentState.selectedTime) {
1377
+ const timePickerContainer = this._templateRenderer.renderTemplateToElement({
1378
+ templateKey: 'timePickerWrapper',
1379
+ data: {},
1380
+ configClasses: this._config.classes
1381
+ });
1382
+ timePickerContainer.setAttribute('data-kt-datepicker-time-container', '');
1383
+
1384
+ // Store the time picker renderer result
1385
+ this._timePickerRenderer = renderTimePicker(timePickerContainer, {
1386
+ time: currentState.selectedTime,
1387
+ granularity: currentState.timeGranularity,
1388
+ format: this._config.timeFormat || '24h',
1389
+ minTime: this._config.minTime,
1390
+ maxTime: this._config.maxTime,
1391
+ timeStep: this._config.timeStep || 1,
1392
+ disabled: !!this._config.disabled,
1393
+ onChange: (newTime: any) => {
1394
+ // Update unified state manager
1395
+ this._unifiedStateManager.updateState({
1396
+ selectedTime: newTime
1397
+ }, 'time-picker');
1398
+
1399
+ // Apply time to selected date if exists
1400
+ const updatedState = this._unifiedStateManager.getState();
1401
+ if (updatedState.selectedDate) {
1402
+ const dateWithTime = applyTimeToDate(updatedState.selectedDate, newTime);
1403
+ this._unifiedStateManager.updateState({
1404
+ selectedDate: dateWithTime
1405
+ }, 'time-picker');
1406
+ }
1407
+
1408
+ // Update the time picker renderer with the new state
1409
+ if (this._timePickerRenderer) {
1410
+ this._timePickerRenderer.update(newTime);
1411
+ }
1412
+ },
1413
+ templates: this._templateSet
1414
+ });
1415
+
1416
+ dropdownEl.appendChild(timePickerContainer);
1417
+ }
1418
+ }
1419
+
1420
+ /**
1421
+ * Render single month calendar
1422
+ */
1423
+ private _renderSingleMonth(dropdownEl: HTMLElement) {
1424
+ let prevButtonHtml: string;
1425
+ let nextButtonHtml: string;
1426
+ const prevButtonTpl = this._templateSet.prevButton || defaultTemplates.prevButton;
1427
+ const nextButtonTpl = this._templateSet.nextButton || defaultTemplates.nextButton;
1428
+ prevButtonHtml = typeof prevButtonTpl === 'function' ? prevButtonTpl({}) : prevButtonTpl;
1429
+ nextButtonHtml = typeof nextButtonTpl === 'function' ? nextButtonTpl({}) : nextButtonTpl;
1430
+
1431
+ const currentState = this._unifiedStateManager.getState();
1432
+ const header = renderHeader(
1433
+ this._templateSet.header,
1434
+ {
1435
+ month: currentState.currentDate.toLocaleString(this._config.locale, { month: 'long' }),
1436
+ year: currentState.currentDate.getFullYear(),
1437
+ prevButton: prevButtonHtml,
1438
+ nextButton: nextButtonHtml,
1439
+ },
1440
+ (e) => { e.stopPropagation(); this._changeMonth(-1); },
1441
+ (e) => { e.stopPropagation(); this._changeMonth(1); }
1442
+ );
1443
+ dropdownEl.appendChild(header);
1444
+
1445
+ const dayClickHandler = (day: Date) => {
1446
+ this.setDate(day);
1447
+
1448
+ };
1449
+
1450
+ const calendar = renderCalendar(
1451
+ this._templateSet.dayCell,
1452
+ this._getCalendarDays(currentState.currentDate),
1453
+ currentState.currentDate,
1454
+ currentState.selectedDate,
1455
+ dayClickHandler,
1456
+ this._config.locale,
1457
+ this._config.range ? currentState.selectedRange : undefined,
1458
+ this._config.multiDate ? currentState.selectedDates : undefined
1459
+ );
1460
+ dropdownEl.appendChild(calendar);
1461
+ }
1462
+
1463
+ /**
1464
+ * Get array of dates for multi-month display
1465
+ * @param baseDate - The base date to calculate months from
1466
+ * @param count - Number of months to generate
1467
+ * @returns Array of dates representing the first day of each month
1468
+ */
1469
+ private _getMultiMonthDates(baseDate: Date, count: number): Date[] {
1470
+ const dates: Date[] = [];
1471
+ for (let i = 0; i < count; i++) {
1472
+ const monthDate = new Date(baseDate.getFullYear(), baseDate.getMonth() + i, 1);
1473
+ dates.push(monthDate);
1474
+ }
1475
+ return dates;
1476
+ }
1477
+
1478
+ /**
1479
+ * Render a single calendar month for multi-month display
1480
+ * @param monthDate - The date representing the month to render
1481
+ * @param index - Index of this month in the multi-month display
1482
+ * @param totalMonths - Total number of months being displayed
1483
+ * @returns HTMLElement containing the rendered month
1484
+ */
1485
+ private _renderMultiMonthCalendar(monthDate: Date, index: number, totalMonths: number): HTMLElement {
1486
+ // Navigation buttons: only first gets prev, only last gets next
1487
+ let prevButtonHtml = '';
1488
+ let nextButtonHtml = '';
1489
+ if (index === 0) {
1490
+ const prevButtonTpl = this._templateSet.prevButton || defaultTemplates.prevButton;
1491
+ prevButtonHtml = typeof prevButtonTpl === 'function' ? prevButtonTpl({}) : prevButtonTpl;
1492
+ }
1493
+ if (index === totalMonths - 1) {
1494
+ const nextButtonTpl = this._templateSet.nextButton || defaultTemplates.nextButton;
1495
+ nextButtonHtml = typeof nextButtonTpl === 'function' ? nextButtonTpl({}) : nextButtonTpl;
1496
+ }
1497
+
1498
+ const header = renderHeader(
1499
+ this._templateSet.header,
1500
+ {
1501
+ month: monthDate.toLocaleString(this._config.locale, { month: 'long' }),
1502
+ year: monthDate.getFullYear(),
1503
+ prevButton: prevButtonHtml,
1504
+ nextButton: nextButtonHtml,
1505
+ },
1506
+ (e) => { e.stopPropagation(); this._changeMonth(-1); },
1507
+ (e) => { e.stopPropagation(); this._changeMonth(1); }
1508
+ );
1509
+
1510
+ const currentState = this._unifiedStateManager.getState();
1511
+ const calendar = renderCalendar(
1512
+ this._templateSet.dayCell,
1513
+ this._getCalendarDays(monthDate),
1514
+ monthDate,
1515
+ currentState.selectedDate,
1516
+ (day) => { this.setDate(day); },
1517
+ this._config.locale,
1518
+ this._config.range ? currentState.selectedRange : undefined,
1519
+ this._config.multiDate ? currentState.selectedDates : undefined
1520
+ );
1521
+
1522
+ // Create panel element and append header + calendar directly to preserve event listeners
1523
+ // Instead of converting to HTML strings which lose event listeners
1524
+ const panel = document.createElement('div');
1525
+ panel.setAttribute('data-kt-datepicker-panel', '');
1526
+
1527
+ // Apply default panel styling (flex flex-col gap-1.5 from CSS)
1528
+ panel.className = 'flex flex-col gap-1.5';
1529
+
1530
+ // Append header and calendar directly (preserves event listeners)
1531
+ panel.appendChild(header);
1532
+ panel.appendChild(calendar);
1533
+
1534
+ return panel;
1535
+ }
1536
+
1537
+ /**
1538
+ * Render multi-month calendar
1539
+ */
1540
+ private _renderMultiMonth(dropdownEl: HTMLElement, visibleMonths: number) {
1541
+ const currentState = this._unifiedStateManager.getState();
1542
+ const baseDate = new Date(currentState.currentDate);
1543
+
1544
+ // Get all month dates for multi-month display
1545
+ const monthDates = this._getMultiMonthDates(baseDate, visibleMonths);
1546
+
1547
+ // Create multi-month container element
1548
+ const multiMonthContainer = document.createElement('div');
1549
+ multiMonthContainer.setAttribute('data-kt-datepicker-multimonth-container', '');
1550
+
1551
+ // Apply classes: flex flex-col md:flex-row gap-4
1552
+ multiMonthContainer.className = 'flex flex-col md:flex-row gap-4';
1553
+ if (this._config.classes?.multiMonthContainer) {
1554
+ multiMonthContainer.className += ' ' + this._config.classes.multiMonthContainer;
1555
+ }
1556
+
1557
+ // Render each month panel and append directly (preserves event listeners)
1558
+ monthDates.forEach((monthDate, index) => {
1559
+ const panel = this._renderMultiMonthCalendar(monthDate, index, visibleMonths);
1560
+ multiMonthContainer.appendChild(panel);
1561
+ });
1562
+
1563
+ dropdownEl.appendChild(multiMonthContainer);
1564
+ }
1565
+
1566
+ /**
1567
+ * Render the datepicker UI using templates
1568
+ */
1569
+ private _render() {
1570
+ // Performance marker: Start render
1571
+ if (typeof performance !== 'undefined' && this._config.debug) {
1572
+ performance.mark('datepicker-render-start');
1573
+ }
1574
+
1575
+ // Store current state before rendering
1576
+ const wasOpen = this._unifiedStateManager.isDropdownOpen();
1577
+ const selectedDate = this._unifiedStateManager.getState().selectedDate;
1578
+ const selectedRange = this._unifiedStateManager.getState().selectedRange;
1579
+ const selectedDates = this._unifiedStateManager.getState().selectedDates;
1580
+
1581
+ // Remove any previous container
1582
+ if (this._container && this._container.parentNode) {
1583
+ this._container.parentNode.removeChild(this._container);
1584
+ }
1585
+ // Render main container from template
1586
+ const containerEl = this._renderContainer();
1587
+ // Remove any previous input wrapper
1588
+ const existingWrapper = this._element.querySelector('[data-kt-datepicker-input-wrapper]');
1589
+ if (existingWrapper && existingWrapper.parentNode) {
1590
+ existingWrapper.parentNode.removeChild(existingWrapper);
1591
+ }
1592
+ // Render calendar button from template
1593
+ const calendarButtonTpl = this._templateSet.calendarButton || defaultTemplates.calendarButton;
1594
+ let calendarButtonHtml: string;
1595
+ if (typeof calendarButtonTpl === 'function') {
1596
+ const classData = mergeClassData('calendarButton', { ariaLabel: this._config.calendarButtonAriaLabel || 'Open calendar' }, this._config.classes);
1597
+ calendarButtonHtml = calendarButtonTpl(classData);
1598
+ } else {
1599
+ const classData = mergeClassData('calendarButton', { ariaLabel: this._config.calendarButtonAriaLabel || 'Open calendar' }, this._config.classes);
1600
+ calendarButtonHtml = renderTemplateString(calendarButtonTpl, classData);
1601
+ }
1602
+ // Render input wrapper and calendar button from template
1603
+ const inputWrapperEl = this._renderInputWrapper(calendarButtonHtml);
1604
+ // Attach calendar button event listener
1605
+ this._bindCalendarButtonEvent(inputWrapperEl);
1606
+ // Insert wrapper at the start of the element
1607
+ this._element.insertBefore(inputWrapperEl, this._element.firstChild);
1608
+ // --- Dropdown rendering and attachment ---
1609
+ // Remove any previous dropdown
1610
+ const existingDropdown = this._element.querySelector('[data-kt-datepicker-dropdown]');
1611
+ if (existingDropdown && existingDropdown.parentNode) {
1612
+ existingDropdown.parentNode.removeChild(existingDropdown);
1613
+ }
1614
+ // Render dropdown from template system (never inline)
1615
+ const dropdownEl = this._renderDropdown();
1616
+ this._renderDropdownContent(dropdownEl);
1617
+ this._attachDropdown(inputWrapperEl, dropdownEl);
1618
+
1619
+ // Restore state
1620
+ this._unifiedStateManager.updateState({
1621
+ selectedDate,
1622
+ selectedRange,
1623
+ selectedDates
1624
+ }, 'render-restore');
1625
+
1626
+ // Restore open state
1627
+ if (wasOpen) {
1628
+ this._unifiedStateManager.setDropdownOpen(true, 'render-restore');
1629
+ // The dropdown module will automatically open via observer pattern
1630
+ }
1631
+
1632
+
1633
+ this._updatePlaceholder();
1634
+ this._updateDisabledState();
1635
+
1636
+ // Enforce min/max dates after rendering
1637
+ // Use requestAnimationFrame to align with browser render cycle
1638
+ requestAnimationFrame(() => {
1639
+ this._enforceMinMaxDates();
1640
+ });
1641
+
1642
+ // Attach keyboard event listeners
1643
+ if (this._input) {
1644
+ this._eventManager.removeListener(this._input, 'keydown', this._onKeyDown);
1645
+ this._eventManager.addListener(this._input, 'keydown', this._onKeyDown);
1646
+ }
1647
+ if (dropdownEl) {
1648
+ this._eventManager.removeListener(dropdownEl, 'keydown', this._onKeyDown);
1649
+ this._eventManager.addListener(dropdownEl, 'keydown', this._onKeyDown);
1650
+ }
1651
+ // Ensure live region exists
1652
+ let liveRegion = this._element.querySelector('[data-kt-datepicker-live]');
1653
+ if (!liveRegion) {
1654
+ liveRegion = this._templateRenderer.renderTemplateToElement({
1655
+ templateKey: 'liveRegion',
1656
+ data: {},
1657
+ configClasses: this._config.classes
1658
+ });
1659
+ this._element.appendChild(liveRegion);
1660
+ }
1661
+
1662
+ // Initialize DOM element cache after rendering
1663
+ this._initializeElementCache();
1664
+
1665
+ // Performance marker: End render
1666
+ if (typeof performance !== 'undefined' && this._config.debug) {
1667
+ performance.mark('datepicker-render-end');
1668
+ performance.measure('datepicker-render', 'datepicker-render-start', 'datepicker-render-end');
1669
+ }
1670
+ }
1671
+
1672
+ /**
1673
+ * Attach the dropdown after the input wrapper
1674
+ */
1675
+ private _attachDropdown(inputWrapperEl: HTMLElement, dropdownEl: HTMLElement) {
1676
+ // Clean up existing dropdown module
1677
+ if (this._dropdownModule) {
1678
+ this._dropdownModule.dispose();
1679
+ }
1680
+
1681
+ // Always attach dropdown element to DOM first
1682
+ if (inputWrapperEl.nextSibling) {
1683
+ this._element.insertBefore(dropdownEl, inputWrapperEl.nextSibling);
1684
+ } else {
1685
+ this._element.appendChild(dropdownEl);
1686
+ }
1687
+
1688
+ // Find the toggle element (calendar button)
1689
+ const toggleElement = this._element.querySelector('button[data-kt-datepicker-calendar-btn]') as HTMLElement;
1690
+
1691
+ if (toggleElement) {
1692
+ // Create new dropdown module
1693
+ this._dropdownModule = new KTDatepickerDropdown(
1694
+ this._element,
1695
+ toggleElement,
1696
+ dropdownEl,
1697
+ this._config
1698
+ );
1699
+
1700
+ // Connect dropdown module to unified state manager
1701
+ if (this._dropdownModule) {
1702
+ this._dropdownModule.setUnifiedStateManager(this._unifiedStateManager);
1703
+ this._unifiedStateManager.subscribe(this._dropdownModule);
1704
+ }
1705
+ }
1706
+ }
1707
+
1708
+ private _getCalendarDays(date: Date): Date[] {
1709
+ const year = date.getFullYear();
1710
+ const month = date.getMonth();
1711
+ const firstDay = new Date(year, month, 1);
1712
+ const lastDay = new Date(year, month + 1, 0);
1713
+ const days: Date[] = [];
1714
+ // Start from Sunday of the first week
1715
+ const start = new Date(firstDay);
1716
+ start.setDate(firstDay.getDate() - firstDay.getDay());
1717
+ // End at Saturday of the last week
1718
+ const end = new Date(lastDay);
1719
+ end.setDate(lastDay.getDate() + (6 - lastDay.getDay()));
1720
+ // Reuse date object by incrementing instead of creating new ones
1721
+ const currentDate = new Date(start);
1722
+ while (currentDate <= end) {
1723
+ days.push(new Date(currentDate)); // Create new Date instance for each day to avoid mutation issues
1724
+ currentDate.setDate(currentDate.getDate() + 1);
1725
+ }
1726
+ return days;
1727
+ }
1728
+
1729
+
1730
+ private _changeMonth(offset: number) {
1731
+ const currentState = this._unifiedStateManager.getState();
1732
+ const d = new Date(currentState.currentDate);
1733
+ d.setMonth(d.getMonth() + offset);
1734
+
1735
+ // Update the unified state manager and wait for state to propagate
1736
+ this._unifiedStateManager.updateState({
1737
+ currentDate: d
1738
+ }, 'month-navigation');
1739
+
1740
+ // Ensure state is fully updated before proceeding
1741
+ setTimeout(() => {
1742
+ // Force a state refresh to ensure we have the latest state
1743
+ const updatedState = this._unifiedStateManager.getState();
1744
+
1745
+ // Check if the state actually changed
1746
+ if (updatedState.currentDate.getMonth() === d.getMonth()) {
1747
+ // Use multi-month update if multiple months are visible
1748
+ if (this._config.visibleMonths && this._config.visibleMonths > 1) {
1749
+ this._updateMultiMonthCalendarContent();
1750
+ } else {
1751
+ this._updateCalendarContent();
1752
+ }
1753
+ } else {
1754
+ // Wait a bit more for state to propagate
1755
+ setTimeout(() => {
1756
+ if (this._config.visibleMonths && this._config.visibleMonths > 1) {
1757
+ this._updateMultiMonthCalendarContent();
1758
+ } else {
1759
+ this._updateCalendarContent();
1760
+ }
1761
+ }, 50);
1762
+ }
1763
+ }, 10);
1764
+ }
676
1765
 
677
1766
  /**
678
- * Update the display element for a single date
679
- *
680
- * @param date - The date to display
681
- */
682
- private _updateDisplayElement(date: Date | null): void {
683
- if (!this._displayElement) return;
684
-
685
- if (!date) {
686
- // If no date, show placeholder
687
- const placeholder =
688
- this._dateInputElement?.getAttribute('placeholder') || 'Select date';
689
- this._displayElement.innerHTML = placeholderTemplate(placeholder);
690
- return;
691
- }
692
-
693
- if (this._useSegmentedDisplay) {
694
- // Update segmented display
695
- const day = date.getDate();
696
- const month = date.getMonth() + 1;
697
- const year = date.getFullYear();
698
-
699
- const daySegment = this._displayElement.querySelector(
700
- '[data-segment="day"]',
701
- );
702
- const monthSegment = this._displayElement.querySelector(
703
- '[data-segment="month"]',
704
- );
705
- const yearSegment = this._displayElement.querySelector(
706
- '[data-segment="year"]',
707
- );
708
-
709
- if (daySegment) daySegment.textContent = day < 10 ? `0${day}` : `${day}`;
710
- if (monthSegment)
711
- monthSegment.textContent = month < 10 ? `0${month}` : `${month}`;
712
- if (yearSegment) yearSegment.textContent = `${year}`;
713
- } else {
714
- // Simple display
715
- this._displayElement.textContent = formatDate(
716
- date,
717
- this._config.format,
718
- this._config,
719
- );
720
- }
721
- }
722
-
723
- /**
724
- * Update the display element for a date range
725
- *
726
- * @param startDate - The start date of the range
727
- * @param endDate - The end date of the range
728
- */
729
- private _updateRangeDisplayElement(
730
- startDate: Date | null,
731
- endDate: Date | null,
732
- ): void {
733
- if (!this._displayElement) return;
734
-
735
- if (!startDate) {
736
- // If no date, show placeholder
737
- const placeholder =
738
- this._dateInputElement?.getAttribute('placeholder') ||
739
- 'Select date range';
740
- this._displayElement.innerHTML = placeholderTemplate(placeholder);
741
- return;
742
- }
743
-
744
- if (this._useSegmentedDisplay) {
745
- // Update segmented range display
746
- // Start date segments
747
- const startDay = this._displayElement.querySelector(
748
- '[data-segment="start-day"]',
749
- );
750
- const startMonth = this._displayElement.querySelector(
751
- '[data-segment="start-month"]',
752
- );
753
- const startYear = this._displayElement.querySelector(
754
- '[data-segment="start-year"]',
755
- );
756
-
757
- if (startDay)
758
- startDay.textContent =
759
- startDate.getDate() < 10
760
- ? `0${startDate.getDate()}`
761
- : `${startDate.getDate()}`;
762
- if (startMonth)
763
- startMonth.textContent =
764
- startDate.getMonth() + 1 < 10
765
- ? `0${startDate.getMonth() + 1}`
766
- : `${startDate.getMonth() + 1}`;
767
- if (startYear) startYear.textContent = `${startDate.getFullYear()}`;
768
-
769
- // End date segments
770
- if (endDate) {
771
- const endDay = this._displayElement.querySelector(
772
- '[data-segment="end-day"]',
773
- );
774
- const endMonth = this._displayElement.querySelector(
775
- '[data-segment="end-month"]',
776
- );
777
- const endYear = this._displayElement.querySelector(
778
- '[data-segment="end-year"]',
779
- );
780
-
781
- if (endDay)
782
- endDay.textContent =
783
- endDate.getDate() < 10
784
- ? `0${endDate.getDate()}`
785
- : `${endDate.getDate()}`;
786
- if (endMonth)
787
- endMonth.textContent =
788
- endDate.getMonth() + 1 < 10
789
- ? `0${endDate.getMonth() + 1}`
790
- : `${endDate.getMonth() + 1}`;
791
- if (endYear) endYear.textContent = `${endDate.getFullYear()}`;
792
- }
793
- } else {
794
- // Simple display
795
- let displayText = formatDate(
796
- startDate,
797
- this._config.format,
798
- this._config,
799
- );
800
-
801
- if (endDate) {
802
- const endFormatted = formatDate(
803
- endDate,
804
- this._config.format,
805
- this._config,
806
- );
807
- displayText += `${this._config.rangeSeparator}${endFormatted}`;
808
- }
809
-
810
- this._displayElement.textContent = displayText;
811
- }
812
- }
813
-
814
- /**
815
- * Handle input change events
816
- *
817
- * @param e - Input change event
818
- */
819
- private _handleInputChange(e: Event): void {
820
- const input = e.target as HTMLInputElement;
821
- const inputValue = input.value.trim();
822
-
823
- if (!inputValue) {
824
- // Clear selection if input is empty
825
- this._state.setSelectedDate(null);
826
- return;
827
- }
828
-
829
- if (this._config.range) {
830
- // Handle range input
831
- const rangeParts = inputValue.split(this._config.rangeSeparator);
832
-
833
- if (rangeParts.length === 2) {
834
- const startDate = parseDate(
835
- rangeParts[0].trim(),
836
- this._config.format,
837
- this._config,
838
- );
839
- const endDate = parseDate(
840
- rangeParts[1].trim(),
841
- this._config.format,
842
- this._config,
843
- );
844
-
845
- // Validate dates are within min/max constraints
846
- if (startDate && isDateDisabled(startDate, this._config)) {
847
- console.log(
848
- 'Start date from input is outside allowed range:',
849
- startDate.toISOString(),
850
- );
851
- return;
852
- }
853
-
854
- if (endDate && isDateDisabled(endDate, this._config)) {
855
- console.log(
856
- 'End date from input is outside allowed range:',
857
- endDate.toISOString(),
858
- );
859
- return;
860
- }
861
-
862
- if (startDate && endDate) {
863
- this.setDateRange(startDate, endDate);
864
- }
865
- } else if (rangeParts.length === 1) {
866
- const singleDate = parseDate(
867
- rangeParts[0].trim(),
868
- this._config.format,
869
- this._config,
870
- );
871
-
872
- // Validate date is within min/max constraints
873
- if (singleDate && isDateDisabled(singleDate, this._config)) {
874
- console.log(
875
- 'Date from input is outside allowed range:',
876
- singleDate.toISOString(),
877
- );
878
- return;
879
- }
880
-
881
- if (singleDate) {
882
- this.setDateRange(singleDate, null);
883
- }
884
- }
885
- } else {
886
- // Handle single date input
887
- const parsedDate = parseDate(
888
- inputValue,
889
- this._config.format,
890
- this._config,
891
- );
892
-
893
- // Validate date is within min/max constraints
894
- if (parsedDate && isDateDisabled(parsedDate, this._config)) {
895
- console.log(
896
- 'Date from input is outside allowed range:',
897
- parsedDate.toISOString(),
898
- );
899
- return;
900
- }
901
-
902
- if (parsedDate) {
903
- this.setDate(parsedDate);
904
- }
905
- }
906
- }
907
-
908
- /**
909
- * Initialize with default values from input
910
- */
911
- private _initializeDefaultValues(): void {
912
- // Set min and max dates from attributes if they exist
913
- const minDateAttr = this._element.getAttribute(
914
- 'data-kt-datepicker-min-date',
915
- );
916
- const maxDateAttr = this._element.getAttribute(
917
- 'data-kt-datepicker-max-date',
918
- );
919
-
920
- if (minDateAttr) {
921
- const minDate = parseDate(minDateAttr, this._config.format, this._config);
922
- if (minDate) {
923
- this.setMinDate(minDate);
924
- }
925
- }
926
-
927
- if (maxDateAttr) {
928
- const maxDate = parseDate(maxDateAttr, this._config.format, this._config);
929
- if (maxDate) {
930
- this.setMaxDate(maxDate);
931
- }
932
- }
933
-
934
- // Check for default value in main input
935
- if (this._dateInputElement && this._dateInputElement.value) {
936
- this._handleInputChange({
937
- target: this._dateInputElement,
938
- } as unknown as Event);
939
- }
940
- // Check for default values in range inputs
941
- else if (
942
- this._config.range &&
943
- this._startDateInputElement &&
944
- this._startDateInputElement.value
945
- ) {
946
- const startDate = parseDate(
947
- this._startDateInputElement.value,
948
- this._config.format,
949
- this._config,
950
- );
951
- let endDate = null;
952
-
953
- if (this._endDateInputElement && this._endDateInputElement.value) {
954
- endDate = parseDate(
955
- this._endDateInputElement.value,
956
- this._config.format,
957
- this._config,
958
- );
959
- }
960
-
961
- if (startDate) {
962
- this.setDateRange(startDate, endDate);
963
- }
964
- }
965
- }
966
-
967
- /**
968
- * ========================================================================
969
- * Public API
970
- * ========================================================================
971
- */
972
-
973
- /**
974
- * Get the currently selected date
975
- *
976
- * @returns Selected date, null if no selection, or date range object
977
- */
978
- public getDate(): Date | null | DateRangeInterface {
979
- const state = this._state.getState();
980
- const config = this._state.getConfig();
981
-
982
- if (config.range) {
983
- return state.selectedDateRange || { startDate: null, endDate: null };
984
- } else {
985
- return state.selectedDate;
986
- }
987
- }
988
-
989
- /**
990
- * Set the selected date
991
- *
992
- * @param date - Date to select or null to clear selection
993
- */
994
- public setDate(date: Date | null): void {
995
- // Skip if the date is disabled (outside min/max range)
996
- if (date && isDateDisabled(date, this._config)) {
997
- console.log(
998
- 'Date is disabled in setDate, ignoring selection:',
999
- date.toISOString(),
1000
- );
1001
- return;
1002
- }
1003
-
1004
- this._state.setSelectedDate(date);
1005
-
1006
- if (date) {
1007
- this._state.setCurrentDate(date);
1008
- }
1009
-
1010
- // Update the display
1011
- this._updateDisplayElement(date);
1012
-
1013
- // Update hidden input
1014
- if (this._dateInputElement && date) {
1015
- this._dateInputElement.value = formatDate(
1016
- date,
1017
- this._config.format,
1018
- this._config,
1019
- );
1020
- this._dateInputElement.dispatchEvent(
1021
- new Event('change', { bubbles: true }),
1022
- );
1023
- } else if (this._dateInputElement) {
1024
- this._dateInputElement.value = '';
1025
- this._dateInputElement.dispatchEvent(
1026
- new Event('change', { bubbles: true }),
1027
- );
1028
- }
1029
- }
1030
-
1031
- /**
1032
- * Get the currently selected date range
1033
- *
1034
- * @returns Selected date range or null if no selection
1035
- */
1036
- public getDateRange(): DateRangeInterface | null {
1037
- const state = this._state.getState();
1038
- return state.selectedDateRange;
1039
- }
1040
-
1041
- /**
1042
- * Set the selected date range
1043
- *
1044
- * @param start - Start date of the range
1045
- * @param end - End date of the range
1046
- */
1047
- public setDateRange(start: Date | null, end: Date | null): void {
1048
- const state = this._state.getState();
1049
-
1050
- // Ensure we're in range mode
1051
- if (!this._config.range) {
1052
- console.warn('Cannot set date range when range mode is disabled');
1053
- return;
1054
- }
1055
-
1056
- // Validate start and end dates are within min/max range
1057
- if (start && isDateDisabled(start, this._config)) {
1058
- console.log(
1059
- 'Start date is disabled in setDateRange, ignoring selection:',
1060
- start.toISOString(),
1061
- );
1062
- return;
1063
- }
1064
-
1065
- if (end && isDateDisabled(end, this._config)) {
1066
- console.log(
1067
- 'End date is disabled in setDateRange, ignoring selection:',
1068
- end.toISOString(),
1069
- );
1070
- return;
1071
- }
1072
-
1073
- // Reset range selection state
1074
- this._state.getState().isRangeSelectionStart = true;
1075
-
1076
- // Set start date
1077
- if (start) {
1078
- if (!state.selectedDateRange) {
1079
- state.selectedDateRange = { startDate: null, endDate: null };
1080
- }
1081
-
1082
- state.selectedDateRange.startDate = start;
1083
- this._state.setCurrentDate(start);
1084
-
1085
- // Set end date if provided
1086
- if (end) {
1087
- state.selectedDateRange.endDate = end;
1088
- } else {
1089
- state.selectedDateRange.endDate = null;
1090
- }
1091
-
1092
- // Update display element
1093
- this._updateRangeDisplayElement(start, end);
1094
-
1095
- // Update hidden inputs
1096
- if (this._dateInputElement) {
1097
- let inputValue = formatDate(start, this._config.format, this._config);
1098
- if (end) {
1099
- inputValue += `${this._config.rangeSeparator}${formatDate(
1100
- end,
1101
- this._config.format,
1102
- this._config,
1103
- )}`;
1104
- }
1105
- this._dateInputElement.value = inputValue;
1106
- this._dateInputElement.dispatchEvent(
1107
- new Event('change', { bubbles: true }),
1108
- );
1109
- }
1110
-
1111
- if (this._startDateInputElement) {
1112
- this._startDateInputElement.value = formatDate(
1113
- start,
1114
- this._config.format,
1115
- this._config,
1116
- );
1117
- this._startDateInputElement.dispatchEvent(
1118
- new Event('change', { bubbles: true }),
1119
- );
1120
- }
1121
-
1122
- if (this._endDateInputElement && end) {
1123
- this._endDateInputElement.value = formatDate(
1124
- end,
1125
- this._config.format,
1126
- this._config,
1127
- );
1128
- this._endDateInputElement.dispatchEvent(
1129
- new Event('change', { bubbles: true }),
1130
- );
1131
- } else if (this._endDateInputElement) {
1132
- this._endDateInputElement.value = '';
1133
- }
1134
-
1135
- // Dispatch change event
1136
- this._eventManager.dispatchEvent(KTDatepickerEventName.DATE_CHANGE, {
1137
- selectedDateRange: state.selectedDateRange,
1138
- });
1139
- } else {
1140
- // Clear selection
1141
- this._state.getState().selectedDateRange = null;
1142
-
1143
- // Clear display
1144
- if (this._displayElement) {
1145
- const placeholder =
1146
- this._dateInputElement?.getAttribute('placeholder') ||
1147
- 'Select date range';
1148
- this._displayElement.innerHTML = placeholderTemplate(placeholder);
1149
- }
1150
-
1151
- // Clear inputs
1152
- if (this._dateInputElement) {
1153
- this._dateInputElement.value = '';
1154
- this._dateInputElement.dispatchEvent(
1155
- new Event('change', { bubbles: true }),
1156
- );
1157
- }
1158
-
1159
- if (this._startDateInputElement) {
1160
- this._startDateInputElement.value = '';
1161
- this._startDateInputElement.dispatchEvent(
1162
- new Event('change', { bubbles: true }),
1163
- );
1164
- }
1165
-
1166
- if (this._endDateInputElement) {
1167
- this._endDateInputElement.value = '';
1168
- this._endDateInputElement.dispatchEvent(
1169
- new Event('change', { bubbles: true }),
1170
- );
1171
- }
1172
-
1173
- this._eventManager.dispatchEvent(KTDatepickerEventName.DATE_CHANGE, {
1174
- selectedDateRange: null,
1175
- });
1176
- }
1177
- }
1178
-
1179
- /**
1180
- * Set the minimum selectable date
1181
- *
1182
- * @param minDate - Minimum date or null to remove constraint
1183
- */
1184
- public setMinDate(minDate: Date | null): void {
1185
- this._config.minDate = minDate;
1186
-
1187
- // Refresh calendar view to apply new constraints
1188
- this._eventManager.dispatchEvent(KTDatepickerEventName.UPDATE);
1189
- }
1190
-
1191
- /**
1192
- * Set the maximum selectable date
1193
- *
1194
- * @param maxDate - Maximum date or null to remove constraint
1195
- */
1196
- public setMaxDate(maxDate: Date | null): void {
1197
- this._config.maxDate = maxDate;
1198
-
1199
- // Refresh calendar view to apply new constraints
1200
- this._eventManager.dispatchEvent(KTDatepickerEventName.UPDATE);
1201
- }
1202
-
1203
- /**
1204
- * Update the datepicker (refresh view)
1205
- */
1206
- public update(): void {
1207
- // Trigger calendar update through events
1208
- this._eventManager.dispatchEvent(KTDatepickerEventName.UPDATE);
1209
- }
1210
-
1211
- /**
1212
- * Destroy the datepicker instance and clean up
1213
- */
1214
- public destroy(): void {
1215
- // Remove event listeners
1216
- this._eventManager.removeEventListener(
1217
- KTDatepickerEventName.DATE_CHANGE,
1218
- this._handleDateChange.bind(this),
1219
- );
1220
-
1221
- if (this._dateInputElement) {
1222
- this._dateInputElement.removeEventListener(
1223
- 'change',
1224
- this._handleInputChange.bind(this),
1225
- );
1226
- }
1227
-
1228
- if (this._displayElement) {
1229
- this._displayElement.remove();
1230
- }
1231
-
1232
- // Remove instance from element
1233
- this._element.removeAttribute('data-kt-datepicker-initialized');
1234
- delete (this._element as any).instance;
1235
-
1236
- // Remove initialized class
1237
- this._element.classList.remove('relative');
1238
-
1239
- // Remove from instances map
1240
- KTDatepicker._instances.delete(this._element);
1241
- }
1242
-
1243
- /**
1244
- * Dispatch a custom event
1245
- *
1246
- * @param eventName - Name of the event
1247
- * @param payload - Optional event payload
1248
- */
1249
- protected _dispatchEvent(eventName: string, payload?: any): void {
1250
- this._eventManager.dispatchEvent(eventName, payload);
1251
- }
1252
-
1253
- /**
1254
- * ========================================================================
1255
- * Static instances
1256
- * ========================================================================
1257
- */
1258
-
1259
- private static readonly _instances = new Map<HTMLElement, KTDatepicker>();
1260
-
1261
- /**
1262
- * Create instances for all datepicker elements on the page
1263
- */
1264
- public static createInstances(): void {
1265
- const elements = document.querySelectorAll<HTMLElement>(
1266
- '[data-kt-datepicker]',
1267
- );
1268
-
1269
- elements.forEach((element) => {
1270
- if (
1271
- element.hasAttribute('data-kt-datepicker') &&
1272
- !element.getAttribute('data-kt-datepicker-initialized')
1273
- ) {
1274
- // Create instance
1275
- const instance = new KTDatepicker(element);
1276
- this._instances.set(element, instance);
1277
- }
1278
- });
1279
- }
1280
-
1281
- /**
1282
- * Initialize all datepickers on the page
1283
- */
1284
- public static init(): void {
1285
- KTDatepicker.createInstances();
1286
- }
1767
+ * Update only the calendar content without recreating the dropdown
1768
+ * This preserves the dropdown state while updating the month view
1769
+ */
1770
+ private _updateCalendarContent() {
1771
+ // Performance marker: Start incremental update
1772
+ if (typeof performance !== 'undefined' && this._config.debug) {
1773
+ performance.mark('datepicker-incremental-update-start');
1774
+ }
1775
+
1776
+ // Instance-scoped dropdown element selection strategy
1777
+ let dropdownEl: HTMLElement | null = null;
1778
+
1779
+ // First priority: find dropdown within current datepicker instance
1780
+ dropdownEl = this._element.querySelector('[data-kt-datepicker-dropdown]') as HTMLElement;
1781
+
1782
+ // Second priority: if instance ID is available, find by instance ID
1783
+ if (!dropdownEl && this._instanceId) {
1784
+ dropdownEl = document.querySelector(`[data-kt-datepicker-dropdown][data-kt-datepicker-instance-id="${this._instanceId}"]`) as HTMLElement;
1785
+ }
1786
+
1787
+ // Third priority: check dropdown module reference
1788
+ if (!dropdownEl && this._dropdownModule) {
1789
+ const dropdownModuleElement = (this._dropdownModule as any)._dropdownElement;
1790
+ if (dropdownModuleElement) {
1791
+ dropdownEl = dropdownModuleElement;
1792
+ }
1793
+ }
1794
+
1795
+ // Final fallback: global search with warnings (only if no other instances exist)
1796
+ if (!dropdownEl) {
1797
+ const allDropdowns = document.querySelectorAll('[data-kt-datepicker-dropdown]');
1798
+ if (allDropdowns.length === 1) {
1799
+ // Only one dropdown exists globally, safe to use
1800
+ dropdownEl = allDropdowns[0] as HTMLElement;
1801
+ } else if (allDropdowns.length > 1) {
1802
+ // Multiple dropdowns exist - this could cause cross-instance issues
1803
+ this._render();
1804
+ return;
1805
+ } else {
1806
+ this._render();
1807
+ return;
1808
+ }
1809
+ }
1810
+
1811
+ if (!dropdownEl) {
1812
+ // Fallback to full render if dropdown doesn't exist (should be rare)
1813
+ this._render();
1814
+ return;
1815
+ }
1816
+
1817
+ // Clear existing calendar content and update only the calendar
1818
+ const calendarEl = dropdownEl.querySelector('[data-kt-datepicker-calendar-table]');
1819
+ if (calendarEl) {
1820
+ // Remove the old calendar
1821
+ calendarEl.remove();
1822
+
1823
+ // Get current state - ensure we have the most up-to-date state
1824
+ const currentState = this._unifiedStateManager.getState();
1825
+
1826
+ // Update the month/year display in the header with multiple selector strategies
1827
+ let monthYearEl = dropdownEl.querySelector('[data-kt-datepicker-month-year]');
1828
+
1829
+ // Fallback selectors if primary selector fails
1830
+ if (!monthYearEl) {
1831
+ monthYearEl = dropdownEl.querySelector('[data-kt-datepicker-month]');
1832
+ }
1833
+
1834
+ // Additional fallback: look for span containing month and year
1835
+ if (!monthYearEl) {
1836
+ const spans = dropdownEl.querySelectorAll('span');
1837
+ monthYearEl = Array.from(spans).find(span =>
1838
+ span.textContent && span.textContent.match(/[A-Za-z]+ \d{4}/)
1839
+ ) as HTMLElement;
1840
+ }
1841
+
1842
+ // Final fallback: any span in header
1843
+ if (!monthYearEl) {
1844
+ const headerEl = dropdownEl.querySelector('[data-kt-datepicker-header]');
1845
+ if (headerEl) {
1846
+ monthYearEl = headerEl.querySelector('span') as HTMLElement;
1847
+ }
1848
+ }
1849
+
1850
+ if (monthYearEl) {
1851
+ const newMonthYear = `${currentState.currentDate.toLocaleString(this._config.locale, { month: 'long' })} ${currentState.currentDate.getFullYear()}`;
1852
+ monthYearEl.textContent = newMonthYear;
1853
+ }
1854
+
1855
+ // Render only the new calendar
1856
+ const dayClickHandler = (day: Date) => {
1857
+ this.setDate(day);
1858
+ };
1859
+
1860
+ const newCalendar = renderCalendar(
1861
+ this._templateSet.dayCell,
1862
+ this._getCalendarDays(currentState.currentDate),
1863
+ currentState.currentDate,
1864
+ currentState.selectedDate,
1865
+ dayClickHandler,
1866
+ this._config.locale,
1867
+ this._config.range ? currentState.selectedRange : undefined,
1868
+ this._config.multiDate ? currentState.selectedDates : undefined
1869
+ );
1870
+
1871
+ // Insert the new calendar after the header
1872
+ const headerEl = dropdownEl.querySelector('[data-kt-datepicker-header]');
1873
+ if (headerEl) {
1874
+ headerEl.insertAdjacentElement('afterend', newCalendar);
1875
+ } else {
1876
+ // Fallback: append to dropdown
1877
+ dropdownEl.appendChild(newCalendar);
1878
+ }
1879
+
1880
+ // Refresh cache with the new calendar element
1881
+ this._cachedElements.calendarElement = newCalendar as HTMLElement;
1882
+
1883
+ // Enforce min/max dates after calendar update
1884
+ this._enforceMinMaxDates();
1885
+
1886
+ // Performance marker: End incremental update
1887
+ if (typeof performance !== 'undefined' && this._config.debug) {
1888
+ performance.mark('datepicker-incremental-update-end');
1889
+ performance.measure('datepicker-incremental-update', 'datepicker-incremental-update-start', 'datepicker-incremental-update-end');
1890
+ }
1891
+ } else {
1892
+ // If calendar element not found, fallback to full render
1893
+ this._render();
1894
+ }
1895
+ }
1896
+
1897
+ /**
1898
+ * Update multi-month calendar content without recreating the dropdown
1899
+ * This preserves the dropdown state while updating all visible months
1900
+ */
1901
+ private _updateMultiMonthCalendarContent() {
1902
+ // Instance-scoped dropdown element selection strategy
1903
+ let dropdownEl: HTMLElement | null = null;
1904
+
1905
+ // First priority: find dropdown within current datepicker instance
1906
+ dropdownEl = this._element.querySelector('[data-kt-datepicker-dropdown]') as HTMLElement;
1907
+
1908
+ // Second priority: if instance ID is available, find by instance ID
1909
+ if (!dropdownEl && this._instanceId) {
1910
+ dropdownEl = document.querySelector(`[data-kt-datepicker-dropdown][data-kt-datepicker-instance-id="${this._instanceId}"]`) as HTMLElement;
1911
+ }
1912
+
1913
+ // Third priority: check dropdown module reference
1914
+ if (!dropdownEl && this._dropdownModule) {
1915
+ const dropdownModuleElement = (this._dropdownModule as any)._dropdownElement;
1916
+ if (dropdownModuleElement) {
1917
+ dropdownEl = dropdownModuleElement;
1918
+ }
1919
+ }
1920
+
1921
+ // Final fallback: global search with warnings (only if no other instances exist)
1922
+ if (!dropdownEl) {
1923
+ const allDropdowns = document.querySelectorAll('[data-kt-datepicker-dropdown]');
1924
+ if (allDropdowns.length === 1) {
1925
+ // Only one dropdown exists globally, safe to use
1926
+ dropdownEl = allDropdowns[0] as HTMLElement;
1927
+ } else if (allDropdowns.length > 1) {
1928
+ // Multiple dropdowns exist - this could cause cross-instance issues
1929
+ this._render();
1930
+ return;
1931
+ } else {
1932
+ this._render();
1933
+ return;
1934
+ }
1935
+ }
1936
+
1937
+ if (!dropdownEl) {
1938
+ // Fallback to full render if dropdown doesn't exist (should be rare)
1939
+ this._render();
1940
+ return;
1941
+ }
1942
+
1943
+ // Get current state - ensure we have the most up-to-date state
1944
+ const currentState = this._unifiedStateManager.getState();
1945
+ const visibleMonths = this._config.visibleMonths || 1;
1946
+
1947
+ // Find the multi-month container
1948
+ const multiMonthContainer = dropdownEl.querySelector('[data-kt-datepicker-multimonth-container]');
1949
+ if (!multiMonthContainer) {
1950
+ // Multi-month container not found, fallback to full render
1951
+ this._render();
1952
+ return;
1953
+ }
1954
+
1955
+ // Clear existing multi-month content
1956
+ multiMonthContainer.innerHTML = '';
1957
+
1958
+ // Get all month dates for multi-month display
1959
+ const monthDates = this._getMultiMonthDates(currentState.currentDate, visibleMonths);
1960
+
1961
+ // Re-render each month using the helper method
1962
+ monthDates.forEach((monthDate, index) => {
1963
+ const panel = this._renderMultiMonthCalendar(monthDate, index, visibleMonths);
1964
+ multiMonthContainer.appendChild(panel);
1965
+ });
1966
+
1967
+ // Enforce min/max dates after multi-month calendar update
1968
+ this._enforceMinMaxDates();
1969
+
1970
+ console.log('[KTDatepicker] Multi-month calendar content updated successfully');
1971
+ }
1972
+
1973
+ /**
1974
+ * Set the selected date (single, range, or multi-date)
1975
+ * @param date - The date to select
1976
+ */
1977
+ public setDate(date: Date) {
1978
+ console.log('🗓️ [KTDatepicker] setDate called with:', date);
1979
+ // Prevent selection if date is outside min/max
1980
+ if (this._config.minDate && date < new Date(this._config.minDate)) {
1981
+ console.log('🗓️ [KTDatepicker] setDate blocked: date is before minDate');
1982
+ return;
1983
+ }
1984
+ if (this._config.maxDate && date > new Date(this._config.maxDate)) {
1985
+ console.log('🗓️ [KTDatepicker] setDate blocked: date is after maxDate');
1986
+ return;
1987
+ }
1988
+ // Validate time constraints if time is enabled
1989
+ if (this._config.enableTime) {
1990
+ const timeState = dateToTimeState(date);
1991
+ const validation = validateTime(timeState, this._config.minTime, this._config.maxTime);
1992
+ if (!validation.isValid) {
1993
+ console.log('🗓️ [KTDatepicker] setDate blocked: time validation failed:', validation.error);
1994
+ return;
1995
+ }
1996
+ }
1997
+ if (this._config.multiDate) {
1998
+ this._selectMultiDate(date);
1999
+ return;
2000
+ }
2001
+ if (this._config.range) {
2002
+ this._selectRangeDate(date);
2003
+ return;
2004
+ }
2005
+ this._selectSingleDate(date);
2006
+ }
2007
+
2008
+
2009
+ /**
2010
+ * Get the selected date
2011
+ */
2012
+ public getDate(): Date | null {
2013
+ return this._unifiedStateManager.getState().selectedDate;
2014
+ }
2015
+
2016
+ /**
2017
+ * Update the datepicker (re-render)
2018
+ */
2019
+ public update() {
2020
+ this._render();
2021
+ }
2022
+
2023
+ /**
2024
+ * Clean up event listeners and DOM on destroy
2025
+ */
2026
+ public destroy() {
2027
+ console.log('🗓️ [KTDatepicker] destroy() called');
2028
+
2029
+ if (this._container && this._container.parentNode) {
2030
+ this._container.parentNode.removeChild(this._container);
2031
+ }
2032
+
2033
+ // Clean up event manager
2034
+ this._eventManager.removeListener(
2035
+ document as unknown as HTMLElement,
2036
+ 'click',
2037
+ this._handleDocumentClick
2038
+ );
2039
+ if (this._input) {
2040
+ this._eventManager.removeAllListeners(this._input);
2041
+ }
2042
+
2043
+ // Clean up focus manager
2044
+ if (this._focusManager) {
2045
+ this._focusManager.dispose();
2046
+ }
2047
+
2048
+ // Clean up dropdown module
2049
+ if (this._dropdownModule) {
2050
+ this._dropdownModule.dispose();
2051
+ }
2052
+
2053
+
2054
+
2055
+
2056
+ // Clean up unified state manager
2057
+ if (this._unsubscribeFromState) {
2058
+ this._unsubscribeFromState();
2059
+ this._unsubscribeFromState = null;
2060
+ }
2061
+ if (this._unifiedStateManager) {
2062
+ this._unifiedStateManager.dispose();
2063
+ }
2064
+
2065
+ // Clean up element observer
2066
+ if (this._elementObserver) {
2067
+ this._elementObserver.disconnect();
2068
+ this._elementObserver = null;
2069
+ }
2070
+
2071
+ // Clean up time picker renderer
2072
+ if (this._timePickerRenderer) {
2073
+ this._timePickerRenderer.cleanup();
2074
+ this._timePickerRenderer = null;
2075
+ }
2076
+
2077
+ // Clear DOM element cache
2078
+ this._cachedElements = {
2079
+ calendarElement: null,
2080
+ timePickerElement: null,
2081
+ startContainer: null,
2082
+ endContainer: null,
2083
+ yearElement: null,
2084
+ monthElement: null,
2085
+ dayElement: null,
2086
+ hourElement: null,
2087
+ minuteElement: null,
2088
+ secondElement: null,
2089
+ ampmElement: null,
2090
+ monthYearElement: null,
2091
+ timeDisplay: null
2092
+ };
2093
+
2094
+ (this._element as any).instance = null;
2095
+ console.log('🗓️ [KTDatepicker] destroy() completed');
2096
+ }
2097
+
2098
+ /**
2099
+ * Start observing for dynamic element creation
2100
+ */
2101
+ private _startElementObservation(): void {
2102
+ if (this._elementObserver) {
2103
+ this._elementObserver.disconnect();
2104
+ }
2105
+
2106
+ this._elementObserver = new MutationObserver((mutations) => {
2107
+ mutations.forEach((mutation) => {
2108
+ if (mutation.type === 'childList') {
2109
+ mutation.addedNodes.forEach((node) => {
2110
+ if (node.nodeType === Node.ELEMENT_NODE) {
2111
+ const element = node as Element;
2112
+ // Segmented input elements are now handled by direct UI updates
2113
+ }
2114
+ });
2115
+ }
2116
+ });
2117
+ });
2118
+
2119
+ this._elementObserver.observe(this._element, {
2120
+ childList: true,
2121
+ subtree: true
2122
+ });
2123
+ }
2124
+
2125
+
2126
+ /**
2127
+ * Update the input placeholder if no date is selected
2128
+ */
2129
+ private _updatePlaceholder() {
2130
+ const currentState = this._unifiedStateManager.getState();
2131
+ if (this._input && !currentState.selectedDate && this._config.placeholder) {
2132
+ this._input.setAttribute('placeholder', this._config.placeholder);
2133
+ console.log('🗓️ [KTDatepicker] _render: Placeholder set to:', this._config.placeholder);
2134
+ }
2135
+ }
2136
+
2137
+ /**
2138
+ * Update the disabled state of the input and calendar button
2139
+ *
2140
+ * Accessibility rationale:
2141
+ * - When disabling the calendar button, set:
2142
+ * - disabled attribute (removes from tab order, blocks interaction)
2143
+ * - aria-disabled="true" (announces as disabled to screen readers)
2144
+ * - tabindex="-1" (removes from tab order for extra safety)
2145
+ * - When enabling, remove these attributes.
2146
+ * This matches accessibility best practices and ensures the button is properly announced and not focusable when disabled.
2147
+ */
2148
+ private _updateDisabledState() {
2149
+ const calendarButton = this._element.querySelector('button[data-kt-datepicker-calendar-btn]');
2150
+ if (this._input && this._config.disabled) {
2151
+ this._input.setAttribute('disabled', 'true');
2152
+ if (calendarButton) {
2153
+ // Set disabled attribute
2154
+ calendarButton.setAttribute('disabled', 'true');
2155
+ // Set aria-disabled for screen readers
2156
+ calendarButton.setAttribute('aria-disabled', 'true');
2157
+ // Remove from tab order
2158
+ calendarButton.setAttribute('tabindex', '-1');
2159
+ }
2160
+ console.log('🗓️ [KTDatepicker] _render: Input and calendar button disabled');
2161
+ } else if (this._input) {
2162
+ this._input.removeAttribute('disabled');
2163
+ if (calendarButton) {
2164
+ // Remove disabled attribute
2165
+ calendarButton.removeAttribute('disabled');
2166
+ // Remove aria-disabled attribute
2167
+ calendarButton.removeAttribute('aria-disabled');
2168
+ // Restore tab order (remove tabindex)
2169
+ calendarButton.removeAttribute('tabindex');
2170
+ }
2171
+ }
2172
+ }
2173
+
2174
+ /**
2175
+ * Update the segmented input UI to reflect current state
2176
+ */
2177
+ private _updateSegmentedInput(state: KTDatepickerState): void {
2178
+ const segmentedContainer = this._element.querySelector('[data-kt-datepicker-segmented-input]');
2179
+ if (!segmentedContainer) return;
2180
+
2181
+ // Re-instantiate the segmented input with the current state
2182
+ // This will update the display without recreating the entire UI
2183
+ instantiateSingleSegmentedInput(
2184
+ segmentedContainer as HTMLElement,
2185
+ state,
2186
+ this._config,
2187
+ (date) => this._handleSegmentedInputChange(date)
2188
+ );
2189
+ }
2190
+
2191
+ /**
2192
+ * Update the range segmented inputs UI to reflect current state
2193
+ */
2194
+ private _updateRangeSegmentedInput(state: KTDatepickerState): void {
2195
+ const startContainer = this._element.querySelector('[data-kt-datepicker-start-container]') as HTMLElement;
2196
+ const endContainer = this._element.querySelector('[data-kt-datepicker-end-container]') as HTMLElement;
2197
+
2198
+ if (!startContainer || !endContainer) return;
2199
+
2200
+ // Re-instantiate the range segmented inputs with the current state
2201
+ // This will update the display without recreating the entire UI
2202
+ instantiateRangeSegmentedInputs(
2203
+ startContainer,
2204
+ endContainer,
2205
+ state,
2206
+ this._config,
2207
+ (date: Date) => {
2208
+ const end = state.selectedRange?.end || null;
2209
+ let newEnd = end;
2210
+ if (end && date > end) newEnd = null;
2211
+ this._unifiedStateManager.updateState({ selectedRange: { start: date, end: newEnd } }, 'range-selection');
2212
+ this._updateCalendarContent();
2213
+ },
2214
+ (date: Date) => {
2215
+ const start = state.selectedRange?.start || null;
2216
+ let newStart = start;
2217
+ if (start && date < start) newStart = null;
2218
+ this._unifiedStateManager.updateState({ selectedRange: { start: newStart, end: date } }, 'range-selection');
2219
+ this._updateCalendarContent();
2220
+ }
2221
+ );
2222
+ }
2223
+
2224
+ /**
2225
+ * Handle changes from the segmented input
2226
+ */
2227
+ private _handleSegmentedInputChange(date: Date): void {
2228
+ console.log('🗓️ [KTDatepicker] Segmented input change:', date);
2229
+
2230
+ // When date changes from segmented input, also update the calendar view to show the selected date's month/year
2231
+ const currentState = this._unifiedStateManager.getState();
2232
+ const newCurrentDate = new Date(date.getFullYear(), date.getMonth(), 1); // First day of selected date's month
2233
+
2234
+ // Prepare state updates
2235
+ const stateUpdates: Partial<KTDatepickerState> = {
2236
+ selectedDate: date,
2237
+ currentDate: newCurrentDate
2238
+ };
2239
+
2240
+ // Update selectedTime if time is enabled
2241
+ if (this._config.enableTime) {
2242
+ stateUpdates.selectedTime = dateToTimeState(date);
2243
+ }
2244
+
2245
+ // Update both selectedDate and currentDate to sync the calendar view
2246
+ // Use immediate update to ensure events fire synchronously
2247
+ this._unifiedStateManager.updateState(stateUpdates, 'segmented-input', true);
2248
+
2249
+ // Update calendar content to sync dropdown even when closed
2250
+ this._updateCalendarContent();
2251
+
2252
+ // Fire onChange event
2253
+ this._fireDatepickerEvent('onChange', date, this);
2254
+ }
2255
+
2256
+ /**
2257
+ * Normalize a date to midnight for date-only comparison (removes time component)
2258
+ */
2259
+
2260
+ /**
2261
+ * Disable day buttons outside min/max date range
2262
+ */
2263
+ private _enforceMinMaxDates() {
2264
+ if (this._config.minDate || this._config.maxDate) {
2265
+ // Normalize min/max dates to midnight for date-only comparison
2266
+ const minDate = this._config.minDate ? normalizeDateToMidnight(new Date(this._config.minDate)) : null;
2267
+ const maxDate = this._config.maxDate ? normalizeDateToMidnight(new Date(this._config.maxDate)) : null;
2268
+
2269
+ // Find calendar element - try multiple strategies
2270
+ let calendarElement = this._cachedElements.calendarElement;
2271
+ if (!calendarElement || !calendarElement.isConnected) {
2272
+ // Try to find dropdown first
2273
+ let dropdownEl: HTMLElement | null = this._element.querySelector('[data-kt-datepicker-dropdown]') as HTMLElement;
2274
+ if (!dropdownEl && this._instanceId) {
2275
+ dropdownEl = document.querySelector(`[data-kt-datepicker-dropdown][data-kt-datepicker-instance-id="${this._instanceId}"]`) as HTMLElement;
2276
+ }
2277
+ if (dropdownEl) {
2278
+ calendarElement = dropdownEl.querySelector('[data-kt-datepicker-calendar-table]') as HTMLElement;
2279
+ }
2280
+ }
2281
+
2282
+ if (!calendarElement) return;
2283
+
2284
+ // Get all day cells using the calendar element scope
2285
+ const dayCells = calendarElement.querySelectorAll('td[data-kt-datepicker-day]');
2286
+
2287
+ dayCells.forEach((td) => {
2288
+ // Get the actual date from the data-date attribute (stored as ISO string)
2289
+ const dateISO = td.getAttribute('data-date');
2290
+ if (!dateISO) return;
2291
+
2292
+ // Parse the ISO date and normalize to midnight for comparison
2293
+ const cellDate = normalizeDateToMidnight(new Date(dateISO));
2294
+ const btn = td.querySelector('button[data-day]') as HTMLButtonElement;
2295
+ if (!btn) return;
2296
+
2297
+ let disabled = false;
2298
+ // Compare dates (normalized to midnight) for proper date-only comparison
2299
+ if (minDate && cellDate < minDate) disabled = true;
2300
+ if (maxDate && cellDate > maxDate) disabled = true;
2301
+
2302
+ if (disabled) {
2303
+ btn.setAttribute('disabled', 'true');
2304
+ td.setAttribute('data-out-of-range', 'true');
2305
+ } else {
2306
+ btn.removeAttribute('disabled');
2307
+ td.removeAttribute('data-out-of-range');
2308
+ }
2309
+ });
2310
+ }
2311
+ }
2312
+
2313
+
2314
+
2315
+ /**
2316
+ * Opens the datepicker dropdown.
2317
+ */
2318
+ public open() {
2319
+ if (this._unifiedStateManager.isDropdownOpen()) {
2320
+ console.log('🗓️ [KTDatepicker] open() skipped: already open');
2321
+ return;
2322
+ }
2323
+ if (this._config.disabled) {
2324
+ console.log('🗓️ [KTDatepicker] open() blocked: datepicker is disabled');
2325
+ return;
2326
+ }
2327
+
2328
+ console.log('🗓️ [KTDatepicker] open() called, attempting to open dropdown');
2329
+
2330
+ // Use unified state management
2331
+ const success = this._unifiedStateManager.setDropdownOpen(true, 'datepicker-open');
2332
+ if (!success) {
2333
+ console.log('🗓️ [KTDatepicker] open() blocked by state validation');
2334
+ return;
2335
+ }
2336
+
2337
+ console.log('🗓️ [KTDatepicker] State manager open() successful, dropdown module:', this._dropdownModule);
2338
+
2339
+ // Ensure dropdown content is rendered before opening
2340
+ const dropdownEl = this._element.querySelector('[data-kt-datepicker-dropdown]') as HTMLElement;
2341
+ if (dropdownEl) {
2342
+ // Always render dropdown content to ensure it's up to date
2343
+ console.log('🗓️ [KTDatepicker] Rendering dropdown content before opening');
2344
+ this._renderDropdownContent(dropdownEl);
2345
+ }
2346
+
2347
+ // Use dropdown module if available
2348
+ if (this._dropdownModule) {
2349
+ console.log('🗓️ [KTDatepicker] Dropdown module available, state change will trigger open');
2350
+ // The dropdown module will automatically open via observer pattern
2351
+ } else {
2352
+ console.log('🗓️ [KTDatepicker] No dropdown module, using fallback');
2353
+ }
2354
+
2355
+ // Don't call _render() here as it recreates the dropdown module
2356
+ // The dropdown module handles its own visibility
2357
+ }
2358
+
2359
+ /**
2360
+ * Closes the datepicker dropdown.
2361
+ */
2362
+ public close() {
2363
+ console.log('🗓️ [KTDatepicker] close() called, current state:', {
2364
+ stateManagerOpen: this._unifiedStateManager.isDropdownOpen(),
2365
+ dropdownModuleOpen: this._dropdownModule?.isOpen(),
2366
+ stateManagerState: this._unifiedStateManager.getDropdownState()
2367
+ });
2368
+
2369
+ if (!this._unifiedStateManager.isDropdownOpen()) {
2370
+ console.log('🗓️ [KTDatepicker] close() skipped: already closed');
2371
+ return;
2372
+ }
2373
+ // Debug log with stack trace
2374
+ console.log('[KTDatepicker] close() called. Dropdown will close. Stack trace:', new Error().stack);
2375
+
2376
+ // Use unified state management
2377
+ const success = this._unifiedStateManager.setDropdownOpen(false, 'datepicker-close');
2378
+ if (!success) {
2379
+ console.log('🗓️ [KTDatepicker] close() blocked by state validation');
2380
+ return;
2381
+ }
2382
+
2383
+ console.log('🗓️ [KTDatepicker] State manager close() successful');
2384
+
2385
+ // Use dropdown module if available
2386
+ if (this._dropdownModule) {
2387
+ console.log('🗓️ [KTDatepicker] Dropdown module available, state change will trigger close');
2388
+ // The dropdown module will automatically close via observer pattern
2389
+ } else {
2390
+ console.log('🗓️ [KTDatepicker] No dropdown module, using fallback');
2391
+ }
2392
+
2393
+ // Don't call _render() here as it recreates the dropdown module
2394
+ // The dropdown module handles its own visibility
2395
+ }
2396
+
2397
+ public toggle() {
2398
+ if (this._unifiedStateManager.isDropdownOpen()) {
2399
+ this.close();
2400
+ } else {
2401
+ this.open();
2402
+ }
2403
+ }
2404
+
2405
+ /**
2406
+ * Returns whether the datepicker dropdown is currently open.
2407
+ */
2408
+ public isOpen(): boolean {
2409
+ return this._unifiedStateManager.isDropdownOpen();
2410
+ }
2411
+
2412
+ /**
2413
+ * Returns the current state of the datepicker component.
2414
+ */
2415
+ public getState(): KTDatepickerState {
2416
+ return { ...this._unifiedStateManager.getState() };
2417
+ }
2418
+
2419
+ /**
2420
+ * Returns the current dropdown state.
2421
+ */
2422
+ public getDropdownState(): DropdownState {
2423
+ return this._unifiedStateManager.getDropdownState();
2424
+ }
2425
+
2426
+ /**
2427
+ * StateObserver implementation: Called when state changes
2428
+ */
2429
+ /**
2430
+ * StateObserver implementation: Returns update priority (lower = higher priority)
2431
+ */
2432
+ getUpdatePriority(): number {
2433
+ return 10; // Medium priority
2434
+ }
2435
+
2436
+ /**
2437
+ * StateObserver implementation: Handle state changes from unified state manager
2438
+ */
2439
+ onStateChange(newState: KTDatepickerState, oldState: KTDatepickerState): void {
2440
+ this._handleStateChange(newState, oldState);
2441
+ }
2442
+
2443
+ /**
2444
+ * Handle state changes from the unified state manager
2445
+ */
2446
+ private _handleStateChange(newState: KTDatepickerState, oldState: KTDatepickerState): void {
2447
+ console.log('🗓️ [KTDatepicker] _handleStateChange called:', { newState, oldState });
2448
+
2449
+ // Skip UI updates if this is from segmented input arrow navigation
2450
+ if (typeof window !== 'undefined' && (window as any).__ktui_segmented_input_arrow_navigation) {
2451
+ console.log('🗓️ [KTDatepicker] Skipping UI update due to segmented input navigation');
2452
+ return;
2453
+ }
2454
+
2455
+ // Get changed properties for selective updates
2456
+ const changedProperties = this._unifiedStateManager.getLastChangedProperties();
2457
+
2458
+ // Get the source of the last state update
2459
+ const lastUpdateSource = this._unifiedStateManager.getLastUpdateSource();
2460
+
2461
+ // Update dropdown state if open/closed changed
2462
+ if (changedProperties.has('isOpen') && newState.isOpen !== oldState.isOpen) {
2463
+ if (this._dropdownModule) {
2464
+ console.log('🗓️ [KTDatepicker] Dropdown module available, state change will trigger open');
2465
+ // The dropdown module will automatically open via observer pattern
2466
+ } else {
2467
+ console.log('🗓️ [KTDatepicker] No dropdown module, using fallback');
2468
+ }
2469
+
2470
+ // Don't call _render() here as it recreates the dropdown module
2471
+ // The dropdown module handles its own visibility
2472
+ }
2473
+
2474
+ // Update disabled state only if it changed
2475
+ if (changedProperties.has('isDisabled') && newState.isDisabled !== oldState.isDisabled) {
2476
+ this._updateDisabledState();
2477
+ }
2478
+
2479
+ // Selective UI updates based on changed properties
2480
+ // Update input field only if selection-related properties changed
2481
+ if (changedProperties.has('selectedDate') ||
2482
+ changedProperties.has('selectedRange') ||
2483
+ changedProperties.has('selectedDates') ||
2484
+ changedProperties.has('selectedRange.start') ||
2485
+ changedProperties.has('selectedRange.end')) {
2486
+ this._updateInput(newState);
2487
+ }
2488
+
2489
+ // Update segmented input only if selectedDate changed (and not from segmented input itself)
2490
+ if (this._config.range) {
2491
+ if ((changedProperties.has('selectedRange') ||
2492
+ changedProperties.has('selectedRange.start') ||
2493
+ changedProperties.has('selectedRange.end')) &&
2494
+ lastUpdateSource !== 'range-selection') {
2495
+ this._updateRangeSegmentedInput(newState);
2496
+ } else if (lastUpdateSource === 'range-selection') {
2497
+ console.log('🗓️ [KTDatepicker] Skipping _updateRangeSegmentedInput to preserve focus during typing');
2498
+ }
2499
+ } else {
2500
+ if (changedProperties.has('selectedDate') && lastUpdateSource !== 'segmented-input') {
2501
+ this._updateSegmentedInput(newState);
2502
+ } else if (lastUpdateSource === 'segmented-input') {
2503
+ console.log('🗓️ [KTDatepicker] Skipping _updateSegmentedInput to preserve focus during typing');
2504
+ }
2505
+ }
2506
+
2507
+ // Update calendar highlighting only if selection state changed
2508
+ if (changedProperties.has('selectedDate') ||
2509
+ changedProperties.has('selectedRange') ||
2510
+ changedProperties.has('selectedRange.start') ||
2511
+ changedProperties.has('selectedRange.end') ||
2512
+ changedProperties.has('selectedDates') ||
2513
+ changedProperties.has('currentDate')) {
2514
+ this._updateCalendar(newState);
2515
+ }
2516
+
2517
+ // Update time picker only if selectedTime changed
2518
+ if (changedProperties.has('selectedTime')) {
2519
+ this._updateTimePicker(newState);
2520
+ }
2521
+
2522
+ // Fire events based on state changes
2523
+ this._fireEvents(newState, oldState);
2524
+
2525
+ // Start observing for dynamic element creation
2526
+ this._startElementObservation();
2527
+ }
2528
+
2529
+ // Static init method for auto-initialization
2530
+ public static init(): void {
2531
+ const elements = document.querySelectorAll<HTMLElement>('[data-kt-datepicker]');
2532
+ elements.forEach((el) => {
2533
+ if (!(el as any).instance) {
2534
+ new KTDatepicker(el);
2535
+ }
2536
+ });
2537
+ }
1287
2538
  }
2539
+
2540
+ // Optionally, export a static init method for auto-initialization
2541
+ export function initDatepickers() {
2542
+ const elements = document.querySelectorAll<HTMLElement>('[data-kt-datepicker]');
2543
+ elements.forEach((el) => {
2544
+ if (!(el as any).instance) {
2545
+ new KTDatepicker(el);
2546
+ }
2547
+ });
2548
+ }