@keenthemes/ktui 1.1.0 → 1.1.1

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 (222) hide show
  1. package/README.md +0 -27
  2. package/dist/ktui.js +6790 -14063
  3. package/dist/ktui.min.js +1 -1
  4. package/dist/ktui.min.js.map +1 -1
  5. package/dist/styles.css +1132 -2705
  6. package/lib/cjs/components/datatable/__tests__/pagination-reset.test.js +596 -0
  7. package/lib/cjs/components/datatable/__tests__/pagination-reset.test.js.map +1 -0
  8. package/lib/cjs/components/datatable/__tests__/race-conditions.test.js +548 -0
  9. package/lib/cjs/components/datatable/__tests__/race-conditions.test.js.map +1 -0
  10. package/lib/cjs/components/datatable/__tests__/setup.js +63 -0
  11. package/lib/cjs/components/datatable/__tests__/setup.js.map +1 -0
  12. package/lib/cjs/components/datatable/datatable.js +92 -30
  13. package/lib/cjs/components/datatable/datatable.js.map +1 -1
  14. package/lib/cjs/index.js +1 -10
  15. package/lib/cjs/index.js.map +1 -1
  16. package/lib/esm/components/datatable/__tests__/pagination-reset.test.js +594 -0
  17. package/lib/esm/components/datatable/__tests__/pagination-reset.test.js.map +1 -0
  18. package/lib/esm/components/datatable/__tests__/race-conditions.test.js +546 -0
  19. package/lib/esm/components/datatable/__tests__/race-conditions.test.js.map +1 -0
  20. package/lib/esm/components/datatable/__tests__/setup.js +58 -0
  21. package/lib/esm/components/datatable/__tests__/setup.js.map +1 -0
  22. package/lib/esm/components/datatable/datatable.js +92 -30
  23. package/lib/esm/components/datatable/datatable.js.map +1 -1
  24. package/lib/esm/index.js +0 -7
  25. package/lib/esm/index.js.map +1 -1
  26. package/package.json +7 -9
  27. package/src/components/alert/alert.css +188 -429
  28. package/src/components/datatable/__tests__/pagination-reset.test.ts +657 -0
  29. package/src/components/datatable/__tests__/race-conditions.test.ts +455 -0
  30. package/src/components/datatable/__tests__/setup.ts +67 -0
  31. package/src/components/datatable/datatable.ts +66 -11
  32. package/src/components/input/input.css +0 -1
  33. package/src/components/select/select.css +0 -1
  34. package/src/components/select/variants.css +4 -0
  35. package/src/components/textarea/textarea.css +0 -1
  36. package/src/index.ts +0 -10
  37. package/styles.css +0 -1
  38. package/lib/cjs/components/alert/alert.js +0 -1025
  39. package/lib/cjs/components/alert/alert.js.map +0 -1
  40. package/lib/cjs/components/alert/index.js +0 -20
  41. package/lib/cjs/components/alert/index.js.map +0 -1
  42. package/lib/cjs/components/alert/templates.js +0 -120
  43. package/lib/cjs/components/alert/templates.js.map +0 -1
  44. package/lib/cjs/components/alert/types.js +0 -7
  45. package/lib/cjs/components/alert/types.js.map +0 -1
  46. package/lib/cjs/components/datepicker/config/config.js +0 -42
  47. package/lib/cjs/components/datepicker/config/config.js.map +0 -1
  48. package/lib/cjs/components/datepicker/config/index.js +0 -24
  49. package/lib/cjs/components/datepicker/config/index.js.map +0 -1
  50. package/lib/cjs/components/datepicker/config/interfaces.js +0 -7
  51. package/lib/cjs/components/datepicker/config/interfaces.js.map +0 -1
  52. package/lib/cjs/components/datepicker/config/types.js +0 -7
  53. package/lib/cjs/components/datepicker/config/types.js.map +0 -1
  54. package/lib/cjs/components/datepicker/core/event-manager.js +0 -135
  55. package/lib/cjs/components/datepicker/core/event-manager.js.map +0 -1
  56. package/lib/cjs/components/datepicker/core/focus-manager.js +0 -167
  57. package/lib/cjs/components/datepicker/core/focus-manager.js.map +0 -1
  58. package/lib/cjs/components/datepicker/core/helpers.js +0 -219
  59. package/lib/cjs/components/datepicker/core/helpers.js.map +0 -1
  60. package/lib/cjs/components/datepicker/core/index.js +0 -25
  61. package/lib/cjs/components/datepicker/core/index.js.map +0 -1
  62. package/lib/cjs/components/datepicker/core/unified-state-manager.js +0 -394
  63. package/lib/cjs/components/datepicker/core/unified-state-manager.js.map +0 -1
  64. package/lib/cjs/components/datepicker/datepicker.js +0 -2252
  65. package/lib/cjs/components/datepicker/datepicker.js.map +0 -1
  66. package/lib/cjs/components/datepicker/index.js +0 -24
  67. package/lib/cjs/components/datepicker/index.js.map +0 -1
  68. package/lib/cjs/components/datepicker/ui/index.js +0 -23
  69. package/lib/cjs/components/datepicker/ui/index.js.map +0 -1
  70. package/lib/cjs/components/datepicker/ui/input/dropdown.js +0 -489
  71. package/lib/cjs/components/datepicker/ui/input/dropdown.js.map +0 -1
  72. package/lib/cjs/components/datepicker/ui/input/index.js +0 -23
  73. package/lib/cjs/components/datepicker/ui/input/index.js.map +0 -1
  74. package/lib/cjs/components/datepicker/ui/input/segmented-input.js +0 -640
  75. package/lib/cjs/components/datepicker/ui/input/segmented-input.js.map +0 -1
  76. package/lib/cjs/components/datepicker/ui/renderers/calendar.js +0 -446
  77. package/lib/cjs/components/datepicker/ui/renderers/calendar.js.map +0 -1
  78. package/lib/cjs/components/datepicker/ui/renderers/footer.js +0 -42
  79. package/lib/cjs/components/datepicker/ui/renderers/footer.js.map +0 -1
  80. package/lib/cjs/components/datepicker/ui/renderers/header.js +0 -32
  81. package/lib/cjs/components/datepicker/ui/renderers/header.js.map +0 -1
  82. package/lib/cjs/components/datepicker/ui/renderers/index.js +0 -25
  83. package/lib/cjs/components/datepicker/ui/renderers/index.js.map +0 -1
  84. package/lib/cjs/components/datepicker/ui/renderers/time-picker.js +0 -384
  85. package/lib/cjs/components/datepicker/ui/renderers/time-picker.js.map +0 -1
  86. package/lib/cjs/components/datepicker/ui/templates/index.js +0 -22
  87. package/lib/cjs/components/datepicker/ui/templates/index.js.map +0 -1
  88. package/lib/cjs/components/datepicker/ui/templates/templates.js +0 -253
  89. package/lib/cjs/components/datepicker/ui/templates/templates.js.map +0 -1
  90. package/lib/cjs/components/datepicker/utils/date-formatters.js +0 -88
  91. package/lib/cjs/components/datepicker/utils/date-formatters.js.map +0 -1
  92. package/lib/cjs/components/datepicker/utils/date-utils.js +0 -194
  93. package/lib/cjs/components/datepicker/utils/date-utils.js.map +0 -1
  94. package/lib/cjs/components/datepicker/utils/index.js +0 -24
  95. package/lib/cjs/components/datepicker/utils/index.js.map +0 -1
  96. package/lib/cjs/components/datepicker/utils/time-utils.js +0 -213
  97. package/lib/cjs/components/datepicker/utils/time-utils.js.map +0 -1
  98. package/lib/esm/components/alert/alert.js +0 -1022
  99. package/lib/esm/components/alert/alert.js.map +0 -1
  100. package/lib/esm/components/alert/index.js +0 -4
  101. package/lib/esm/components/alert/index.js.map +0 -1
  102. package/lib/esm/components/alert/templates.js +0 -112
  103. package/lib/esm/components/alert/templates.js.map +0 -1
  104. package/lib/esm/components/alert/types.js +0 -6
  105. package/lib/esm/components/alert/types.js.map +0 -1
  106. package/lib/esm/components/datepicker/config/config.js +0 -39
  107. package/lib/esm/components/datepicker/config/config.js.map +0 -1
  108. package/lib/esm/components/datepicker/config/index.js +0 -8
  109. package/lib/esm/components/datepicker/config/index.js.map +0 -1
  110. package/lib/esm/components/datepicker/config/interfaces.js +0 -6
  111. package/lib/esm/components/datepicker/config/interfaces.js.map +0 -1
  112. package/lib/esm/components/datepicker/config/types.js +0 -6
  113. package/lib/esm/components/datepicker/config/types.js.map +0 -1
  114. package/lib/esm/components/datepicker/core/event-manager.js +0 -133
  115. package/lib/esm/components/datepicker/core/event-manager.js.map +0 -1
  116. package/lib/esm/components/datepicker/core/focus-manager.js +0 -164
  117. package/lib/esm/components/datepicker/core/focus-manager.js.map +0 -1
  118. package/lib/esm/components/datepicker/core/helpers.js +0 -211
  119. package/lib/esm/components/datepicker/core/helpers.js.map +0 -1
  120. package/lib/esm/components/datepicker/core/index.js +0 -9
  121. package/lib/esm/components/datepicker/core/index.js.map +0 -1
  122. package/lib/esm/components/datepicker/core/unified-state-manager.js +0 -391
  123. package/lib/esm/components/datepicker/core/unified-state-manager.js.map +0 -1
  124. package/lib/esm/components/datepicker/datepicker.js +0 -2248
  125. package/lib/esm/components/datepicker/datepicker.js.map +0 -1
  126. package/lib/esm/components/datepicker/index.js +0 -7
  127. package/lib/esm/components/datepicker/index.js.map +0 -1
  128. package/lib/esm/components/datepicker/ui/index.js +0 -7
  129. package/lib/esm/components/datepicker/ui/index.js.map +0 -1
  130. package/lib/esm/components/datepicker/ui/input/dropdown.js +0 -486
  131. package/lib/esm/components/datepicker/ui/input/dropdown.js.map +0 -1
  132. package/lib/esm/components/datepicker/ui/input/index.js +0 -7
  133. package/lib/esm/components/datepicker/ui/input/index.js.map +0 -1
  134. package/lib/esm/components/datepicker/ui/input/segmented-input.js +0 -637
  135. package/lib/esm/components/datepicker/ui/input/segmented-input.js.map +0 -1
  136. package/lib/esm/components/datepicker/ui/renderers/calendar.js +0 -443
  137. package/lib/esm/components/datepicker/ui/renderers/calendar.js.map +0 -1
  138. package/lib/esm/components/datepicker/ui/renderers/footer.js +0 -39
  139. package/lib/esm/components/datepicker/ui/renderers/footer.js.map +0 -1
  140. package/lib/esm/components/datepicker/ui/renderers/header.js +0 -29
  141. package/lib/esm/components/datepicker/ui/renderers/header.js.map +0 -1
  142. package/lib/esm/components/datepicker/ui/renderers/index.js +0 -9
  143. package/lib/esm/components/datepicker/ui/renderers/index.js.map +0 -1
  144. package/lib/esm/components/datepicker/ui/renderers/time-picker.js +0 -381
  145. package/lib/esm/components/datepicker/ui/renderers/time-picker.js.map +0 -1
  146. package/lib/esm/components/datepicker/ui/templates/index.js +0 -6
  147. package/lib/esm/components/datepicker/ui/templates/index.js.map +0 -1
  148. package/lib/esm/components/datepicker/ui/templates/templates.js +0 -242
  149. package/lib/esm/components/datepicker/ui/templates/templates.js.map +0 -1
  150. package/lib/esm/components/datepicker/utils/date-formatters.js +0 -83
  151. package/lib/esm/components/datepicker/utils/date-formatters.js.map +0 -1
  152. package/lib/esm/components/datepicker/utils/date-utils.js +0 -184
  153. package/lib/esm/components/datepicker/utils/date-utils.js.map +0 -1
  154. package/lib/esm/components/datepicker/utils/index.js +0 -8
  155. package/lib/esm/components/datepicker/utils/index.js.map +0 -1
  156. package/lib/esm/components/datepicker/utils/time-utils.js +0 -201
  157. package/lib/esm/components/datepicker/utils/time-utils.js.map +0 -1
  158. package/src/components/alert/alert.ts +0 -990
  159. package/src/components/alert/index.ts +0 -4
  160. package/src/components/alert/templates.ts +0 -110
  161. package/src/components/alert/tests/accessibility/aria-roles.test.ts +0 -19
  162. package/src/components/alert/tests/accessibility/focus-management.test.ts +0 -19
  163. package/src/components/alert/tests/accessibility/keyboard-nav.test.ts +0 -22
  164. package/src/components/alert/tests/actions/confirm-cancel.test.ts +0 -122
  165. package/src/components/alert/tests/actions/input-field.test.ts +0 -180
  166. package/src/components/alert/tests/alert.basic.test.ts +0 -126
  167. package/src/components/alert/tests/alert.config.test.ts +0 -75
  168. package/src/components/alert/tests/alert.templates.test.ts +0 -17
  169. package/src/components/alert/tests/config/attribute-config.test.ts +0 -94
  170. package/src/components/alert/tests/config/json-config.test.ts +0 -119
  171. package/src/components/alert/tests/config/merging.test.ts +0 -89
  172. package/src/components/alert/tests/dismissal/auto-dismiss.test.ts +0 -96
  173. package/src/components/alert/tests/dismissal/escape-key-dismiss.test.ts +0 -105
  174. package/src/components/alert/tests/dismissal/manual-dismiss.test.ts +0 -90
  175. package/src/components/alert/tests/dismissal/outside-click-dismiss.test.ts +0 -91
  176. package/src/components/alert/tests/edge-cases/invalid-config.test.ts +0 -19
  177. package/src/components/alert/tests/edge-cases/multiple-alerts.test.ts +0 -19
  178. package/src/components/alert/tests/rendering/custom-content.test.ts +0 -81
  179. package/src/components/alert/tests/rendering/info-alert.test.ts +0 -84
  180. package/src/components/alert/tests/rendering/success-alert.test.ts +0 -100
  181. package/src/components/alert/tests/templates/default-templates.test.ts +0 -16
  182. package/src/components/alert/tests/templates/user-templates.test.ts +0 -16
  183. package/src/components/alert/types.ts +0 -145
  184. package/src/components/datepicker/__tests__/datepicker-events.test.ts +0 -356
  185. package/src/components/datepicker/__tests__/datepicker-init.test.ts +0 -343
  186. package/src/components/datepicker/__tests__/datepicker-integration.test.ts +0 -435
  187. package/src/components/datepicker/__tests__/datepicker-timezone.test.ts +0 -220
  188. package/src/components/datepicker/__tests__/segmented-input-focus.test.ts +0 -380
  189. package/src/components/datepicker/__tests__/selective-state-updates.test.ts +0 -400
  190. package/src/components/datepicker/__tests__/state-manager.test.ts +0 -421
  191. package/src/components/datepicker/__tests__/time-preservation.test.ts +0 -387
  192. package/src/components/datepicker/config/config.ts +0 -40
  193. package/src/components/datepicker/config/index.ts +0 -8
  194. package/src/components/datepicker/config/interfaces.ts +0 -82
  195. package/src/components/datepicker/config/types.ts +0 -188
  196. package/src/components/datepicker/core/event-manager.ts +0 -159
  197. package/src/components/datepicker/core/focus-manager.ts +0 -201
  198. package/src/components/datepicker/core/helpers.ts +0 -231
  199. package/src/components/datepicker/core/index.ts +0 -9
  200. package/src/components/datepicker/core/unified-state-manager.ts +0 -459
  201. package/src/components/datepicker/datepicker.css +0 -435
  202. package/src/components/datepicker/datepicker.ts +0 -2548
  203. package/src/components/datepicker/index.ts +0 -8
  204. package/src/components/datepicker/ui/index.ts +0 -7
  205. package/src/components/datepicker/ui/input/dropdown.ts +0 -552
  206. package/src/components/datepicker/ui/input/index.ts +0 -7
  207. package/src/components/datepicker/ui/input/segmented-input.ts +0 -638
  208. package/src/components/datepicker/ui/renderers/__tests__/calendar-optimizations.test.ts +0 -611
  209. package/src/components/datepicker/ui/renderers/calendar.ts +0 -530
  210. package/src/components/datepicker/ui/renderers/footer.ts +0 -43
  211. package/src/components/datepicker/ui/renderers/header.ts +0 -33
  212. package/src/components/datepicker/ui/renderers/index.ts +0 -9
  213. package/src/components/datepicker/ui/renderers/time-picker.ts +0 -438
  214. package/src/components/datepicker/ui/templates/index.ts +0 -6
  215. package/src/components/datepicker/ui/templates/templates.ts +0 -306
  216. package/src/components/datepicker/utils/__tests__/date-formatters.test.ts +0 -160
  217. package/src/components/datepicker/utils/__tests__/date-utils-keys.test.ts +0 -86
  218. package/src/components/datepicker/utils/__tests__/date-utils-timezone.test.ts +0 -215
  219. package/src/components/datepicker/utils/date-formatters.ts +0 -85
  220. package/src/components/datepicker/utils/date-utils.ts +0 -172
  221. package/src/components/datepicker/utils/index.ts +0 -8
  222. package/src/components/datepicker/utils/time-utils.ts +0 -221
@@ -1,2548 +0,0 @@
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.
5
- */
6
-
7
- import KTComponent from '../component';
8
- import { KTDatepickerConfig, KTDatepickerState } from './config/types';
9
- import {
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';
38
-
39
- /**
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
49
- */
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
- }
1765
-
1766
- /**
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
- }
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
- }