@keenthemes/ktui 1.0.28 → 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 +22 -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/CONTRIBUTING.md +0 -101
- package/examples/datatable/checkbox-events-test.html +0 -400
- package/examples/datatable/credentials-test.html +0 -423
- package/examples/datatable/remote-checkbox-test.html +0 -365
- package/examples/datatable/sorting-test.html +0 -258
- package/examples/image-input/file-upload-example.html +0 -189
- package/examples/modal/persistent.html +0 -205
- package/examples/modal/remote-select-dropdown.html +0 -166
- package/examples/modal/select-dropdown-container.html +0 -129
- package/examples/select/avatar.html +0 -47
- package/examples/select/basic-usage.html +0 -39
- package/examples/select/country.html +0 -43
- package/examples/select/dark-mode.html +0 -93
- package/examples/select/description.html +0 -53
- package/examples/select/disable-option.html +0 -37
- package/examples/select/disable-select.html +0 -35
- package/examples/select/dropdowncontainer.html +0 -111
- package/examples/select/dynamic-methods.html +0 -273
- package/examples/select/formdata-remote.html +0 -161
- package/examples/select/global-config.html +0 -81
- package/examples/select/icon-multiple.html +0 -50
- package/examples/select/icon.html +0 -48
- package/examples/select/max-selection.html +0 -38
- package/examples/select/modal-container.html +0 -128
- package/examples/select/modal-positioning-test.html +0 -338
- package/examples/select/modal.html +0 -80
- package/examples/select/multiple.html +0 -40
- package/examples/select/native-selected.html +0 -64
- package/examples/select/placeholder.html +0 -40
- package/examples/select/remote-data-preselected.html +0 -283
- package/examples/select/remote-data.html +0 -38
- package/examples/select/search.html +0 -57
- package/examples/select/sizes.html +0 -94
- package/examples/select/tags-enhanced.html +0 -86
- package/examples/select/tags-icons.html +0 -57
- package/examples/select/template-customization.html +0 -61
- package/examples/sticky/README.md +0 -158
- package/examples/sticky/debug-sticky.html +0 -144
- package/examples/sticky/test-runner.html +0 -175
- package/examples/sticky/test-sticky-logic.js +0 -369
- package/examples/sticky/test-sticky-positioning.html +0 -386
- package/examples/toast/example.html +0 -479
- 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/prettier.config.js +0 -9
- 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
- package/tsconfig.json +0 -17
- package/webpack.config.js +0 -118
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* segmented-input.ts - Modular segmented input for KTDatepicker (2025+)
|
|
3
|
+
* Each date/time part is rendered as a focusable, editable segment.
|
|
4
|
+
*
|
|
5
|
+
* Features:
|
|
6
|
+
* - Segments: day, month, year, (optionally hour, minute, second, AM/PM)
|
|
7
|
+
* - Keyboard navigation: Tab, Shift+Tab, arrow keys, Enter
|
|
8
|
+
* - Direct typing/editing of segments with focus preservation
|
|
9
|
+
* - ARIA roles and accessibility for all segments
|
|
10
|
+
* - Emits change events on value update (debounced)
|
|
11
|
+
* - Integrates with KTDatepicker for value sync
|
|
12
|
+
*
|
|
13
|
+
* Keyboard Navigation:
|
|
14
|
+
* - Tab/Shift+Tab: Move between segments
|
|
15
|
+
* - Arrow Left/Right: Move between segments
|
|
16
|
+
* - Arrow Up/Down: Increment/decrement segment value
|
|
17
|
+
* - Enter: Move to next segment (wraps from last to first)
|
|
18
|
+
* - Number keys: Direct input with validation (optimized for performance)
|
|
19
|
+
*
|
|
20
|
+
* Performance Optimizations:
|
|
21
|
+
* - No DOM re-rendering during number typing
|
|
22
|
+
* - Focus preserved during rapid input
|
|
23
|
+
* - Debounced onChange events (150ms delay)
|
|
24
|
+
* - Caret position maintained during editing
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { getTemplateStrings } from '../templates/templates';
|
|
28
|
+
import { KTDatepickerConfig } from '../../config/types';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* SegmentedInputOptions defines the configuration for the segmented input.
|
|
32
|
+
*/
|
|
33
|
+
export interface SegmentedInputOptions {
|
|
34
|
+
value: Date;
|
|
35
|
+
format?: string; // e.g. 'MM/DD/YYYY', 'YYYY-MM-DD', etc.
|
|
36
|
+
min?: Date;
|
|
37
|
+
max?: Date;
|
|
38
|
+
disabled?: boolean;
|
|
39
|
+
required?: boolean;
|
|
40
|
+
readOnly?: boolean;
|
|
41
|
+
locale?: string;
|
|
42
|
+
onChange?: (value: Date) => void;
|
|
43
|
+
segments?: Array<'day' | 'month' | 'year' | 'hour' | 'minute' | 'second' | 'ampm'>;
|
|
44
|
+
timeFormat?: '12h' | '24h'; // Time format for display
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* SegmentedInput - renders a segmented date/time input.
|
|
49
|
+
* @param container - HTMLElement to render into
|
|
50
|
+
* @param options - SegmentedInputOptions
|
|
51
|
+
* @returns cleanup function
|
|
52
|
+
*/
|
|
53
|
+
export function SegmentedInput(container: HTMLElement, options: SegmentedInputOptions) {
|
|
54
|
+
// --- Internal state ---
|
|
55
|
+
let currentValue = new Date(options.value);
|
|
56
|
+
const segments = options.segments || ['month', 'day', 'year'];
|
|
57
|
+
const locale = options.locale || 'default';
|
|
58
|
+
|
|
59
|
+
// Global flag to track arrow navigation across all segmented inputs
|
|
60
|
+
if (!(window as any).__ktui_segmented_input_arrow_navigation) {
|
|
61
|
+
(window as any).__ktui_segmented_input_arrow_navigation = false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// --- Get templates ---
|
|
65
|
+
// Use a minimal config to get templates; in real usage, pass full config if available
|
|
66
|
+
const templates = getTemplateStrings({} as KTDatepickerConfig);
|
|
67
|
+
const segmentTpl = templates.dateSegment as string | ((data: any) => string) | undefined;
|
|
68
|
+
const separatorTpl = templates.segmentSeparator as string | ((data: any) => string) | undefined;
|
|
69
|
+
|
|
70
|
+
// --- Utility: get separator between segments ---
|
|
71
|
+
function getSeparatorBetweenSegments(segment1: string, segment2: string, format: string | undefined): string {
|
|
72
|
+
// Time segments use ":" as separator
|
|
73
|
+
const timeSegments = ['hour', 'minute', 'second'];
|
|
74
|
+
if (timeSegments.includes(segment1) && timeSegments.includes(segment2)) {
|
|
75
|
+
return ':';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Space between date and time
|
|
79
|
+
const dateSegments = ['day', 'month', 'year'];
|
|
80
|
+
if (dateSegments.includes(segment1) && timeSegments.includes(segment2)) {
|
|
81
|
+
return ' ';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// AM/PM has space before it
|
|
85
|
+
if (segment2 === 'ampm') {
|
|
86
|
+
return ' ';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Second to AM/PM has space
|
|
90
|
+
if (segment1 === 'second' && segment2 === 'ampm') {
|
|
91
|
+
return ' ';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Date segments use separator from format
|
|
95
|
+
if (dateSegments.includes(segment1) && dateSegments.includes(segment2)) {
|
|
96
|
+
return getSeparatorFromFormat(format);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Default fallback
|
|
100
|
+
return '';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// --- Utility: get separator from format ---
|
|
104
|
+
function getSeparatorFromFormat(format: string | undefined): string {
|
|
105
|
+
if (!format) return '/'; // Default fallback
|
|
106
|
+
|
|
107
|
+
// Find the first non-date token character to use as separator
|
|
108
|
+
const tokenRegex = /(yyyy|yy|MM|M|dd|d)/g;
|
|
109
|
+
let lastIndex = 0;
|
|
110
|
+
let match;
|
|
111
|
+
|
|
112
|
+
while ((match = tokenRegex.exec(format)) !== null) {
|
|
113
|
+
if (match.index > lastIndex) {
|
|
114
|
+
// Found a separator character
|
|
115
|
+
const separator = format.slice(lastIndex, match.index);
|
|
116
|
+
if (separator && separator.length > 0) {
|
|
117
|
+
return separator;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
lastIndex = match.index + match[0].length;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Check for separator after the last token
|
|
124
|
+
if (lastIndex < format.length) {
|
|
125
|
+
const separator = format.slice(lastIndex);
|
|
126
|
+
if (separator && separator.length > 0) {
|
|
127
|
+
return separator;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return '/'; // Default fallback
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// --- Utility: check if segment should be padded based on format ---
|
|
135
|
+
function shouldPadSegment(segment: string, format: string | undefined): boolean {
|
|
136
|
+
if (!format) return true; // Default to padded for backward compatibility
|
|
137
|
+
|
|
138
|
+
const tokenRegex = /(yyyy|yy|MM|M|dd|d|HH|H|mm|m|ss|s)/g;
|
|
139
|
+
let match;
|
|
140
|
+
|
|
141
|
+
while ((match = tokenRegex.exec(format)) !== null) {
|
|
142
|
+
switch (match[0]) {
|
|
143
|
+
case 'dd': if (segment === 'day') return true; break;
|
|
144
|
+
case 'd': if (segment === 'day') return false; break;
|
|
145
|
+
case 'MM': if (segment === 'month') return true; break;
|
|
146
|
+
case 'M': if (segment === 'month') return false; break;
|
|
147
|
+
case 'yyyy': if (segment === 'year') return true; break; // 4-digit year
|
|
148
|
+
case 'yy': if (segment === 'year') return false; break; // 2-digit year
|
|
149
|
+
case 'HH':
|
|
150
|
+
case 'H': if (segment === 'hour') return match[0] === 'HH'; break;
|
|
151
|
+
case 'mm':
|
|
152
|
+
case 'm': if (segment === 'minute') return match[0] === 'mm'; break;
|
|
153
|
+
case 'ss':
|
|
154
|
+
case 's': if (segment === 'second') return match[0] === 'ss'; break;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return true; // Default fallback
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// --- Utility: get segment value as string ---
|
|
162
|
+
function getSegmentValue(segment: string, date: Date): string {
|
|
163
|
+
const shouldPad = shouldPadSegment(segment, options.format);
|
|
164
|
+
|
|
165
|
+
switch (segment) {
|
|
166
|
+
case 'day':
|
|
167
|
+
const day = date.getDate();
|
|
168
|
+
return shouldPad ? day.toString().padStart(2, '0') : day.toString();
|
|
169
|
+
case 'month':
|
|
170
|
+
const month = date.getMonth() + 1;
|
|
171
|
+
return shouldPad ? month.toString().padStart(2, '0') : month.toString();
|
|
172
|
+
case 'year':
|
|
173
|
+
const year = date.getFullYear();
|
|
174
|
+
return shouldPad ? year.toString() : year.toString().slice(-2); // yy format shows last 2 digits
|
|
175
|
+
case 'hour':
|
|
176
|
+
// For 12-hour format, convert to 1-12 range
|
|
177
|
+
let hourValue: number;
|
|
178
|
+
if (options.timeFormat === '12h') {
|
|
179
|
+
const hour24 = date.getHours();
|
|
180
|
+
hourValue = hour24 === 0 ? 12 : hour24 > 12 ? hour24 - 12 : hour24;
|
|
181
|
+
} else {
|
|
182
|
+
hourValue = date.getHours();
|
|
183
|
+
}
|
|
184
|
+
return shouldPad ? hourValue.toString().padStart(2, '0') : hourValue.toString();
|
|
185
|
+
case 'minute':
|
|
186
|
+
const minute = date.getMinutes();
|
|
187
|
+
return shouldPad ? minute.toString().padStart(2, '0') : minute.toString();
|
|
188
|
+
case 'second':
|
|
189
|
+
const second = date.getSeconds();
|
|
190
|
+
return shouldPad ? second.toString().padStart(2, '0') : second.toString();
|
|
191
|
+
case 'ampm': return date.getHours() < 12 ? 'AM' : 'PM';
|
|
192
|
+
default: return '';
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// --- Utility: set segment value ---
|
|
197
|
+
function setSegmentValue(segment: string, value: string, date: Date): Date {
|
|
198
|
+
const d = new Date(date);
|
|
199
|
+
switch (segment) {
|
|
200
|
+
case 'day': d.setDate(Number(value)); break;
|
|
201
|
+
case 'month': d.setMonth(Number(value) - 1); break;
|
|
202
|
+
case 'year': d.setFullYear(Number(value)); break;
|
|
203
|
+
case 'hour':
|
|
204
|
+
// Handle 12-hour vs 24-hour format
|
|
205
|
+
let hourValue = Number(value);
|
|
206
|
+
if (options.timeFormat === '12h') {
|
|
207
|
+
// In 12-hour mode, convert 12-hour input to 24-hour
|
|
208
|
+
const currentHour = d.getHours();
|
|
209
|
+
const isPM = currentHour >= 12;
|
|
210
|
+
if (hourValue === 12) {
|
|
211
|
+
hourValue = isPM ? 12 : 0; // 12 AM = 0, 12 PM = 12
|
|
212
|
+
} else if (isPM) {
|
|
213
|
+
hourValue += 12; // PM hours: add 12
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// Preserve existing minutes and seconds when setting hour
|
|
217
|
+
const currentMinutes = d.getMinutes();
|
|
218
|
+
const currentSecondsForHour = d.getSeconds();
|
|
219
|
+
d.setHours(hourValue, currentMinutes, currentSecondsForHour);
|
|
220
|
+
break;
|
|
221
|
+
case 'minute':
|
|
222
|
+
// Preserve existing seconds when setting minute
|
|
223
|
+
const currentSecondsForMinute = d.getSeconds();
|
|
224
|
+
d.setMinutes(Number(value), currentSecondsForMinute);
|
|
225
|
+
break;
|
|
226
|
+
case 'second': d.setSeconds(Number(value)); break;
|
|
227
|
+
case 'ampm':
|
|
228
|
+
if (value === 'AM' && d.getHours() >= 12) {
|
|
229
|
+
d.setHours(d.getHours() - 12);
|
|
230
|
+
} else if (value === 'PM' && d.getHours() < 12) {
|
|
231
|
+
d.setHours(d.getHours() + 12);
|
|
232
|
+
}
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
return d;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// --- Utility: get min/max for a segment ---
|
|
239
|
+
function getSegmentMin(segment: string, date: Date): number | undefined {
|
|
240
|
+
switch (segment) {
|
|
241
|
+
case 'day':
|
|
242
|
+
// Use actual month/year for max days
|
|
243
|
+
return 1;
|
|
244
|
+
case 'month':
|
|
245
|
+
return 1;
|
|
246
|
+
case 'year':
|
|
247
|
+
return options.min ? options.min.getFullYear() : undefined;
|
|
248
|
+
case 'hour':
|
|
249
|
+
return 0;
|
|
250
|
+
case 'minute':
|
|
251
|
+
case 'second':
|
|
252
|
+
return 0;
|
|
253
|
+
case 'ampm':
|
|
254
|
+
return 0; // 0 = AM, 1 = PM
|
|
255
|
+
default:
|
|
256
|
+
return undefined;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
function getSegmentMax(segment: string, date: Date): number | undefined {
|
|
260
|
+
switch (segment) {
|
|
261
|
+
case 'day':
|
|
262
|
+
// Use actual month/year for max days
|
|
263
|
+
return new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate();
|
|
264
|
+
case 'month':
|
|
265
|
+
return 12;
|
|
266
|
+
case 'year':
|
|
267
|
+
return options.max ? options.max.getFullYear() : undefined;
|
|
268
|
+
case 'hour':
|
|
269
|
+
return 23;
|
|
270
|
+
case 'minute':
|
|
271
|
+
case 'second':
|
|
272
|
+
return 59;
|
|
273
|
+
case 'ampm':
|
|
274
|
+
return 1; // 0 = AM, 1 = PM
|
|
275
|
+
default:
|
|
276
|
+
return undefined;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// --- Track focused segment index ---
|
|
281
|
+
let focusedIdx = 0;
|
|
282
|
+
// --- Track caret position (offset) ---
|
|
283
|
+
let caretOffset: number | null = null;
|
|
284
|
+
// --- Track if this is the first render ---
|
|
285
|
+
let isInitialRender = true;
|
|
286
|
+
// --- Track if we're in the middle of Arrow Up/Down navigation ---
|
|
287
|
+
let isArrowNavigation = false;
|
|
288
|
+
|
|
289
|
+
// --- Focus a segment by index and restore caret position ---
|
|
290
|
+
/**
|
|
291
|
+
* Restores focus to the segment at the given index and restores caret position if available.
|
|
292
|
+
* @param idx - Index of the segment to focus
|
|
293
|
+
* @param caret - Caret offset to restore (null for end)
|
|
294
|
+
*/
|
|
295
|
+
function restoreFocus(idx: number, caret: number | null = null) {
|
|
296
|
+
try {
|
|
297
|
+
const segs = Array.from(container.querySelectorAll('[data-segment]')) as HTMLElement[];
|
|
298
|
+
if (segs[idx] && segs[idx].offsetParent !== null) { // Check if element is in DOM
|
|
299
|
+
segs.forEach((el, i) => el.setAttribute('tabindex', i === idx ? '0' : '-1'));
|
|
300
|
+
segs[idx].focus();
|
|
301
|
+
// Restore caret position (at end if null)
|
|
302
|
+
if (segs[idx].isContentEditable) {
|
|
303
|
+
const range = document.createRange();
|
|
304
|
+
range.selectNodeContents(segs[idx]);
|
|
305
|
+
range.collapse(false); // place at end
|
|
306
|
+
if (caret !== null && segs[idx].firstChild) {
|
|
307
|
+
range.setStart(segs[idx].firstChild, Math.min(caret, segs[idx].textContent?.length || 0));
|
|
308
|
+
range.collapse(true);
|
|
309
|
+
}
|
|
310
|
+
const sel = window.getSelection();
|
|
311
|
+
if (sel) {
|
|
312
|
+
sel.removeAllRanges();
|
|
313
|
+
sel.addRange(range);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
} catch (error) {
|
|
318
|
+
// Fallback: focus first available segment
|
|
319
|
+
const segs = Array.from(container.querySelectorAll('[data-segment]')) as HTMLElement[];
|
|
320
|
+
if (segs[0]) segs[0].focus();
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// --- Render segments using templates ---
|
|
325
|
+
function render() {
|
|
326
|
+
// Capture caret position before DOM update
|
|
327
|
+
const prevSegs = Array.from(container.querySelectorAll('[data-segment]')) as HTMLElement[];
|
|
328
|
+
if (prevSegs[focusedIdx] && document.activeElement === prevSegs[focusedIdx]) {
|
|
329
|
+
const sel = window.getSelection();
|
|
330
|
+
if (sel && sel.anchorNode === prevSegs[focusedIdx].firstChild) {
|
|
331
|
+
caretOffset = sel.anchorOffset;
|
|
332
|
+
} else {
|
|
333
|
+
caretOffset = null;
|
|
334
|
+
}
|
|
335
|
+
} else {
|
|
336
|
+
caretOffset = null;
|
|
337
|
+
}
|
|
338
|
+
container.innerHTML = '';
|
|
339
|
+
container.setAttribute('role', 'group');
|
|
340
|
+
container.setAttribute('aria-label', 'Date input');
|
|
341
|
+
container.tabIndex = -1;
|
|
342
|
+
|
|
343
|
+
// Build segments HTML using templates
|
|
344
|
+
let segmentsHtml = '';
|
|
345
|
+
segments.forEach((segment, idx) => {
|
|
346
|
+
const segmentValue = getSegmentValue(segment, currentValue);
|
|
347
|
+
const segmentData = {
|
|
348
|
+
segmentType: segment,
|
|
349
|
+
segmentValue,
|
|
350
|
+
ariaLabel: segment.charAt(0).toUpperCase() + segment.slice(1),
|
|
351
|
+
ariaValueNow: segmentValue,
|
|
352
|
+
ariaValueText: segmentValue,
|
|
353
|
+
ariaValueMin: getSegmentMin(segment, currentValue)?.toString() ?? '',
|
|
354
|
+
ariaValueMax: getSegmentMax(segment, currentValue)?.toString() ?? '',
|
|
355
|
+
tabindex: idx === focusedIdx ? '0' : '-1',
|
|
356
|
+
contenteditable: (!options.disabled && !options.readOnly).toString(),
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
let segmentHtml = '';
|
|
360
|
+
if (typeof segmentTpl === 'function') {
|
|
361
|
+
segmentHtml = segmentTpl(segmentData);
|
|
362
|
+
} else if (typeof segmentTpl === 'string') {
|
|
363
|
+
segmentHtml = segmentTpl
|
|
364
|
+
.replace(/{{segmentType}}/g, segmentData.segmentType)
|
|
365
|
+
.replace(/{{segmentValue}}/g, segmentData.segmentValue)
|
|
366
|
+
.replace(/{{ariaLabel}}/g, segmentData.ariaLabel)
|
|
367
|
+
.replace(/{{ariaValueNow}}/g, segmentData.ariaValueNow)
|
|
368
|
+
.replace(/{{ariaValueText}}/g, segmentData.ariaValueText)
|
|
369
|
+
.replace(/{{ariaValueMin}}/g, segmentData.ariaValueMin)
|
|
370
|
+
.replace(/{{ariaValueMax}}/g, segmentData.ariaValueMax)
|
|
371
|
+
.replace(/{{tabindex}}/g, segmentData.tabindex)
|
|
372
|
+
.replace(/{{contenteditable}}/g, segmentData.contenteditable)
|
|
373
|
+
.replace(/{{class}}/g, ''); // Replace class placeholder with empty string
|
|
374
|
+
} else {
|
|
375
|
+
segmentHtml = '';
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
segmentsHtml += segmentHtml;
|
|
379
|
+
|
|
380
|
+
if (idx < segments.length - 1) {
|
|
381
|
+
// Get appropriate separator between these segments
|
|
382
|
+
const nextSegment = segments[idx + 1];
|
|
383
|
+
const separator = getSeparatorBetweenSegments(segment, nextSegment, options.format);
|
|
384
|
+
|
|
385
|
+
let sepHtml = '';
|
|
386
|
+
if (typeof separatorTpl === 'function') {
|
|
387
|
+
sepHtml = separatorTpl({ separator });
|
|
388
|
+
} else if (typeof separatorTpl === 'string') {
|
|
389
|
+
sepHtml = separatorTpl
|
|
390
|
+
.replace(/{{separator}}/g, separator)
|
|
391
|
+
.replace(/{{class}}/g, ''); // Replace class placeholder with empty string
|
|
392
|
+
} else {
|
|
393
|
+
sepHtml = '';
|
|
394
|
+
}
|
|
395
|
+
segmentsHtml += sepHtml;
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// Render segments directly into container (container already has the correct structure from template)
|
|
400
|
+
container.innerHTML = segmentsHtml;
|
|
401
|
+
|
|
402
|
+
// Verify template rendering was successful
|
|
403
|
+
const segs = Array.from(container.querySelectorAll('[data-segment]')) as HTMLElement[];
|
|
404
|
+
|
|
405
|
+
if (segs.length === 0) {
|
|
406
|
+
throw new Error('Segmented input template rendering failed');
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Re-bind events to all segments
|
|
410
|
+
|
|
411
|
+
segs.forEach((span, idx) => {
|
|
412
|
+
span.addEventListener('keydown', (e) => {
|
|
413
|
+
if (options.disabled || options.readOnly) return;
|
|
414
|
+
// Wrapping navigation
|
|
415
|
+
if (e.key === 'ArrowRight' || (e.key === 'Tab' && !e.shiftKey)) {
|
|
416
|
+
e.preventDefault();
|
|
417
|
+
isArrowNavigation = true; // Set flag to prevent blur onChange
|
|
418
|
+
focusedIdx = (idx + 1) % segments.length;
|
|
419
|
+
caretOffset = null;
|
|
420
|
+
// Update tabindex directly instead of full re-render
|
|
421
|
+
const segs = Array.from(container.querySelectorAll('[data-segment]')) as HTMLElement[];
|
|
422
|
+
segs.forEach((el, i) => el.setAttribute('tabindex', i === focusedIdx ? '0' : '-1'));
|
|
423
|
+
restoreFocus(focusedIdx, caretOffset);
|
|
424
|
+
// Reset flag after a short delay to allow focus to be restored
|
|
425
|
+
setTimeout(() => {
|
|
426
|
+
isArrowNavigation = false;
|
|
427
|
+
}, 10);
|
|
428
|
+
} else if (e.key === 'ArrowLeft' || (e.key === 'Tab' && e.shiftKey)) {
|
|
429
|
+
e.preventDefault();
|
|
430
|
+
isArrowNavigation = true; // Set flag to prevent blur onChange
|
|
431
|
+
focusedIdx = (idx - 1 + segments.length) % segments.length;
|
|
432
|
+
caretOffset = null;
|
|
433
|
+
// Update tabindex directly instead of full re-render
|
|
434
|
+
const segs = Array.from(container.querySelectorAll('[data-segment]')) as HTMLElement[];
|
|
435
|
+
segs.forEach((el, i) => el.setAttribute('tabindex', i === focusedIdx ? '0' : '-1'));
|
|
436
|
+
restoreFocus(focusedIdx, caretOffset);
|
|
437
|
+
// Reset flag after a short delay to allow focus to be restored
|
|
438
|
+
setTimeout(() => {
|
|
439
|
+
isArrowNavigation = false;
|
|
440
|
+
}, 10);
|
|
441
|
+
} else if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
|
|
442
|
+
// Increment/decrement value
|
|
443
|
+
e.preventDefault();
|
|
444
|
+
e.stopPropagation(); // Prevent bubbling to main datepicker
|
|
445
|
+
isArrowNavigation = true; // Set flag to prevent blur onChange
|
|
446
|
+
|
|
447
|
+
let newValue: string;
|
|
448
|
+
|
|
449
|
+
if (segments[idx] === 'ampm') {
|
|
450
|
+
// Handle AM/PM toggle
|
|
451
|
+
const currentAmPm = span.textContent || 'AM';
|
|
452
|
+
if (e.key === 'ArrowUp') {
|
|
453
|
+
newValue = currentAmPm === 'AM' ? 'PM' : 'AM';
|
|
454
|
+
} else {
|
|
455
|
+
newValue = currentAmPm === 'AM' ? 'PM' : 'AM';
|
|
456
|
+
}
|
|
457
|
+
} else {
|
|
458
|
+
// Handle numeric segments
|
|
459
|
+
const min = getSegmentMin(segments[idx], currentValue) ?? 0;
|
|
460
|
+
const max = getSegmentMax(segments[idx], currentValue) ?? 9999;
|
|
461
|
+
let current = Number(getSegmentValue(segments[idx], currentValue)) || min;
|
|
462
|
+
if (e.key === 'ArrowUp') {
|
|
463
|
+
current = Math.min(max, current + 1);
|
|
464
|
+
} else if (e.key === 'ArrowDown') {
|
|
465
|
+
current = Math.max(min, current - 1);
|
|
466
|
+
}
|
|
467
|
+
newValue = current.toString();
|
|
468
|
+
const shouldPad = shouldPadSegment(segments[idx], options.format);
|
|
469
|
+
if (segments[idx] === 'year') {
|
|
470
|
+
if (shouldPad) {
|
|
471
|
+
newValue = newValue.padStart(4, '0');
|
|
472
|
+
}
|
|
473
|
+
} else if (shouldPad) {
|
|
474
|
+
newValue = newValue.padStart(2, '0');
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
span.textContent = newValue;
|
|
479
|
+
currentValue = setSegmentValue(segments[idx], newValue, currentValue);
|
|
480
|
+
|
|
481
|
+
// Set global flag to prevent unified observer from overriding UI
|
|
482
|
+
(window as any).__ktui_segmented_input_arrow_navigation = true;
|
|
483
|
+
|
|
484
|
+
// Call onChange immediately for Arrow Up/Down to update the main datepicker
|
|
485
|
+
if (options.onChange) {
|
|
486
|
+
options.onChange(currentValue);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Clear flag after onChange callback
|
|
490
|
+
setTimeout(() => {
|
|
491
|
+
(window as any).__ktui_segmented_input_arrow_navigation = false;
|
|
492
|
+
}, 50);
|
|
493
|
+
// Preserve caret position at end of content
|
|
494
|
+
if (span.isContentEditable) {
|
|
495
|
+
const range = document.createRange();
|
|
496
|
+
range.selectNodeContents(span);
|
|
497
|
+
range.collapse(false); // place at end
|
|
498
|
+
const sel = window.getSelection();
|
|
499
|
+
if (sel) {
|
|
500
|
+
sel.removeAllRanges();
|
|
501
|
+
sel.addRange(range);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
// Reset flag after a short delay to allow focus to be restored
|
|
505
|
+
setTimeout(() => {
|
|
506
|
+
isArrowNavigation = false;
|
|
507
|
+
}, 10);
|
|
508
|
+
} else if (e.key === 'Enter') {
|
|
509
|
+
// Move to next segment on Enter
|
|
510
|
+
e.preventDefault();
|
|
511
|
+
e.stopPropagation(); // Prevent bubbling to main datepicker
|
|
512
|
+
isArrowNavigation = true; // Set flag to prevent blur onChange
|
|
513
|
+
focusedIdx = (idx + 1) % segments.length;
|
|
514
|
+
caretOffset = null;
|
|
515
|
+
// Update tabindex directly instead of full re-render
|
|
516
|
+
const segs = Array.from(container.querySelectorAll('[data-segment]')) as HTMLElement[];
|
|
517
|
+
segs.forEach((el, i) => el.setAttribute('tabindex', i === focusedIdx ? '0' : '-1'));
|
|
518
|
+
restoreFocus(focusedIdx, caretOffset);
|
|
519
|
+
// Reset flag after a short delay to allow focus to be restored
|
|
520
|
+
setTimeout(() => {
|
|
521
|
+
isArrowNavigation = false;
|
|
522
|
+
}, 10);
|
|
523
|
+
} else if (/^[0-9]$/.test(e.key)) {
|
|
524
|
+
// Direct typing, enforce min/max - optimized to avoid focus loss
|
|
525
|
+
e.preventDefault();
|
|
526
|
+
let newValue: string;
|
|
527
|
+
const currentText = span.textContent || '';
|
|
528
|
+
|
|
529
|
+
if (segments[idx] === 'year') {
|
|
530
|
+
// For year: shift left and append new digit (e.g., "2024" + "6" = "0246", then becomes "2026" after validation)
|
|
531
|
+
// If already 4 digits, shift left by removing first digit and appending new one
|
|
532
|
+
if (currentText.length === 4) {
|
|
533
|
+
newValue = (currentText.slice(1) + e.key);
|
|
534
|
+
} else {
|
|
535
|
+
newValue = (currentText + e.key).slice(-4);
|
|
536
|
+
}
|
|
537
|
+
} else {
|
|
538
|
+
// For day/month: shift left and append new digit (e.g., "12" + "5" = "25")
|
|
539
|
+
// If already 2 digits, shift left by removing first digit and appending new one
|
|
540
|
+
if (currentText.length === 2) {
|
|
541
|
+
newValue = (currentText.slice(1) + e.key);
|
|
542
|
+
} else {
|
|
543
|
+
newValue = (currentText + e.key).slice(-2);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const min = getSegmentMin(segments[idx], currentValue) ?? 0;
|
|
548
|
+
const max = getSegmentMax(segments[idx], currentValue) ?? (segments[idx] === 'year' ? 9999 : 99);
|
|
549
|
+
let num = Math.max(min, Math.min(max, Number(newValue)));
|
|
550
|
+
if (isNaN(num)) num = min;
|
|
551
|
+
|
|
552
|
+
// Update content directly without re-rendering to preserve focus
|
|
553
|
+
const shouldPad = shouldPadSegment(segments[idx], options.format);
|
|
554
|
+
if (segments[idx] === 'year') {
|
|
555
|
+
span.textContent = shouldPad ? num.toString().padStart(4, '0') : num.toString();
|
|
556
|
+
} else {
|
|
557
|
+
span.textContent = shouldPad ? num.toString().padStart(2, '0') : num.toString();
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Update internal value
|
|
561
|
+
currentValue = setSegmentValue(segments[idx], span.textContent || '', currentValue);
|
|
562
|
+
|
|
563
|
+
// Debounce onChange to avoid excessive updates during rapid typing
|
|
564
|
+
if (options.onChange) {
|
|
565
|
+
clearTimeout((span as any)._onChangeTimeout);
|
|
566
|
+
(span as any)._onChangeTimeout = setTimeout(() => {
|
|
567
|
+
options.onChange!(currentValue);
|
|
568
|
+
}, 150); // 150ms debounce
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Preserve caret position at end of content
|
|
572
|
+
if (span.isContentEditable) {
|
|
573
|
+
const range = document.createRange();
|
|
574
|
+
range.selectNodeContents(span);
|
|
575
|
+
range.collapse(false); // place at end
|
|
576
|
+
const sel = window.getSelection();
|
|
577
|
+
if (sel) {
|
|
578
|
+
sel.removeAllRanges();
|
|
579
|
+
sel.addRange(range);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
// Focus/blur styling (no classes, just ARIA/tabindex)
|
|
585
|
+
span.addEventListener('focus', () => {
|
|
586
|
+
span.setAttribute('tabindex', '0');
|
|
587
|
+
focusedIdx = idx;
|
|
588
|
+
});
|
|
589
|
+
span.addEventListener('blur', () => {
|
|
590
|
+
span.setAttribute('tabindex', '-1');
|
|
591
|
+
// Clear any pending debounced onChange calls
|
|
592
|
+
if ((span as any)._onChangeTimeout) {
|
|
593
|
+
clearTimeout((span as any)._onChangeTimeout);
|
|
594
|
+
(span as any)._onChangeTimeout = null;
|
|
595
|
+
}
|
|
596
|
+
// Call onChange when user finishes editing to update the main datepicker
|
|
597
|
+
// But not during Arrow Up/Down navigation and only if value actually changed
|
|
598
|
+
if (options.onChange && !isArrowNavigation) {
|
|
599
|
+
const updatedValue = setSegmentValue(segments[idx], span.textContent || '', currentValue);
|
|
600
|
+
// Only call onChange if the value has actually changed
|
|
601
|
+
if (updatedValue.getTime() !== currentValue.getTime()) {
|
|
602
|
+
options.onChange(updatedValue);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
// Mouse click focuses segment (optimized to avoid unnecessary re-rendering)
|
|
607
|
+
span.addEventListener('mousedown', (e) => {
|
|
608
|
+
e.preventDefault();
|
|
609
|
+
focusedIdx = idx;
|
|
610
|
+
caretOffset = null;
|
|
611
|
+
// Only update tabindex and focus, no full re-render
|
|
612
|
+
const segs = Array.from(container.querySelectorAll('[data-segment]')) as HTMLElement[];
|
|
613
|
+
segs.forEach((el, i) => el.setAttribute('tabindex', i === focusedIdx ? '0' : '-1'));
|
|
614
|
+
restoreFocus(focusedIdx, caretOffset);
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
// After rendering, restore focus to the correct segment and caret (skip on initial render)
|
|
618
|
+
if (!isInitialRender) {
|
|
619
|
+
restoreFocus(focusedIdx, caretOffset);
|
|
620
|
+
}
|
|
621
|
+
isInitialRender = false;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// --- Initial render ---
|
|
625
|
+
render();
|
|
626
|
+
|
|
627
|
+
// --- Cleanup function ---
|
|
628
|
+
return () => {
|
|
629
|
+
// Clear any pending debounced onChange calls
|
|
630
|
+
const segs = Array.from(container.querySelectorAll('[data-segment]')) as HTMLElement[];
|
|
631
|
+
segs.forEach(span => {
|
|
632
|
+
if ((span as any)._onChangeTimeout) {
|
|
633
|
+
clearTimeout((span as any)._onChangeTimeout);
|
|
634
|
+
}
|
|
635
|
+
});
|
|
636
|
+
container.innerHTML = '';
|
|
637
|
+
};
|
|
638
|
+
}
|