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