tyrell-components 1.0.0-RC13 → 1.0.0-RC15
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 +10 -2
- package/css/tyrell-theme.css +192 -180
- package/css/tyrell.css +23 -0
- package/dist/tyrell-colors-static.css +1 -316
- package/dist/tyrell-theme.css +1 -1454
- package/dist/tyrell.css +1 -1371
- package/dist/tyrell.js +1 -1
- package/lib/base/ty-component.d.ts +20 -10
- package/lib/base/ty-component.js +196 -319
- package/lib/components/button.d.ts +3 -17
- package/lib/components/button.js +261 -295
- package/lib/components/calendar-month.d.ts +4 -50
- package/lib/components/calendar-month.js +443 -582
- package/lib/components/calendar-navigation.d.ts +3 -48
- package/lib/components/calendar-navigation.js +239 -362
- package/lib/components/calendar.d.ts +8 -83
- package/lib/components/calendar.js +616 -829
- package/lib/components/checkbox.d.ts +5 -72
- package/lib/components/checkbox.js +273 -416
- package/lib/components/copy.d.ts +4 -57
- package/lib/components/copy.js +280 -397
- package/lib/components/date-picker.d.ts +21 -163
- package/lib/components/date-picker.js +1143 -1674
- package/lib/components/file-upload.d.ts +9 -15
- package/lib/components/file-upload.js +404 -426
- package/lib/components/icon.d.ts +3 -24
- package/lib/components/icon.js +141 -230
- package/lib/components/input.d.ts +8 -108
- package/lib/components/input.js +593 -784
- package/lib/components/modal.d.ts +17 -26
- package/lib/components/modal.js +340 -484
- package/lib/components/option.d.ts +2 -9
- package/lib/components/option.js +168 -216
- package/lib/components/popup.d.ts +2 -17
- package/lib/components/popup.js +249 -380
- package/lib/components/radio.d.ts +5 -25
- package/lib/components/radio.js +407 -461
- package/lib/components/resize-observer.d.ts +3 -30
- package/lib/components/resize-observer.js +69 -103
- package/lib/components/scroll-container.d.ts +2 -12
- package/lib/components/scroll-container.js +293 -338
- package/lib/components/select.d.ts +87 -159
- package/lib/components/select.js +1609 -2090
- package/lib/components/selected-tags.d.ts +5 -48
- package/lib/components/selected-tags.js +188 -276
- package/lib/components/step.d.ts +1 -6
- package/lib/components/step.js +62 -68
- package/lib/components/switch.d.ts +1 -12
- package/lib/components/switch.js +214 -247
- package/lib/components/tab.d.ts +1 -6
- package/lib/components/tab.js +62 -69
- package/lib/components/tabs.d.ts +6 -63
- package/lib/components/tabs.js +545 -777
- package/lib/components/tag.d.ts +3 -37
- package/lib/components/tag.js +236 -335
- package/lib/components/textarea.d.ts +7 -87
- package/lib/components/textarea.js +457 -627
- package/lib/components/tooltip.d.ts +3 -11
- package/lib/components/tooltip.js +307 -443
- package/lib/components/wizard.d.ts +6 -54
- package/lib/components/wizard.js +405 -618
- package/lib/icons/fontawesome/brands.d.ts +0 -1
- package/lib/icons/fontawesome/brands.js +0 -8
- package/lib/icons/fontawesome/regular.d.ts +0 -1
- package/lib/icons/fontawesome/regular.js +0 -8
- package/lib/icons/fontawesome/solid.d.ts +0 -1
- package/lib/icons/fontawesome/solid.js +0 -8
- package/lib/icons/heroicons/micro.d.ts +0 -1
- package/lib/icons/heroicons/micro.js +392 -400
- package/lib/icons/heroicons/mini.d.ts +0 -1
- package/lib/icons/heroicons/mini.js +382 -390
- package/lib/icons/heroicons/outline.d.ts +0 -1
- package/lib/icons/heroicons/outline.js +337 -345
- package/lib/icons/heroicons/solid.d.ts +0 -1
- package/lib/icons/heroicons/solid.js +407 -415
- package/lib/icons/lucide.d.ts +0 -1
- package/lib/icons/lucide.js +24479 -26351
- package/lib/icons/material/filled.d.ts +0 -1
- package/lib/icons/material/filled.js +9653 -9663
- package/lib/icons/material/outlined.d.ts +0 -1
- package/lib/icons/material/outlined.js +10271 -10281
- package/lib/icons/material/round.d.ts +0 -1
- package/lib/icons/material/round.js +10495 -10505
- package/lib/icons/material/sharp.d.ts +0 -1
- package/lib/icons/material/sharp.js +9905 -9915
- package/lib/icons/material/two-tone.d.ts +0 -1
- package/lib/icons/material/two-tone.js +12792 -12802
- package/lib/index.d.ts +0 -1
- package/lib/index.js +21 -41
- package/lib/styles/button.d.ts +2 -20
- package/lib/styles/button.js +202 -404
- package/lib/styles/calendar-month.d.ts +1 -5
- package/lib/styles/calendar-month.js +127 -244
- package/lib/styles/calendar-navigation.d.ts +2 -6
- package/lib/styles/calendar-navigation.js +64 -104
- package/lib/styles/calendar.d.ts +1 -2
- package/lib/styles/calendar.js +9 -22
- package/lib/styles/checkbox.d.ts +0 -1
- package/lib/styles/checkbox.js +45 -94
- package/lib/styles/copy.d.ts +0 -1
- package/lib/styles/copy.js +67 -106
- package/lib/styles/custom-scrollbar.d.ts +1 -2
- package/lib/styles/custom-scrollbar.js +80 -115
- package/lib/styles/date-picker.d.ts +1 -5
- package/lib/styles/date-picker.js +261 -335
- package/lib/styles/field-size.d.ts +6 -0
- package/lib/styles/field-size.js +91 -0
- package/lib/styles/file-upload.d.ts +1 -2
- package/lib/styles/file-upload.js +143 -188
- package/lib/styles/icon.d.ts +2 -6
- package/lib/styles/icon.js +92 -170
- package/lib/styles/input.d.ts +1 -6
- package/lib/styles/input.js +125 -290
- package/lib/styles/modal.d.ts +1 -2
- package/lib/styles/modal.js +71 -108
- package/lib/styles/option.d.ts +2 -6
- package/lib/styles/option.js +49 -64
- package/lib/styles/popup.d.ts +1 -2
- package/lib/styles/popup.js +42 -77
- package/lib/styles/radio.d.ts +0 -1
- package/lib/styles/radio.js +69 -110
- package/lib/styles/resize-observer.d.ts +1 -2
- package/lib/styles/resize-observer.js +2 -12
- package/lib/styles/scroll-container.d.ts +1 -2
- package/lib/styles/scroll-container.js +100 -149
- package/lib/styles/select-base.d.ts +0 -1
- package/lib/styles/select-base.js +597 -785
- package/lib/styles/select.d.ts +1 -2
- package/lib/styles/select.js +140 -236
- package/lib/styles/step.d.ts +1 -2
- package/lib/styles/step.js +22 -33
- package/lib/styles/switch.d.ts +0 -1
- package/lib/styles/switch.js +34 -73
- package/lib/styles/tab.d.ts +1 -2
- package/lib/styles/tab.js +22 -34
- package/lib/styles/tabs.d.ts +3 -11
- package/lib/styles/tabs.js +190 -222
- package/lib/styles/tag.d.ts +1 -5
- package/lib/styles/tag.js +136 -218
- package/lib/styles/textarea.d.ts +1 -2
- package/lib/styles/textarea.js +210 -319
- package/lib/styles/tooltip.d.ts +0 -1
- package/lib/styles/tooltip.js +3 -13
- package/lib/styles/wizard.d.ts +5 -21
- package/lib/styles/wizard.js +170 -306
- package/lib/types/common.d.ts +2 -2
- package/lib/types/common.js +6 -17
- package/lib/utils/calendar-utils.d.ts +4 -21
- package/lib/utils/calendar-utils.js +100 -273
- package/lib/utils/custom-scrollbar.d.ts +6 -20
- package/lib/utils/custom-scrollbar.js +273 -317
- package/lib/utils/flavor-sheet.d.ts +0 -1
- package/lib/utils/flavor-sheet.js +14 -37
- package/lib/utils/icon-registry.d.ts +0 -2
- package/lib/utils/icon-registry.js +137 -243
- package/lib/utils/icons.d.ts +0 -1
- package/lib/utils/icons.js +0 -9
- package/lib/utils/loader-registry.d.ts +0 -1
- package/lib/utils/loader-registry.js +3 -35
- package/lib/utils/locale.d.ts +2 -23
- package/lib/utils/locale.js +34 -120
- package/lib/utils/mobile.d.ts +0 -1
- package/lib/utils/mobile.js +1 -18
- package/lib/utils/number-format.d.ts +6 -3
- package/lib/utils/number-format.js +54 -110
- package/lib/utils/positioning.d.ts +50 -7
- package/lib/utils/positioning.js +224 -234
- package/lib/utils/property-capture.d.ts +5 -27
- package/lib/utils/property-capture.js +36 -144
- package/lib/utils/property-manager.d.ts +0 -1
- package/lib/utils/property-manager.js +126 -196
- package/lib/utils/resize-observer.d.ts +0 -1
- package/lib/utils/resize-observer.js +23 -59
- package/lib/utils/scroll-lock.d.ts +4 -11
- package/lib/utils/scroll-lock.js +65 -172
- package/lib/utils/styles.d.ts +0 -1
- package/lib/utils/styles.js +17 -41
- package/lib/version.d.ts +1 -2
- package/lib/version.js +1 -11
- package/package.json +4 -4
- package/lib/base/ty-component.d.ts.map +0 -1
- package/lib/base/ty-component.js.map +0 -1
- package/lib/components/button.d.ts.map +0 -1
- package/lib/components/button.js.map +0 -1
- package/lib/components/calendar-month.d.ts.map +0 -1
- package/lib/components/calendar-month.js.map +0 -1
- package/lib/components/calendar-navigation.d.ts.map +0 -1
- package/lib/components/calendar-navigation.js.map +0 -1
- package/lib/components/calendar.d.ts.map +0 -1
- package/lib/components/calendar.js.map +0 -1
- package/lib/components/checkbox.d.ts.map +0 -1
- package/lib/components/checkbox.js.map +0 -1
- package/lib/components/copy.d.ts.map +0 -1
- package/lib/components/copy.js.map +0 -1
- package/lib/components/date-picker.d.ts.map +0 -1
- package/lib/components/date-picker.js.map +0 -1
- package/lib/components/file-upload.d.ts.map +0 -1
- package/lib/components/file-upload.js.map +0 -1
- package/lib/components/icon.d.ts.map +0 -1
- package/lib/components/icon.js.map +0 -1
- package/lib/components/input.d.ts.map +0 -1
- package/lib/components/input.js.map +0 -1
- package/lib/components/modal.d.ts.map +0 -1
- package/lib/components/modal.js.map +0 -1
- package/lib/components/option.d.ts.map +0 -1
- package/lib/components/option.js.map +0 -1
- package/lib/components/popup.d.ts.map +0 -1
- package/lib/components/popup.js.map +0 -1
- package/lib/components/radio.d.ts.map +0 -1
- package/lib/components/radio.js.map +0 -1
- package/lib/components/resize-observer.d.ts.map +0 -1
- package/lib/components/resize-observer.js.map +0 -1
- package/lib/components/scroll-container.d.ts.map +0 -1
- package/lib/components/scroll-container.js.map +0 -1
- package/lib/components/select.d.ts.map +0 -1
- package/lib/components/select.js.map +0 -1
- package/lib/components/selected-tags.d.ts.map +0 -1
- package/lib/components/selected-tags.js.map +0 -1
- package/lib/components/step.d.ts.map +0 -1
- package/lib/components/step.js.map +0 -1
- package/lib/components/switch.d.ts.map +0 -1
- package/lib/components/switch.js.map +0 -1
- package/lib/components/tab.d.ts.map +0 -1
- package/lib/components/tab.js.map +0 -1
- package/lib/components/tabs.d.ts.map +0 -1
- package/lib/components/tabs.js.map +0 -1
- package/lib/components/tag.d.ts.map +0 -1
- package/lib/components/tag.js.map +0 -1
- package/lib/components/textarea.d.ts.map +0 -1
- package/lib/components/textarea.js.map +0 -1
- package/lib/components/tooltip.d.ts.map +0 -1
- package/lib/components/tooltip.js.map +0 -1
- package/lib/components/wizard.d.ts.map +0 -1
- package/lib/components/wizard.js.map +0 -1
- package/lib/icons/fontawesome/brands.d.ts.map +0 -1
- package/lib/icons/fontawesome/brands.js.map +0 -1
- package/lib/icons/fontawesome/regular.d.ts.map +0 -1
- package/lib/icons/fontawesome/regular.js.map +0 -1
- package/lib/icons/fontawesome/solid.d.ts.map +0 -1
- package/lib/icons/fontawesome/solid.js.map +0 -1
- package/lib/icons/heroicons/micro.d.ts.map +0 -1
- package/lib/icons/heroicons/micro.js.map +0 -1
- package/lib/icons/heroicons/mini.d.ts.map +0 -1
- package/lib/icons/heroicons/mini.js.map +0 -1
- package/lib/icons/heroicons/outline.d.ts.map +0 -1
- package/lib/icons/heroicons/outline.js.map +0 -1
- package/lib/icons/heroicons/solid.d.ts.map +0 -1
- package/lib/icons/heroicons/solid.js.map +0 -1
- package/lib/icons/lucide.d.ts.map +0 -1
- package/lib/icons/lucide.js.map +0 -1
- package/lib/icons/material/filled.d.ts.map +0 -1
- package/lib/icons/material/filled.js.map +0 -1
- package/lib/icons/material/outlined.d.ts.map +0 -1
- package/lib/icons/material/outlined.js.map +0 -1
- package/lib/icons/material/round.d.ts.map +0 -1
- package/lib/icons/material/round.js.map +0 -1
- package/lib/icons/material/sharp.d.ts.map +0 -1
- package/lib/icons/material/sharp.js.map +0 -1
- package/lib/icons/material/two-tone.d.ts.map +0 -1
- package/lib/icons/material/two-tone.js.map +0 -1
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js.map +0 -1
- package/lib/styles/button.d.ts.map +0 -1
- package/lib/styles/button.js.map +0 -1
- package/lib/styles/calendar-month.d.ts.map +0 -1
- package/lib/styles/calendar-month.js.map +0 -1
- package/lib/styles/calendar-navigation.d.ts.map +0 -1
- package/lib/styles/calendar-navigation.js.map +0 -1
- package/lib/styles/calendar.d.ts.map +0 -1
- package/lib/styles/calendar.js.map +0 -1
- package/lib/styles/checkbox.d.ts.map +0 -1
- package/lib/styles/checkbox.js.map +0 -1
- package/lib/styles/copy.d.ts.map +0 -1
- package/lib/styles/copy.js.map +0 -1
- package/lib/styles/custom-scrollbar.d.ts.map +0 -1
- package/lib/styles/custom-scrollbar.js.map +0 -1
- package/lib/styles/date-picker.d.ts.map +0 -1
- package/lib/styles/date-picker.js.map +0 -1
- package/lib/styles/file-upload.d.ts.map +0 -1
- package/lib/styles/file-upload.js.map +0 -1
- package/lib/styles/icon.d.ts.map +0 -1
- package/lib/styles/icon.js.map +0 -1
- package/lib/styles/input.d.ts.map +0 -1
- package/lib/styles/input.js.map +0 -1
- package/lib/styles/modal.d.ts.map +0 -1
- package/lib/styles/modal.js.map +0 -1
- package/lib/styles/multiselect.d.ts +0 -8
- package/lib/styles/multiselect.d.ts.map +0 -1
- package/lib/styles/multiselect.js +0 -849
- package/lib/styles/multiselect.js.map +0 -1
- package/lib/styles/option.d.ts.map +0 -1
- package/lib/styles/option.js.map +0 -1
- package/lib/styles/popup.d.ts.map +0 -1
- package/lib/styles/popup.js.map +0 -1
- package/lib/styles/radio.d.ts.map +0 -1
- package/lib/styles/radio.js.map +0 -1
- package/lib/styles/resize-observer.d.ts.map +0 -1
- package/lib/styles/resize-observer.js.map +0 -1
- package/lib/styles/scroll-container.d.ts.map +0 -1
- package/lib/styles/scroll-container.js.map +0 -1
- package/lib/styles/select-base.d.ts.map +0 -1
- package/lib/styles/select-base.js.map +0 -1
- package/lib/styles/select.d.ts.map +0 -1
- package/lib/styles/select.js.map +0 -1
- package/lib/styles/step.d.ts.map +0 -1
- package/lib/styles/step.js.map +0 -1
- package/lib/styles/switch.d.ts.map +0 -1
- package/lib/styles/switch.js.map +0 -1
- package/lib/styles/tab.d.ts.map +0 -1
- package/lib/styles/tab.js.map +0 -1
- package/lib/styles/tabs.d.ts.map +0 -1
- package/lib/styles/tabs.js.map +0 -1
- package/lib/styles/tag.d.ts.map +0 -1
- package/lib/styles/tag.js.map +0 -1
- package/lib/styles/textarea.d.ts.map +0 -1
- package/lib/styles/textarea.js.map +0 -1
- package/lib/styles/tooltip.d.ts.map +0 -1
- package/lib/styles/tooltip.js.map +0 -1
- package/lib/styles/wizard.d.ts.map +0 -1
- package/lib/styles/wizard.js.map +0 -1
- package/lib/types/common.d.ts.map +0 -1
- package/lib/types/common.js.map +0 -1
- package/lib/utils/calendar-utils.d.ts.map +0 -1
- package/lib/utils/calendar-utils.js.map +0 -1
- package/lib/utils/custom-scrollbar.d.ts.map +0 -1
- package/lib/utils/custom-scrollbar.js.map +0 -1
- package/lib/utils/flavor-sheet.d.ts.map +0 -1
- package/lib/utils/flavor-sheet.js.map +0 -1
- package/lib/utils/icon-registry.d.ts.map +0 -1
- package/lib/utils/icon-registry.js.map +0 -1
- package/lib/utils/icons.d.ts.map +0 -1
- package/lib/utils/icons.js.map +0 -1
- package/lib/utils/loader-registry.d.ts.map +0 -1
- package/lib/utils/loader-registry.js.map +0 -1
- package/lib/utils/locale.d.ts.map +0 -1
- package/lib/utils/locale.js.map +0 -1
- package/lib/utils/mobile.d.ts.map +0 -1
- package/lib/utils/mobile.js.map +0 -1
- package/lib/utils/number-format.d.ts.map +0 -1
- package/lib/utils/number-format.js.map +0 -1
- package/lib/utils/parse-boolean.d.ts +0 -39
- package/lib/utils/parse-boolean.d.ts.map +0 -1
- package/lib/utils/parse-boolean.js +0 -58
- package/lib/utils/parse-boolean.js.map +0 -1
- package/lib/utils/positioning.d.ts.map +0 -1
- package/lib/utils/positioning.js.map +0 -1
- package/lib/utils/property-capture.d.ts.map +0 -1
- package/lib/utils/property-capture.js.map +0 -1
- package/lib/utils/property-manager.d.ts.map +0 -1
- package/lib/utils/property-manager.js.map +0 -1
- package/lib/utils/resize-observer.d.ts.map +0 -1
- package/lib/utils/resize-observer.js.map +0 -1
- package/lib/utils/scroll-lock.d.ts.map +0 -1
- package/lib/utils/scroll-lock.js.map +0 -1
- package/lib/utils/styles.d.ts.map +0 -1
- package/lib/utils/styles.js.map +0 -1
- package/lib/version.d.ts.map +0 -1
- package/lib/version.js.map +0 -1
|
@@ -1,1705 +1,1174 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TyDatePicker Web Component
|
|
3
|
-
* PORTED FROM: cljs/ty/components/date_picker.cljs
|
|
4
|
-
*
|
|
5
|
-
* A date picker component with read-only input and calendar dropdown.
|
|
6
|
-
* Supports date-only and date+time modes with smart time input.
|
|
7
|
-
*
|
|
8
|
-
* Architecture:
|
|
9
|
-
* - Read-only input stub (displays formatted date)
|
|
10
|
-
* - Calendar dropdown (modal dialog with ty-calendar)
|
|
11
|
-
* - Optional time input (with smart digit navigation)
|
|
12
|
-
* - Form participation via ElementInternals
|
|
13
|
-
* - UTC output, local display
|
|
14
|
-
*
|
|
15
|
-
* Features:
|
|
16
|
-
* - Date selection with calendar dropdown
|
|
17
|
-
* - Optional time input with smart navigation
|
|
18
|
-
* - Form integration (works with FormData)
|
|
19
|
-
* - UTC value output for server communication
|
|
20
|
-
* - Localized display formatting (Intl API)
|
|
21
|
-
* - Clearable with clear button
|
|
22
|
-
* - Keyboard navigation (Escape to close)
|
|
23
|
-
* - Outside click handling
|
|
24
|
-
*
|
|
25
|
-
* @example
|
|
26
|
-
* ```html
|
|
27
|
-
* <!-- Basic date picker -->
|
|
28
|
-
* <ty-date-picker
|
|
29
|
-
* label="Select Date"
|
|
30
|
-
* placeholder="Choose a date">
|
|
31
|
-
* </ty-date-picker>
|
|
32
|
-
*
|
|
33
|
-
* <!-- With time -->
|
|
34
|
-
* <ty-date-picker
|
|
35
|
-
* with-time="true"
|
|
36
|
-
* label="Select Date & Time">
|
|
37
|
-
* </ty-date-picker>
|
|
38
|
-
*
|
|
39
|
-
* <!-- Form integration -->
|
|
40
|
-
* <form>
|
|
41
|
-
* <ty-date-picker name="booking-date"></ty-date-picker>
|
|
42
|
-
* <button type="submit">Submit</button>
|
|
43
|
-
* </form>
|
|
44
|
-
*
|
|
45
|
-
* <!-- Pre-filled value (UTC) -->
|
|
46
|
-
* <ty-date-picker value="2024-09-21T08:30:00.000Z"></ty-date-picker>
|
|
47
|
-
* ```
|
|
48
|
-
*/
|
|
49
1
|
import { ensureStyles } from '../utils/styles.js';
|
|
50
2
|
import { syncCustomFlavorSheet } from '../utils/flavor-sheet.js';
|
|
51
3
|
import { datePickerStyles, datePickerCustomFlavorCss } from '../styles/date-picker.js';
|
|
52
4
|
import { lockScroll, unlockScroll } from '../utils/scroll-lock.js';
|
|
53
|
-
import { computeAnchoredPosition } from '../utils/positioning.js';
|
|
5
|
+
import { computeAnchoredPosition, placementToAnchored } from '../utils/positioning.js';
|
|
54
6
|
import { getEffectiveLocale, observeLocaleChanges } from '../utils/locale.js';
|
|
55
7
|
import { isMobileTouch } from '../utils/mobile.js';
|
|
56
8
|
import { TyComponent } from '../base/ty-component.js';
|
|
57
|
-
|
|
58
|
-
// Constants
|
|
59
|
-
// ============================================================================
|
|
9
|
+
import { DEFAULT_SIZE } from '../types/common.js';
|
|
60
10
|
const CALENDAR_ICON_SVG = `<svg stroke='currentColor' fill='none' stroke-width='2' viewBox='0 0 24 24' width='16' height='16' xmlns='http://www.w3.org/2000/svg'><rect x='3' y='4' width='18' height='18' rx='2' ry='2'></rect><line x1='16' y1='2' x2='16' y2='6'></line><line x1='8' y1='2' x2='8' y2='6'></line><line x1='3' y1='10' x2='21' y2='10'></line></svg>`;
|
|
61
11
|
const CLEAR_ICON_SVG = `<svg stroke='currentColor' fill='none' stroke-width='2' viewBox='0 0 24 24' width='14' height='14' xmlns='http://www.w3.org/2000/svg'><line x1='18' y1='6' x2='6' y2='18'></line><line x1='6' y1='6' x2='18' y2='18'></line></svg>`;
|
|
62
12
|
const SCHEDULE_ICON_SVG = `<svg stroke='currentColor' fill='none' stroke-width='2' viewBox='0 0 24 24' width='16' height='16' xmlns='http://www.w3.org/2000/svg'><circle cx='12' cy='12' r='10'></circle><polyline points='12,6 12,12 16,14'></polyline></svg>`;
|
|
63
|
-
// Stable per-instance id (survives across full-rebuild re-renders, unlike a
|
|
64
|
-
// fresh Math.random() per render) — used to wire the stub's aria-labelledby
|
|
65
|
-
// to the label element it's paired with. Same pattern as select.ts's
|
|
66
|
-
// getElementHash.
|
|
67
13
|
const elementIds = new WeakMap();
|
|
68
14
|
let elementIdCounter = 0;
|
|
69
15
|
function getElementHash(element) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
// ============================================================================
|
|
78
|
-
// Helper Functions - Date Parsing & Conversion
|
|
79
|
-
// ============================================================================
|
|
80
|
-
/**
|
|
81
|
-
* Parse ANY input format into year/month/day/hour/minute components.
|
|
82
|
-
* Accepts:
|
|
83
|
-
* - UTC strings: '2024-09-21T08:30:00Z' or '2024-09-21T08:30:00.000Z'
|
|
84
|
-
* - Datetime-local: '2024-09-21T10:30'
|
|
85
|
-
* - Date only: '2024-09-21'
|
|
86
|
-
* - Timestamps: milliseconds since epoch
|
|
87
|
-
* - With timezone: '2024-09-21T10:30:00+02:00'
|
|
88
|
-
*
|
|
89
|
-
* Always extracts components in LOCAL timezone for display/editing.
|
|
90
|
-
*
|
|
91
|
-
* PORTED FROM: parse-value in date_picker.cljs
|
|
92
|
-
*/
|
|
16
|
+
let id = elementIds.get(element);
|
|
17
|
+
if (id === undefined) {
|
|
18
|
+
id = ++elementIdCounter;
|
|
19
|
+
elementIds.set(element, id);
|
|
20
|
+
}
|
|
21
|
+
return id;
|
|
22
|
+
}
|
|
93
23
|
function parseValue(value, withTime) {
|
|
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
|
-
hour: withTime ? dateObj.getHours() : 0,
|
|
125
|
-
minute: withTime ? dateObj.getMinutes() : 0,
|
|
126
|
-
};
|
|
127
|
-
return components;
|
|
128
|
-
}
|
|
129
|
-
/**
|
|
130
|
-
* Convert internal components to Date object.
|
|
131
|
-
*
|
|
132
|
-
* PORTED FROM: components->date-object in date_picker.cljs
|
|
133
|
-
*/
|
|
24
|
+
if (!value)
|
|
25
|
+
return null;
|
|
26
|
+
let dateObj = null;
|
|
27
|
+
if (typeof value === 'string') {
|
|
28
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
29
|
+
const [yearStr, monthStr, dayStr] = value.split('-');
|
|
30
|
+
const year = parseInt(yearStr, 10);
|
|
31
|
+
const month = parseInt(monthStr, 10) - 1;
|
|
32
|
+
const day = parseInt(dayStr, 10);
|
|
33
|
+
dateObj = new Date(year, month, day, 0, 0, 0, 0);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
dateObj = new Date(value);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
else if (typeof value === 'number') {
|
|
40
|
+
dateObj = new Date(value);
|
|
41
|
+
}
|
|
42
|
+
if (!dateObj || isNaN(dateObj.getTime())) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const components = {
|
|
46
|
+
year: dateObj.getFullYear(),
|
|
47
|
+
month: dateObj.getMonth() + 1,
|
|
48
|
+
day: dateObj.getDate(),
|
|
49
|
+
hour: withTime ? dateObj.getHours() : 0,
|
|
50
|
+
minute: withTime ? dateObj.getMinutes() : 0,
|
|
51
|
+
};
|
|
52
|
+
return components;
|
|
53
|
+
}
|
|
134
54
|
function componentsToDateObject(components) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
day, hour || 0, minute || 0, 0, // seconds
|
|
141
|
-
0 // milliseconds
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
/**
|
|
145
|
-
* Convert internal components to UTC ISO 8601 format.
|
|
146
|
-
*
|
|
147
|
-
* For date+time mode: Outputs full UTC timestamp with milliseconds
|
|
148
|
-
* Example: '2024-09-21T08:30:00.000Z'
|
|
149
|
-
*
|
|
150
|
-
* For date-only mode: Outputs UTC timestamp at midnight local time
|
|
151
|
-
* Example: '2024-09-20T22:00:00.000Z' (midnight Sept 21 CEST = 10pm Sept 20 UTC)
|
|
152
|
-
*
|
|
153
|
-
* Always returns UTC to ensure unambiguous server communication.
|
|
154
|
-
*
|
|
155
|
-
* PORTED FROM: components->output-value in date_picker.cljs
|
|
156
|
-
*/
|
|
55
|
+
const { year, month, day, hour, minute } = components;
|
|
56
|
+
if (!year || !month || !day)
|
|
57
|
+
return null;
|
|
58
|
+
return new Date(year, month - 1, day, hour || 0, minute || 0, 0, 0);
|
|
59
|
+
}
|
|
157
60
|
function componentsToOutputValue(components) {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* Convert internal components to local datetime-local format.
|
|
166
|
-
*
|
|
167
|
-
* For date+time mode: Outputs local datetime without timezone
|
|
168
|
-
* Example: '2024-09-21T10:30'
|
|
169
|
-
*
|
|
170
|
-
* For date-only mode: Outputs date only
|
|
171
|
-
* Example: '2024-09-21'
|
|
172
|
-
*
|
|
173
|
-
* This format matches HTML5 <input type="datetime-local"> and is useful
|
|
174
|
-
* for setting other inputs or displaying local time without timezone conversion.
|
|
175
|
-
*/
|
|
61
|
+
if (!components.year || !components.month || !components.day) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const dateObj = componentsToDateObject(components);
|
|
65
|
+
return dateObj ? dateObj.toISOString() : null;
|
|
66
|
+
}
|
|
176
67
|
function componentsToLocalValue(components, withTime) {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
/**
|
|
193
|
-
* Format components for display in input using Intl API.
|
|
194
|
-
*
|
|
195
|
-
* PORTED FROM: format-display-value in date_picker.cljs
|
|
196
|
-
*/
|
|
68
|
+
if (!components.year || !components.month || !components.day) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const year = components.year.toString().padStart(4, '0');
|
|
72
|
+
const month = components.month.toString().padStart(2, '0');
|
|
73
|
+
const day = components.day.toString().padStart(2, '0');
|
|
74
|
+
if (withTime) {
|
|
75
|
+
const hour = (components.hour || 0).toString().padStart(2, '0');
|
|
76
|
+
const minute = (components.minute || 0).toString().padStart(2, '0');
|
|
77
|
+
return `${year}-${month}-${day}T${hour}:${minute}`;
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
return `${year}-${month}-${day}`;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
197
83
|
function formatDisplayValue(components, formatType, locale, withTime) {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
}
|
|
214
|
-
// ============================================================================
|
|
215
|
-
// Helper Functions - Time Input
|
|
216
|
-
// ============================================================================
|
|
217
|
-
/**
|
|
218
|
-
* Parse hour and minute from raw digits (4 chars: "HHmm")
|
|
219
|
-
*
|
|
220
|
-
* PORTED FROM: parse-time-components in date_picker.cljs
|
|
221
|
-
*/
|
|
84
|
+
if (!components.year || !components.month || !components.day) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
const dateObj = componentsToDateObject(components);
|
|
88
|
+
if (!dateObj)
|
|
89
|
+
return null;
|
|
90
|
+
const options = {
|
|
91
|
+
dateStyle: formatType || 'long',
|
|
92
|
+
};
|
|
93
|
+
if (withTime) {
|
|
94
|
+
options.timeStyle = 'short';
|
|
95
|
+
}
|
|
96
|
+
const formatter = new Intl.DateTimeFormat(locale, options);
|
|
97
|
+
return formatter.format(dateObj);
|
|
98
|
+
}
|
|
222
99
|
function parseTimeComponents(rawDigits) {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
234
|
-
/**
|
|
235
|
-
* Validate if digit is valid for given position
|
|
236
|
-
* Position 0: first hour digit (0-2)
|
|
237
|
-
* Position 1: second hour digit (0-9, but 0-3 if first is 2)
|
|
238
|
-
* Position 3: first minute digit (0-5)
|
|
239
|
-
* Position 4: second minute digit (0-9)
|
|
240
|
-
*
|
|
241
|
-
* PORTED FROM: validate-time-digit in date_picker.cljs
|
|
242
|
-
*/
|
|
100
|
+
if (!rawDigits || rawDigits.length !== 4)
|
|
101
|
+
return null;
|
|
102
|
+
const hourStr = rawDigits.substring(0, 2);
|
|
103
|
+
const minuteStr = rawDigits.substring(2, 4);
|
|
104
|
+
const hour = parseInt(hourStr, 10);
|
|
105
|
+
const minute = parseInt(minuteStr, 10);
|
|
106
|
+
if (hour < 0 || hour > 23 || minute < 0 || minute > 59) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
return { hour, minute };
|
|
110
|
+
}
|
|
243
111
|
function validateTimeDigit(digit, position, currentDigits) {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
}
|
|
259
|
-
/**
|
|
260
|
-
* Format hour and minute into "HH:mm" display
|
|
261
|
-
*
|
|
262
|
-
* PORTED FROM: format-time-display in date_picker.cljs
|
|
263
|
-
*/
|
|
112
|
+
switch (position) {
|
|
113
|
+
case 0:
|
|
114
|
+
return digit <= 2;
|
|
115
|
+
case 1: {
|
|
116
|
+
const firstHour = parseInt(currentDigits[0], 10);
|
|
117
|
+
return firstHour === 2 ? digit <= 3 : true;
|
|
118
|
+
}
|
|
119
|
+
case 3:
|
|
120
|
+
return digit <= 5;
|
|
121
|
+
case 4:
|
|
122
|
+
return true;
|
|
123
|
+
default:
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
264
127
|
function formatTimeDisplay(hour, minute) {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
}
|
|
269
|
-
/**
|
|
270
|
-
* Find next editable position, skipping delimiter at position 2
|
|
271
|
-
*
|
|
272
|
-
* PORTED FROM: find-next-editable-position in date_picker.cljs
|
|
273
|
-
*/
|
|
128
|
+
const hourStr = hour.toString().padStart(2, '0');
|
|
129
|
+
const minuteStr = minute.toString().padStart(2, '0');
|
|
130
|
+
return `${hourStr}:${minuteStr}`;
|
|
131
|
+
}
|
|
274
132
|
function findNextEditablePosition(currentPos) {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
}
|
|
284
|
-
/**
|
|
285
|
-
* Find previous editable position, skipping delimiter at position 2
|
|
286
|
-
*
|
|
287
|
-
* PORTED FROM: find-prev-editable-position in date_picker.cljs
|
|
288
|
-
*/
|
|
133
|
+
switch (currentPos) {
|
|
134
|
+
case 0: return 1;
|
|
135
|
+
case 1: return 3;
|
|
136
|
+
case 3: return 4;
|
|
137
|
+
case 4: return 5;
|
|
138
|
+
case 5: return 5;
|
|
139
|
+
default: return currentPos;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
289
142
|
function findPrevEditablePosition(currentPos) {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
}
|
|
299
|
-
/**
|
|
300
|
-
* Convert internal position (0,1,3,4) to raw digits index (0,1,2,3)
|
|
301
|
-
*
|
|
302
|
-
* PORTED FROM: position->raw-digit-index in date_picker.cljs
|
|
303
|
-
*/
|
|
143
|
+
switch (currentPos) {
|
|
144
|
+
case 5: return 4;
|
|
145
|
+
case 4: return 3;
|
|
146
|
+
case 3: return 1;
|
|
147
|
+
case 1: return 0;
|
|
148
|
+
case 0: return 0;
|
|
149
|
+
default: return currentPos;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
304
152
|
function positionToRawDigitIndex(internalPos) {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
}
|
|
313
|
-
// ============================================================================
|
|
314
|
-
// TimeInput Helper Class
|
|
315
|
-
// ============================================================================
|
|
316
|
-
/**
|
|
317
|
-
* TimeInput manages the state and behavior of the time input element.
|
|
318
|
-
* Handles smart cursor navigation, digit replacement, and validation.
|
|
319
|
-
*
|
|
320
|
-
* PORTED FROM: Time input state management functions in date_picker.cljs
|
|
321
|
-
*/
|
|
153
|
+
switch (internalPos) {
|
|
154
|
+
case 0: return 0;
|
|
155
|
+
case 1: return 1;
|
|
156
|
+
case 3: return 2;
|
|
157
|
+
case 4: return 3;
|
|
158
|
+
default: return 0;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
322
161
|
class TimeInput {
|
|
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
|
-
* Replace digit at specific position
|
|
489
|
-
*
|
|
490
|
-
* PORTED FROM: replace-digit-at-position in date_picker.cljs
|
|
491
|
-
*/
|
|
492
|
-
replaceDigitAtPosition(position, newDigit) {
|
|
493
|
-
const rawIndex = positionToRawDigitIndex(position);
|
|
494
|
-
const digitsArray = this.state.rawDigits.split('');
|
|
495
|
-
digitsArray[rawIndex] = newDigit.toString();
|
|
496
|
-
const newDigits = digitsArray.join('');
|
|
497
|
-
const parsed = parseTimeComponents(newDigits);
|
|
498
|
-
if (!parsed)
|
|
499
|
-
return null;
|
|
500
|
-
return {
|
|
501
|
-
rawDigits: newDigits,
|
|
502
|
-
hour: parsed.hour,
|
|
503
|
-
minute: parsed.minute,
|
|
504
|
-
displayValue: formatTimeDisplay(parsed.hour, parsed.minute),
|
|
505
|
-
};
|
|
506
|
-
}
|
|
507
|
-
/**
|
|
508
|
-
* Zero digit at specific position
|
|
509
|
-
*
|
|
510
|
-
* PORTED FROM: zero-digit-at-position in date_picker.cljs
|
|
511
|
-
*/
|
|
512
|
-
zeroDigitAtPosition(position) {
|
|
513
|
-
return this.replaceDigitAtPosition(position, 0);
|
|
514
|
-
}
|
|
515
|
-
/**
|
|
516
|
-
* Update internal state and refresh display
|
|
517
|
-
*
|
|
518
|
-
* PORTED FROM: update-time-input-state! in date_picker.cljs
|
|
519
|
-
*/
|
|
520
|
-
updateState(updates) {
|
|
521
|
-
this.state = { ...this.state, ...updates };
|
|
522
|
-
// Update display value
|
|
523
|
-
this.element.value = this.state.displayValue;
|
|
524
|
-
// Set cursor position, mapping internal positions to DOM positions
|
|
525
|
-
const caretPos = this.state.caretPosition;
|
|
526
|
-
const actualPos = caretPos === 0 ? 0 :
|
|
527
|
-
caretPos === 1 ? 1 :
|
|
528
|
-
caretPos === 2 ? 3 :
|
|
529
|
-
caretPos === 3 ? 3 :
|
|
530
|
-
caretPos === 4 ? 4 :
|
|
531
|
-
caretPos === 5 ? 5 : caretPos;
|
|
532
|
-
this.element.setSelectionRange(actualPos, actualPos);
|
|
533
|
-
}
|
|
534
|
-
/**
|
|
535
|
-
* Notify date picker of time change
|
|
536
|
-
*/
|
|
537
|
-
notifyTimeChange() {
|
|
538
|
-
this.datePickerElement.handleTimeInputChange(this.state.hour, this.state.minute);
|
|
539
|
-
}
|
|
540
|
-
/**
|
|
541
|
-
* Get current time values
|
|
542
|
-
*/
|
|
543
|
-
getTime() {
|
|
544
|
-
return {
|
|
545
|
-
hour: this.state.hour,
|
|
546
|
-
minute: this.state.minute,
|
|
547
|
-
};
|
|
548
|
-
}
|
|
549
|
-
/**
|
|
550
|
-
* Update time from external source
|
|
551
|
-
*/
|
|
552
|
-
setTime(hour, minute) {
|
|
553
|
-
const display = formatTimeDisplay(hour, minute);
|
|
554
|
-
const rawDigits = hour.toString().padStart(2, '0') + minute.toString().padStart(2, '0');
|
|
555
|
-
this.state = {
|
|
556
|
-
...this.state,
|
|
557
|
-
hour,
|
|
558
|
-
minute,
|
|
559
|
-
displayValue: display,
|
|
560
|
-
rawDigits,
|
|
561
|
-
};
|
|
562
|
-
this.element.value = display;
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
// ============================================================================
|
|
566
|
-
// TyDatePicker Custom Element
|
|
567
|
-
// ============================================================================
|
|
162
|
+
constructor(element, datePickerElement, hour = 0, minute = 0) {
|
|
163
|
+
this.element = element;
|
|
164
|
+
this.datePickerElement = datePickerElement;
|
|
165
|
+
const display = formatTimeDisplay(hour, minute);
|
|
166
|
+
const rawDigits = hour.toString().padStart(2, '0') + minute.toString().padStart(2, '0');
|
|
167
|
+
this.state = {
|
|
168
|
+
hour,
|
|
169
|
+
minute,
|
|
170
|
+
caretPosition: 0,
|
|
171
|
+
displayValue: display,
|
|
172
|
+
rawDigits,
|
|
173
|
+
};
|
|
174
|
+
this.element.value = display;
|
|
175
|
+
this.setupEventListeners();
|
|
176
|
+
}
|
|
177
|
+
setupEventListeners() {
|
|
178
|
+
this.element.addEventListener('keydown', (e) => this.handleKeyDown(e));
|
|
179
|
+
this.element.addEventListener('input', (e) => this.handleInput(e));
|
|
180
|
+
this.element.addEventListener('click', () => this.handleClick());
|
|
181
|
+
this.element.addEventListener('focus', () => this.handleFocus());
|
|
182
|
+
}
|
|
183
|
+
handleKeyDown(event) {
|
|
184
|
+
const key = event.key;
|
|
185
|
+
switch (key) {
|
|
186
|
+
case 'ArrowRight':
|
|
187
|
+
this.handleArrowRight(event);
|
|
188
|
+
break;
|
|
189
|
+
case 'ArrowLeft':
|
|
190
|
+
this.handleArrowLeft(event);
|
|
191
|
+
break;
|
|
192
|
+
case 'Backspace':
|
|
193
|
+
this.handleBackspace(event);
|
|
194
|
+
break;
|
|
195
|
+
case 'Delete':
|
|
196
|
+
this.handleDelete(event);
|
|
197
|
+
break;
|
|
198
|
+
case 'Home':
|
|
199
|
+
event.preventDefault();
|
|
200
|
+
this.updateState({ caretPosition: 0 });
|
|
201
|
+
break;
|
|
202
|
+
case 'End':
|
|
203
|
+
event.preventDefault();
|
|
204
|
+
this.updateState({ caretPosition: 5 });
|
|
205
|
+
break;
|
|
206
|
+
case 'Tab':
|
|
207
|
+
break;
|
|
208
|
+
default:
|
|
209
|
+
if (/^\d$/.test(key)) {
|
|
210
|
+
this.handleDigitInput(event, parseInt(key, 10));
|
|
211
|
+
}
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
handleInput(event) {
|
|
216
|
+
event.preventDefault();
|
|
217
|
+
}
|
|
218
|
+
handleClick() {
|
|
219
|
+
this.updateState({ caretPosition: 0 });
|
|
220
|
+
}
|
|
221
|
+
handleFocus() {
|
|
222
|
+
this.updateState({ caretPosition: 0 });
|
|
223
|
+
}
|
|
224
|
+
handleArrowRight(event) {
|
|
225
|
+
event.preventDefault();
|
|
226
|
+
const nextPos = findNextEditablePosition(this.state.caretPosition);
|
|
227
|
+
this.updateState({ caretPosition: nextPos });
|
|
228
|
+
}
|
|
229
|
+
handleArrowLeft(event) {
|
|
230
|
+
event.preventDefault();
|
|
231
|
+
const prevPos = findPrevEditablePosition(this.state.caretPosition);
|
|
232
|
+
this.updateState({ caretPosition: prevPos });
|
|
233
|
+
}
|
|
234
|
+
handleDigitInput(event, digit) {
|
|
235
|
+
event.preventDefault();
|
|
236
|
+
const currentPos = this.state.caretPosition;
|
|
237
|
+
if (![0, 1, 3, 4].includes(currentPos))
|
|
238
|
+
return;
|
|
239
|
+
if (!validateTimeDigit(digit, currentPos, this.state.rawDigits))
|
|
240
|
+
return;
|
|
241
|
+
const newState = this.replaceDigitAtPosition(currentPos, digit);
|
|
242
|
+
if (!newState)
|
|
243
|
+
return;
|
|
244
|
+
const nextPos = findNextEditablePosition(currentPos);
|
|
245
|
+
this.updateState({ ...newState, caretPosition: nextPos });
|
|
246
|
+
this.notifyTimeChange();
|
|
247
|
+
}
|
|
248
|
+
handleBackspace(event) {
|
|
249
|
+
event.preventDefault();
|
|
250
|
+
const currentPos = this.state.caretPosition;
|
|
251
|
+
if (currentPos === 0)
|
|
252
|
+
return;
|
|
253
|
+
const targetPos = currentPos === 1 ? 0 :
|
|
254
|
+
currentPos === 3 ? 1 :
|
|
255
|
+
currentPos === 4 ? 3 :
|
|
256
|
+
currentPos === 5 ? 4 : 0;
|
|
257
|
+
const newState = this.zeroDigitAtPosition(targetPos);
|
|
258
|
+
if (!newState)
|
|
259
|
+
return;
|
|
260
|
+
this.updateState({ ...newState, caretPosition: targetPos });
|
|
261
|
+
this.notifyTimeChange();
|
|
262
|
+
}
|
|
263
|
+
handleDelete(event) {
|
|
264
|
+
event.preventDefault();
|
|
265
|
+
const currentPos = this.state.caretPosition;
|
|
266
|
+
if (![0, 1, 3, 4].includes(currentPos))
|
|
267
|
+
return;
|
|
268
|
+
const newState = this.zeroDigitAtPosition(currentPos);
|
|
269
|
+
if (!newState)
|
|
270
|
+
return;
|
|
271
|
+
this.updateState({ ...newState, caretPosition: currentPos });
|
|
272
|
+
this.notifyTimeChange();
|
|
273
|
+
}
|
|
274
|
+
replaceDigitAtPosition(position, newDigit) {
|
|
275
|
+
const rawIndex = positionToRawDigitIndex(position);
|
|
276
|
+
const digitsArray = this.state.rawDigits.split('');
|
|
277
|
+
digitsArray[rawIndex] = newDigit.toString();
|
|
278
|
+
const newDigits = digitsArray.join('');
|
|
279
|
+
const parsed = parseTimeComponents(newDigits);
|
|
280
|
+
if (!parsed)
|
|
281
|
+
return null;
|
|
282
|
+
return {
|
|
283
|
+
rawDigits: newDigits,
|
|
284
|
+
hour: parsed.hour,
|
|
285
|
+
minute: parsed.minute,
|
|
286
|
+
displayValue: formatTimeDisplay(parsed.hour, parsed.minute),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
zeroDigitAtPosition(position) {
|
|
290
|
+
return this.replaceDigitAtPosition(position, 0);
|
|
291
|
+
}
|
|
292
|
+
updateState(updates) {
|
|
293
|
+
this.state = { ...this.state, ...updates };
|
|
294
|
+
this.element.value = this.state.displayValue;
|
|
295
|
+
const caretPos = this.state.caretPosition;
|
|
296
|
+
const actualPos = caretPos === 0 ? 0 :
|
|
297
|
+
caretPos === 1 ? 1 :
|
|
298
|
+
caretPos === 2 ? 3 :
|
|
299
|
+
caretPos === 3 ? 3 :
|
|
300
|
+
caretPos === 4 ? 4 :
|
|
301
|
+
caretPos === 5 ? 5 : caretPos;
|
|
302
|
+
this.element.setSelectionRange(actualPos, actualPos);
|
|
303
|
+
}
|
|
304
|
+
notifyTimeChange() {
|
|
305
|
+
this.datePickerElement.handleTimeInputChange(this.state.hour, this.state.minute);
|
|
306
|
+
}
|
|
307
|
+
getTime() {
|
|
308
|
+
return {
|
|
309
|
+
hour: this.state.hour,
|
|
310
|
+
minute: this.state.minute,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
setTime(hour, minute) {
|
|
314
|
+
const display = formatTimeDisplay(hour, minute);
|
|
315
|
+
const rawDigits = hour.toString().padStart(2, '0') + minute.toString().padStart(2, '0');
|
|
316
|
+
this.state = {
|
|
317
|
+
...this.state,
|
|
318
|
+
hour,
|
|
319
|
+
minute,
|
|
320
|
+
displayValue: display,
|
|
321
|
+
rawDigits,
|
|
322
|
+
};
|
|
323
|
+
this.element.value = display;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
568
326
|
export class TyDatePicker extends TyComponent {
|
|
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
|
-
|
|
676
|
-
|
|
677
|
-
|
|
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
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
calendar.setAttribute('year', this._state.year.toString());
|
|
1315
|
-
calendar.setAttribute('month', this._state.month.toString());
|
|
1316
|
-
calendar.setAttribute('day', this._state.day.toString());
|
|
1317
|
-
}
|
|
1318
|
-
// Keep an already-open popup's flavor in sync. render() only rebuilds
|
|
1319
|
-
// (and re-forwards flavor) while the dialog is CLOSED — while open it
|
|
1320
|
-
// takes this partial-update path instead, so without this the calendar
|
|
1321
|
-
// would stay stuck on whatever flavor was active when it was opened.
|
|
1322
|
-
if (calendar) {
|
|
1323
|
-
const flavor = this.getProperty('flavor');
|
|
1324
|
-
if (flavor && flavor !== 'default')
|
|
1325
|
-
calendar.setAttribute('flavor', flavor);
|
|
1326
|
-
else
|
|
1327
|
-
calendar.removeAttribute('flavor');
|
|
1328
|
-
}
|
|
1329
|
-
if (this._timeInput && this._state.hour !== undefined && this._state.minute !== undefined) {
|
|
1330
|
-
this._timeInput.setTime(this._state.hour, this._state.minute);
|
|
1331
|
-
}
|
|
1332
|
-
}
|
|
1333
|
-
/**
|
|
1334
|
-
* Setup document-level event listeners
|
|
1335
|
-
*/
|
|
1336
|
-
setupEventListeners() {
|
|
1337
|
-
// Remove old listeners if they exist
|
|
1338
|
-
if (this._clickListener) {
|
|
1339
|
-
document.removeEventListener('click', this._clickListener);
|
|
1340
|
-
}
|
|
1341
|
-
if (this._keydownListener) {
|
|
1342
|
-
document.removeEventListener('keydown', this._keydownListener);
|
|
1343
|
-
}
|
|
1344
|
-
// Create new listeners
|
|
1345
|
-
this._clickListener = (e) => this.handleOutsideClick(e);
|
|
1346
|
-
this._keydownListener = (e) => this.handleEscapeKey(e);
|
|
1347
|
-
// Add document listeners
|
|
1348
|
-
document.addEventListener('click', this._clickListener);
|
|
1349
|
-
document.addEventListener('keydown', this._keydownListener);
|
|
1350
|
-
// Add dialog click listener
|
|
1351
|
-
const dialog = this.shadowRoot?.querySelector('.calendar-dialog');
|
|
1352
|
-
if (dialog) {
|
|
1353
|
-
this._dialogClickListener = (e) => this.handleDialogClick(e);
|
|
1354
|
-
dialog.addEventListener('click', this._dialogClickListener);
|
|
1355
|
-
}
|
|
1356
|
-
}
|
|
1357
|
-
// ==========================================================================
|
|
1358
|
-
// Event Handlers
|
|
1359
|
-
// ==========================================================================
|
|
1360
|
-
handleStubClick(event) {
|
|
1361
|
-
event.preventDefault();
|
|
1362
|
-
// Heal state/DOM desync: a host re-render can replace the dialog element
|
|
1363
|
-
// while `open` is still true, stranding the picker (openDropdown
|
|
1364
|
-
// early-returns on _state.open). Reality wins: closed dialog → closed state.
|
|
1365
|
-
const dialog = this.shadowRoot?.querySelector('.calendar-dialog');
|
|
1366
|
-
if (this._state.open && (!dialog || !dialog.open)) {
|
|
1367
|
-
this.updateState({ open: false }, true);
|
|
1368
|
-
unlockScroll(`date-picker-${this.id || this.toString()}`);
|
|
1369
|
-
}
|
|
1370
|
-
const disabled = this.getProperty('disabled');
|
|
1371
|
-
// FIXME: Timestamp guard is a workaround. Calendar day cells use pointerdown,
|
|
1372
|
-
// which closes the dialog before the click event fires — the click then hits the
|
|
1373
|
-
// stub and reopens. Proper fix: switch calendar-month day cells from pointerdown
|
|
1374
|
-
// to click so the event is consumed before the dialog closes.
|
|
1375
|
-
if (!disabled && Date.now() - this._closeTimestamp > 300) {
|
|
1376
|
-
this.openDropdown();
|
|
1377
|
-
}
|
|
1378
|
-
}
|
|
1379
|
-
/**
|
|
1380
|
-
* Clear the date value, sync state, emit event, and re-render.
|
|
1381
|
-
*/
|
|
1382
|
-
clearValue() {
|
|
1383
|
-
this.updateState({
|
|
1384
|
-
year: undefined,
|
|
1385
|
-
month: undefined,
|
|
1386
|
-
day: undefined,
|
|
1387
|
-
hour: undefined,
|
|
1388
|
-
minute: undefined,
|
|
1389
|
-
}, true);
|
|
1390
|
-
this.value = null;
|
|
1391
|
-
this.emitChangeEvent(null, 'clear');
|
|
1392
|
-
this.render();
|
|
1393
|
-
}
|
|
1394
|
-
handleClearClick(event) {
|
|
1395
|
-
event.preventDefault();
|
|
1396
|
-
event.stopPropagation();
|
|
1397
|
-
this.clearValue();
|
|
1398
|
-
}
|
|
1399
|
-
/**
|
|
1400
|
-
* Handle calendar date selection
|
|
1401
|
-
*
|
|
1402
|
-
* PORTED FROM: handle-calendar-change! in date_picker.cljs
|
|
1403
|
-
*/
|
|
1404
|
-
handleCalendarChange(event) {
|
|
1405
|
-
event.preventDefault();
|
|
1406
|
-
event.stopPropagation();
|
|
1407
|
-
const customEvent = event;
|
|
1408
|
-
const detail = customEvent.detail;
|
|
1409
|
-
const dayContext = detail.dayContext;
|
|
1410
|
-
if (!dayContext)
|
|
1411
|
-
return;
|
|
1412
|
-
// Update state with new date
|
|
1413
|
-
const newComponents = {
|
|
1414
|
-
year: dayContext.year,
|
|
1415
|
-
month: dayContext.month,
|
|
1416
|
-
day: dayContext.dayInMonth,
|
|
1417
|
-
hour: this._state.hour,
|
|
1418
|
-
minute: this._state.minute,
|
|
1419
|
-
};
|
|
1420
|
-
// Force sync when calendar date changes
|
|
1421
|
-
this.updateState(newComponents, true);
|
|
1422
|
-
this.emitChangeEvent(newComponents, 'selection');
|
|
1423
|
-
// Close calendar if time is not required
|
|
1424
|
-
if (!this._state.withTime) {
|
|
1425
|
-
this.closeDropdown();
|
|
1426
|
-
}
|
|
1427
|
-
else {
|
|
1428
|
-
// Auto-focus time input after date selection
|
|
1429
|
-
requestAnimationFrame(() => {
|
|
1430
|
-
if (!this.shadowRoot)
|
|
1431
|
-
return;
|
|
1432
|
-
const timeInput = this.shadowRoot.querySelector('.time-input');
|
|
1433
|
-
if (timeInput) {
|
|
1434
|
-
timeInput.focus();
|
|
1435
|
-
}
|
|
1436
|
-
});
|
|
1437
|
-
}
|
|
1438
|
-
}
|
|
1439
|
-
/**
|
|
1440
|
-
* Handle dialog backdrop clicks
|
|
1441
|
-
*
|
|
1442
|
-
* PORTED FROM: handle-dialog-click! in date_picker.cljs
|
|
1443
|
-
*/
|
|
1444
|
-
handleDialogClick(event) {
|
|
1445
|
-
if (!this.shadowRoot)
|
|
1446
|
-
return;
|
|
1447
|
-
const dialog = this.shadowRoot.querySelector('.calendar-dialog');
|
|
1448
|
-
const content = this.shadowRoot.querySelector('.calendar-content');
|
|
1449
|
-
// Close if clicking on dialog backdrop (not calendar content)
|
|
1450
|
-
if (event.target === dialog && this._state.open && content && !content.contains(event.target)) {
|
|
1451
|
-
event.preventDefault();
|
|
1452
|
-
event.stopPropagation();
|
|
1453
|
-
this.closeDropdown();
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
/**
|
|
1457
|
-
* Handle clicks outside the date picker
|
|
1458
|
-
*
|
|
1459
|
-
* PORTED FROM: handle-outside-click! in date_picker.cljs
|
|
1460
|
-
*/
|
|
1461
|
-
handleOutsideClick(event) {
|
|
1462
|
-
if (!this.shadowRoot)
|
|
1463
|
-
return;
|
|
1464
|
-
const target = event.target;
|
|
1465
|
-
const dialog = this.shadowRoot.querySelector('.calendar-dialog');
|
|
1466
|
-
// Check if click is truly outside everything
|
|
1467
|
-
if (this._state.open && dialog && !this.contains(target) && !dialog.contains(target)) {
|
|
1468
|
-
this.closeDropdown();
|
|
1469
|
-
}
|
|
1470
|
-
}
|
|
1471
|
-
/**
|
|
1472
|
-
* Handle Escape key press
|
|
1473
|
-
*
|
|
1474
|
-
* PORTED FROM: handle-escape-key! in date_picker.cljs
|
|
1475
|
-
*/
|
|
1476
|
-
handleEscapeKey(event) {
|
|
1477
|
-
const keyboardEvent = event;
|
|
1478
|
-
if (keyboardEvent.key === 'Escape' && this._state.open) {
|
|
1479
|
-
keyboardEvent.preventDefault();
|
|
1480
|
-
this.closeDropdown();
|
|
1481
|
-
}
|
|
1482
|
-
}
|
|
1483
|
-
/**
|
|
1484
|
-
* Open calendar dropdown
|
|
1485
|
-
*
|
|
1486
|
-
* PORTED FROM: open-dropdown! in date_picker.cljs
|
|
1487
|
-
*/
|
|
1488
|
-
openDropdown() {
|
|
1489
|
-
if (this._state.open)
|
|
1490
|
-
return;
|
|
1491
|
-
if (isMobileTouch())
|
|
1492
|
-
return;
|
|
1493
|
-
this.updateState({ open: true });
|
|
1494
|
-
// Dispatch open event
|
|
1495
|
-
this.dispatchEvent(new CustomEvent('open', {
|
|
1496
|
-
bubbles: true,
|
|
1497
|
-
composed: true
|
|
1498
|
-
}));
|
|
1499
|
-
this.render();
|
|
1500
|
-
requestAnimationFrame(() => {
|
|
1501
|
-
if (!this.shadowRoot)
|
|
1502
|
-
return;
|
|
1503
|
-
const dialog = this.shadowRoot.querySelector('.calendar-dialog');
|
|
1504
|
-
if (dialog) {
|
|
1505
|
-
const pickerId = `date-picker-${this.id || this.toString()}`;
|
|
1506
|
-
lockScroll(pickerId);
|
|
1507
|
-
dialog.showModal();
|
|
1508
|
-
this.calculateCalendarPosition();
|
|
1509
|
-
dialog.classList.add('open');
|
|
1510
|
-
const stubEl = this.shadowRoot.querySelector('.date-picker-stub');
|
|
1511
|
-
if (stubEl)
|
|
1512
|
-
stubEl.setAttribute('aria-expanded', 'true');
|
|
1513
|
-
// Remove focus from any focused elements to prevent the blue outline
|
|
1514
|
-
const focusedElement = this.shadowRoot.activeElement;
|
|
1515
|
-
if (focusedElement) {
|
|
1516
|
-
focusedElement.blur();
|
|
1517
|
-
}
|
|
1518
|
-
}
|
|
1519
|
-
});
|
|
1520
|
-
}
|
|
1521
|
-
/**
|
|
1522
|
-
* Close calendar dropdown
|
|
1523
|
-
*
|
|
1524
|
-
* PORTED FROM: close-dropdown! in date_picker.cljs
|
|
1525
|
-
*/
|
|
1526
|
-
closeDropdown() {
|
|
1527
|
-
if (!this._state.open)
|
|
1528
|
-
return;
|
|
1529
|
-
this._closeTimestamp = Date.now();
|
|
1530
|
-
const pickerId = `date-picker-${this.id || this.toString()}`;
|
|
1531
|
-
// Force sync any staged updates when closing
|
|
1532
|
-
this.updateState({ open: false }, true);
|
|
1533
|
-
unlockScroll(pickerId);
|
|
1534
|
-
if (this.shadowRoot) {
|
|
1535
|
-
const dialog = this.shadowRoot.querySelector('.calendar-dialog');
|
|
1536
|
-
if (dialog) {
|
|
1537
|
-
dialog.classList.remove('position-above', 'position-below', 'open');
|
|
1538
|
-
dialog.close();
|
|
1539
|
-
}
|
|
1540
|
-
// closeDropdown() doesn't call render(), so the stub's aria-expanded
|
|
1541
|
-
// (still "true" from open) needs an explicit flip here too.
|
|
1542
|
-
const stubEl = this.shadowRoot.querySelector('.date-picker-stub');
|
|
1543
|
-
if (stubEl)
|
|
1544
|
-
stubEl.setAttribute('aria-expanded', 'false');
|
|
1545
|
-
}
|
|
1546
|
-
this.dispatchEvent(new CustomEvent('close', {
|
|
1547
|
-
bubbles: true,
|
|
1548
|
-
composed: true
|
|
1549
|
-
}));
|
|
1550
|
-
this.render();
|
|
1551
|
-
}
|
|
1552
|
-
// ==========================================================================
|
|
1553
|
-
// Cleanup
|
|
1554
|
-
// ==========================================================================
|
|
1555
|
-
/**
|
|
1556
|
-
* Clean up event listeners and state
|
|
1557
|
-
*
|
|
1558
|
-
* PORTED FROM: cleanup! in date_picker.cljs
|
|
1559
|
-
*/
|
|
1560
|
-
cleanup() {
|
|
1561
|
-
// Remove document listeners
|
|
1562
|
-
if (this._clickListener) {
|
|
1563
|
-
document.removeEventListener('click', this._clickListener);
|
|
1564
|
-
}
|
|
1565
|
-
if (this._keydownListener) {
|
|
1566
|
-
document.removeEventListener('keydown', this._keydownListener);
|
|
1567
|
-
}
|
|
1568
|
-
// Remove dialog listener
|
|
1569
|
-
if (this._dialogClickListener && this.shadowRoot) {
|
|
1570
|
-
const dialog = this.shadowRoot.querySelector('.calendar-dialog');
|
|
1571
|
-
if (dialog) {
|
|
1572
|
-
dialog.removeEventListener('click', this._dialogClickListener);
|
|
1573
|
-
}
|
|
1574
|
-
}
|
|
1575
|
-
// Unlock scroll if open
|
|
1576
|
-
if (this._state.open) {
|
|
1577
|
-
const pickerId = `date-picker-${this.id || this.toString()}`;
|
|
1578
|
-
unlockScroll(pickerId);
|
|
1579
|
-
}
|
|
1580
|
-
// Clear references
|
|
1581
|
-
this._clickListener = undefined;
|
|
1582
|
-
this._keydownListener = undefined;
|
|
1583
|
-
this._dialogClickListener = undefined;
|
|
1584
|
-
this._timeInput = undefined;
|
|
1585
|
-
}
|
|
1586
|
-
}
|
|
1587
|
-
// ============================================================================
|
|
1588
|
-
// PROPERTY CONFIGURATION - Single source of truth
|
|
1589
|
-
// ============================================================================
|
|
327
|
+
constructor() {
|
|
328
|
+
super();
|
|
329
|
+
this._state = {
|
|
330
|
+
withTime: false,
|
|
331
|
+
open: false,
|
|
332
|
+
};
|
|
333
|
+
this._closeTimestamp = 0;
|
|
334
|
+
this._customFlavorSheet = null;
|
|
335
|
+
const shadow = this.shadowRoot;
|
|
336
|
+
ensureStyles(shadow, { css: datePickerStyles, id: 'ty-date-picker' });
|
|
337
|
+
}
|
|
338
|
+
onConnect() {
|
|
339
|
+
this.initializeState();
|
|
340
|
+
this.render();
|
|
341
|
+
this._localeObserver = observeLocaleChanges(this, () => {
|
|
342
|
+
this.render();
|
|
343
|
+
});
|
|
344
|
+
this._syncCustomFlavor();
|
|
345
|
+
}
|
|
346
|
+
_syncCustomFlavor() {
|
|
347
|
+
const flavor = this.flavor === 'default' ? 'neutral' : this.flavor;
|
|
348
|
+
this._customFlavorSheet = syncCustomFlavorSheet(this.shadowRoot, this._customFlavorSheet, flavor, ({ base }) => datePickerCustomFlavorCss(base));
|
|
349
|
+
}
|
|
350
|
+
onDisconnect() {
|
|
351
|
+
if (this._localeObserver) {
|
|
352
|
+
this._localeObserver();
|
|
353
|
+
this._localeObserver = undefined;
|
|
354
|
+
}
|
|
355
|
+
this.cleanup();
|
|
356
|
+
}
|
|
357
|
+
onPropertiesChanged(changes) {
|
|
358
|
+
for (const { name, newValue } of changes) {
|
|
359
|
+
switch (name) {
|
|
360
|
+
case 'flavor':
|
|
361
|
+
this._syncCustomFlavor();
|
|
362
|
+
break;
|
|
363
|
+
case 'size':
|
|
364
|
+
case 'label':
|
|
365
|
+
case 'placeholder':
|
|
366
|
+
case 'required':
|
|
367
|
+
case 'disabled':
|
|
368
|
+
case 'clearable':
|
|
369
|
+
case 'format':
|
|
370
|
+
case 'locale':
|
|
371
|
+
break;
|
|
372
|
+
case 'value': {
|
|
373
|
+
const newComponents = parseValue(newValue, this._state.withTime);
|
|
374
|
+
if (newComponents === null) {
|
|
375
|
+
const hasDate = this._state.year !== undefined ||
|
|
376
|
+
this._state.month !== undefined ||
|
|
377
|
+
this._state.day !== undefined;
|
|
378
|
+
if (hasDate) {
|
|
379
|
+
this._state = {
|
|
380
|
+
withTime: this._state.withTime,
|
|
381
|
+
open: this._state.open,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
this.syncFormValue();
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
const currentComponents = {
|
|
388
|
+
year: this._state.year,
|
|
389
|
+
month: this._state.month,
|
|
390
|
+
day: this._state.day,
|
|
391
|
+
hour: this._state.hour,
|
|
392
|
+
minute: this._state.minute,
|
|
393
|
+
};
|
|
394
|
+
const changed = newComponents?.year !== currentComponents.year ||
|
|
395
|
+
newComponents?.month !== currentComponents.month ||
|
|
396
|
+
newComponents?.day !== currentComponents.day ||
|
|
397
|
+
newComponents?.hour !== currentComponents.hour ||
|
|
398
|
+
newComponents?.minute !== currentComponents.minute;
|
|
399
|
+
if (changed) {
|
|
400
|
+
this._state = {
|
|
401
|
+
...this._state,
|
|
402
|
+
...newComponents,
|
|
403
|
+
};
|
|
404
|
+
this.syncFormValue();
|
|
405
|
+
}
|
|
406
|
+
break;
|
|
407
|
+
}
|
|
408
|
+
case 'with-time': {
|
|
409
|
+
const newWithTime = newValue;
|
|
410
|
+
const oldWithTime = this._state.withTime;
|
|
411
|
+
if (newWithTime !== oldWithTime) {
|
|
412
|
+
this._state.withTime = newWithTime;
|
|
413
|
+
const hasDate = this._state.year !== undefined &&
|
|
414
|
+
this._state.month !== undefined &&
|
|
415
|
+
this._state.day !== undefined;
|
|
416
|
+
if (hasDate) {
|
|
417
|
+
this.syncFormValue();
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
getFormValue() {
|
|
426
|
+
return componentsToOutputValue(this.getComponents());
|
|
427
|
+
}
|
|
428
|
+
getComponents() {
|
|
429
|
+
return {
|
|
430
|
+
year: this._state.year,
|
|
431
|
+
month: this._state.month,
|
|
432
|
+
day: this._state.day,
|
|
433
|
+
hour: this._state.hour,
|
|
434
|
+
minute: this._state.minute,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
hasDate() {
|
|
438
|
+
return !!(this._state.year && this._state.month && this._state.day);
|
|
439
|
+
}
|
|
440
|
+
getFormattedValue(components) {
|
|
441
|
+
const c = components || this.getComponents();
|
|
442
|
+
if (!c.year || !c.month || !c.day)
|
|
443
|
+
return null;
|
|
444
|
+
return formatDisplayValue(c, this.getProperty('format') || 'long', getEffectiveLocale(this, this.getProperty('locale')), this._state.withTime);
|
|
445
|
+
}
|
|
446
|
+
initializeState() {
|
|
447
|
+
const valueAttr = this.getProperty('value');
|
|
448
|
+
const withTime = this.getProperty('with-time');
|
|
449
|
+
const components = parseValue(valueAttr, withTime);
|
|
450
|
+
if (components === null) {
|
|
451
|
+
this._state = {
|
|
452
|
+
withTime,
|
|
453
|
+
open: false,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
this._state = {
|
|
458
|
+
...components,
|
|
459
|
+
withTime,
|
|
460
|
+
open: false,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
this.syncFormValue();
|
|
464
|
+
}
|
|
465
|
+
updateState(updates, forceSync = false) {
|
|
466
|
+
this._state = { ...this._state, ...updates };
|
|
467
|
+
const shouldSync = forceSync || !this._state.open || !this._state.withTime;
|
|
468
|
+
if (shouldSync) {
|
|
469
|
+
this.syncFormValue();
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
syncFormValue() {
|
|
473
|
+
const utcValue = this.getFormValue();
|
|
474
|
+
const currentValue = this.getProperty('value');
|
|
475
|
+
if (utcValue !== currentValue) {
|
|
476
|
+
if (utcValue) {
|
|
477
|
+
this.setProperty('value', utcValue);
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
this.setProperty('value', null);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
updateValidity() {
|
|
485
|
+
if (this.disabled) {
|
|
486
|
+
this._internals.setValidity({});
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
if (this.required && !this.value) {
|
|
490
|
+
this._internals.setValidity({ valueMissing: true }, 'Please fill out this field');
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
this._internals.setValidity({});
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
handleTimeInputChange(hour, minute) {
|
|
497
|
+
if (!this.hasDate())
|
|
498
|
+
return;
|
|
499
|
+
const components = { ...this.getComponents(), hour, minute };
|
|
500
|
+
this.updateState(components, true);
|
|
501
|
+
this.emitChangeEvent(components, 'time-change');
|
|
502
|
+
}
|
|
503
|
+
emitChangeEvent(components, source) {
|
|
504
|
+
const utcValue = components ? componentsToOutputValue(components) : null;
|
|
505
|
+
const localValue = components ? componentsToLocalValue(components, this._state.withTime) : null;
|
|
506
|
+
const milliseconds = components ? componentsToDateObject(components)?.getTime() ?? null : null;
|
|
507
|
+
const formatted = components ? this.getFormattedValue(components) : null;
|
|
508
|
+
const detail = {
|
|
509
|
+
value: utcValue,
|
|
510
|
+
localValue,
|
|
511
|
+
milliseconds,
|
|
512
|
+
formatted,
|
|
513
|
+
source,
|
|
514
|
+
};
|
|
515
|
+
const event = new CustomEvent('change', {
|
|
516
|
+
detail,
|
|
517
|
+
bubbles: true,
|
|
518
|
+
cancelable: true,
|
|
519
|
+
});
|
|
520
|
+
this.dispatchEvent(event);
|
|
521
|
+
}
|
|
522
|
+
get size() { return this.getProperty('size'); }
|
|
523
|
+
set size(v) { this.setProperty('size', v); }
|
|
524
|
+
get placement() { return this.getProperty('placement'); }
|
|
525
|
+
set placement(v) { this.setProperty('placement', v); }
|
|
526
|
+
get flavor() { return this.getProperty('flavor'); }
|
|
527
|
+
set flavor(v) { this.setProperty('flavor', v); }
|
|
528
|
+
get label() { return this.getProperty('label'); }
|
|
529
|
+
set label(v) { this.setProperty('label', v); }
|
|
530
|
+
get placeholder() { return this.getProperty('placeholder'); }
|
|
531
|
+
set placeholder(v) { this.setProperty('placeholder', v); }
|
|
532
|
+
get name() { return this.getProperty('name'); }
|
|
533
|
+
set name(v) { this.setProperty('name', v); }
|
|
534
|
+
get format() { return this.getProperty('format'); }
|
|
535
|
+
set format(v) { this.setProperty('format', v); }
|
|
536
|
+
get locale() { return this.getProperty('locale'); }
|
|
537
|
+
set locale(v) { this.setProperty('locale', v); }
|
|
538
|
+
get required() { return this.getProperty('required'); }
|
|
539
|
+
set required(v) { this.setProperty('required', v); }
|
|
540
|
+
get disabled() { return this.getProperty('disabled'); }
|
|
541
|
+
set disabled(v) { this.setProperty('disabled', v); }
|
|
542
|
+
get clearable() { return this.getProperty('clearable'); }
|
|
543
|
+
set clearable(v) { this.setProperty('clearable', v); }
|
|
544
|
+
get min() { return this.getProperty('min'); }
|
|
545
|
+
set min(v) { this.setProperty('min', v ?? ''); }
|
|
546
|
+
get max() { return this.getProperty('max'); }
|
|
547
|
+
set max(v) { this.setProperty('max', v ?? ''); }
|
|
548
|
+
get withTime() { return this.getProperty('with-time'); }
|
|
549
|
+
set withTime(v) { this.setProperty('with-time', v); }
|
|
550
|
+
get value() {
|
|
551
|
+
return this.getProperty('value') || null;
|
|
552
|
+
}
|
|
553
|
+
set value(val) {
|
|
554
|
+
if (val === null || val === undefined || val === '') {
|
|
555
|
+
this.setProperty('value', null);
|
|
556
|
+
}
|
|
557
|
+
else {
|
|
558
|
+
const strValue = val instanceof Date ? val.toISOString() : val;
|
|
559
|
+
this.setProperty('value', strValue);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
clear() {
|
|
563
|
+
this.clearValue();
|
|
564
|
+
}
|
|
565
|
+
buildStubClasses() {
|
|
566
|
+
const classes = ['date-picker-stub'];
|
|
567
|
+
if (this.getProperty('disabled'))
|
|
568
|
+
classes.push('disabled');
|
|
569
|
+
if (this.getProperty('required'))
|
|
570
|
+
classes.push('required');
|
|
571
|
+
if (this._state.open)
|
|
572
|
+
classes.push('open');
|
|
573
|
+
return classes.join(' ');
|
|
574
|
+
}
|
|
575
|
+
renderStub() {
|
|
576
|
+
const stub = document.createElement('div');
|
|
577
|
+
stub.className = this.buildStubClasses();
|
|
578
|
+
const isDisabled = this.getProperty('disabled');
|
|
579
|
+
if (isDisabled) {
|
|
580
|
+
stub.setAttribute('disabled', 'true');
|
|
581
|
+
}
|
|
582
|
+
stub.setAttribute('role', 'button');
|
|
583
|
+
stub.setAttribute('aria-haspopup', 'dialog');
|
|
584
|
+
stub.setAttribute('aria-expanded', String(this._state.open));
|
|
585
|
+
stub.setAttribute('tabindex', isDisabled ? '-1' : '0');
|
|
586
|
+
if (isDisabled)
|
|
587
|
+
stub.setAttribute('aria-disabled', 'true');
|
|
588
|
+
if (this.getProperty('label')) {
|
|
589
|
+
stub.setAttribute('aria-labelledby', `date-picker-label-${getElementHash(this)}`);
|
|
590
|
+
}
|
|
591
|
+
stub.addEventListener('keydown', (e) => {
|
|
592
|
+
const ke = e;
|
|
593
|
+
if (this._state.open)
|
|
594
|
+
return;
|
|
595
|
+
if (ke.key === 'Enter' || ke.key === ' ') {
|
|
596
|
+
ke.preventDefault();
|
|
597
|
+
this.handleStubClick(e);
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
const startSlot = document.createElement('slot');
|
|
601
|
+
startSlot.name = 'start';
|
|
602
|
+
const displayText = document.createElement('span');
|
|
603
|
+
displayText.className = 'stub-text';
|
|
604
|
+
const formattedValue = this.getFormattedValue();
|
|
605
|
+
const placeholder = this.getProperty('placeholder') || 'Select date...';
|
|
606
|
+
displayText.textContent = formattedValue || placeholder;
|
|
607
|
+
if (!formattedValue) {
|
|
608
|
+
displayText.classList.add('placeholder');
|
|
609
|
+
}
|
|
610
|
+
const iconContainer = document.createElement('div');
|
|
611
|
+
iconContainer.className = 'stub-icons';
|
|
612
|
+
const clearable = this.getProperty('clearable');
|
|
613
|
+
if (clearable && formattedValue && !isDisabled) {
|
|
614
|
+
const clearButton = document.createElement('button');
|
|
615
|
+
clearButton.className = 'stub-clear';
|
|
616
|
+
clearButton.type = 'button';
|
|
617
|
+
clearButton.innerHTML = CLEAR_ICON_SVG;
|
|
618
|
+
clearButton.addEventListener('click', (e) => this.handleClearClick(e));
|
|
619
|
+
iconContainer.appendChild(clearButton);
|
|
620
|
+
}
|
|
621
|
+
const calendarIcon = document.createElement('span');
|
|
622
|
+
calendarIcon.className = 'stub-arrow';
|
|
623
|
+
calendarIcon.innerHTML = CALENDAR_ICON_SVG;
|
|
624
|
+
iconContainer.appendChild(calendarIcon);
|
|
625
|
+
stub.addEventListener('click', (e) => this.handleStubClick(e));
|
|
626
|
+
stub.appendChild(startSlot);
|
|
627
|
+
stub.appendChild(displayText);
|
|
628
|
+
stub.appendChild(iconContainer);
|
|
629
|
+
return stub;
|
|
630
|
+
}
|
|
631
|
+
createTimeInputElement() {
|
|
632
|
+
const input = document.createElement('input');
|
|
633
|
+
input.type = 'text';
|
|
634
|
+
input.className = 'time-input';
|
|
635
|
+
input.placeholder = 'HH:mm';
|
|
636
|
+
input.autocomplete = 'off';
|
|
637
|
+
input.maxLength = 5;
|
|
638
|
+
return input;
|
|
639
|
+
}
|
|
640
|
+
renderTimeSection() {
|
|
641
|
+
const timeSection = document.createElement('div');
|
|
642
|
+
timeSection.className = 'time-section';
|
|
643
|
+
const timeLabel = document.createElement('label');
|
|
644
|
+
timeLabel.className = 'time-label';
|
|
645
|
+
timeLabel.textContent = 'Time:';
|
|
646
|
+
const timeInputElement = this.createTimeInputElement();
|
|
647
|
+
const hour = this._state.hour || 0;
|
|
648
|
+
const minute = this._state.minute || 0;
|
|
649
|
+
this._timeInput = new TimeInput(timeInputElement, this, hour, minute);
|
|
650
|
+
const timeIcon = document.createElement('span');
|
|
651
|
+
timeIcon.className = 'time-icon';
|
|
652
|
+
timeIcon.innerHTML = SCHEDULE_ICON_SVG;
|
|
653
|
+
timeSection.appendChild(timeLabel);
|
|
654
|
+
timeSection.appendChild(timeInputElement);
|
|
655
|
+
timeSection.appendChild(timeIcon);
|
|
656
|
+
return timeSection;
|
|
657
|
+
}
|
|
658
|
+
renderCalendarDropdown() {
|
|
659
|
+
const dialog = document.createElement('dialog');
|
|
660
|
+
dialog.className = 'calendar-dialog';
|
|
661
|
+
const contentWrapper = document.createElement('div');
|
|
662
|
+
contentWrapper.className = 'calendar-content';
|
|
663
|
+
const calendar = document.createElement('ty-calendar');
|
|
664
|
+
if (this._state.year && this._state.month && this._state.day) {
|
|
665
|
+
calendar.setAttribute('year', this._state.year.toString());
|
|
666
|
+
calendar.setAttribute('month', this._state.month.toString());
|
|
667
|
+
calendar.setAttribute('day', this._state.day.toString());
|
|
668
|
+
}
|
|
669
|
+
const locale = getEffectiveLocale(this, this.getProperty('locale'));
|
|
670
|
+
if (locale) {
|
|
671
|
+
calendar.setAttribute('locale', locale);
|
|
672
|
+
}
|
|
673
|
+
const min = this.getProperty('min');
|
|
674
|
+
const max = this.getProperty('max');
|
|
675
|
+
if (min)
|
|
676
|
+
calendar.setAttribute('min', min);
|
|
677
|
+
if (max)
|
|
678
|
+
calendar.setAttribute('max', max);
|
|
679
|
+
const flavor = this.getProperty('flavor');
|
|
680
|
+
if (flavor && flavor !== 'default')
|
|
681
|
+
calendar.setAttribute('flavor', flavor);
|
|
682
|
+
calendar.addEventListener('change', (e) => this.handleCalendarChange(e));
|
|
683
|
+
contentWrapper.appendChild(calendar);
|
|
684
|
+
if (this._state.withTime) {
|
|
685
|
+
contentWrapper.appendChild(this.renderTimeSection());
|
|
686
|
+
}
|
|
687
|
+
dialog.appendChild(contentWrapper);
|
|
688
|
+
dialog.addEventListener('close', () => {
|
|
689
|
+
this.updateState({ open: false });
|
|
690
|
+
});
|
|
691
|
+
return dialog;
|
|
692
|
+
}
|
|
693
|
+
renderNativeInput() {
|
|
694
|
+
const wrapper = document.createElement('div');
|
|
695
|
+
wrapper.className = 'dropdown-wrapper';
|
|
696
|
+
const stub = document.createElement('div');
|
|
697
|
+
stub.className = this.buildStubClasses();
|
|
698
|
+
const isDisabled = this.getProperty('disabled');
|
|
699
|
+
if (isDisabled) {
|
|
700
|
+
stub.setAttribute('disabled', 'true');
|
|
701
|
+
}
|
|
702
|
+
const input = document.createElement('input');
|
|
703
|
+
input.className = 'native-date-input';
|
|
704
|
+
input.type = this._state.withTime ? 'datetime-local' : 'date';
|
|
705
|
+
if (isDisabled)
|
|
706
|
+
input.disabled = true;
|
|
707
|
+
if (this.getProperty('required'))
|
|
708
|
+
input.required = true;
|
|
709
|
+
const minBound = this.getProperty('min');
|
|
710
|
+
const maxBound = this.getProperty('max');
|
|
711
|
+
if (minBound)
|
|
712
|
+
input.min = this._state.withTime ? `${minBound}T00:00` : minBound;
|
|
713
|
+
if (maxBound)
|
|
714
|
+
input.max = this._state.withTime ? `${maxBound}T23:59` : maxBound;
|
|
715
|
+
const localValue = componentsToLocalValue(this.getComponents(), this._state.withTime);
|
|
716
|
+
if (localValue) {
|
|
717
|
+
input.value = localValue;
|
|
718
|
+
}
|
|
719
|
+
const placeholder = this.getProperty('placeholder') || 'Select date...';
|
|
720
|
+
const formattedValue = this.getFormattedValue();
|
|
721
|
+
const placeholderEl = document.createElement('span');
|
|
722
|
+
placeholderEl.className = 'stub-text' + (formattedValue ? '' : ' placeholder');
|
|
723
|
+
placeholderEl.textContent = formattedValue || placeholder;
|
|
724
|
+
input.addEventListener('change', () => {
|
|
725
|
+
if (!input.value) {
|
|
726
|
+
this.clearValue();
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
const parsed = parseValue(input.value, this._state.withTime);
|
|
730
|
+
if (parsed) {
|
|
731
|
+
this.updateState(parsed, true);
|
|
732
|
+
this.emitChangeEvent(parsed, 'selection');
|
|
733
|
+
}
|
|
734
|
+
});
|
|
735
|
+
const startSlot = document.createElement('slot');
|
|
736
|
+
startSlot.name = 'start';
|
|
737
|
+
stub.appendChild(startSlot);
|
|
738
|
+
stub.appendChild(input);
|
|
739
|
+
stub.appendChild(placeholderEl);
|
|
740
|
+
const iconContainer = document.createElement('div');
|
|
741
|
+
iconContainer.className = 'stub-icons';
|
|
742
|
+
const clearable = this.getProperty('clearable');
|
|
743
|
+
if (clearable && localValue && !isDisabled) {
|
|
744
|
+
const clearButton = document.createElement('button');
|
|
745
|
+
clearButton.className = 'stub-clear';
|
|
746
|
+
clearButton.type = 'button';
|
|
747
|
+
clearButton.innerHTML = CLEAR_ICON_SVG;
|
|
748
|
+
clearButton.addEventListener('click', (e) => this.handleClearClick(e));
|
|
749
|
+
iconContainer.appendChild(clearButton);
|
|
750
|
+
}
|
|
751
|
+
const calendarIcon = document.createElement('span');
|
|
752
|
+
calendarIcon.className = 'stub-arrow';
|
|
753
|
+
calendarIcon.innerHTML = CALENDAR_ICON_SVG;
|
|
754
|
+
iconContainer.appendChild(calendarIcon);
|
|
755
|
+
stub.appendChild(iconContainer);
|
|
756
|
+
wrapper.appendChild(stub);
|
|
757
|
+
return wrapper;
|
|
758
|
+
}
|
|
759
|
+
calculateCalendarPosition() {
|
|
760
|
+
if (!this.shadowRoot)
|
|
761
|
+
return;
|
|
762
|
+
if (isMobileTouch())
|
|
763
|
+
return;
|
|
764
|
+
const stub = this.shadowRoot.querySelector('.date-picker-stub');
|
|
765
|
+
const dialog = this.shadowRoot.querySelector('.calendar-dialog');
|
|
766
|
+
if (!stub || !dialog)
|
|
767
|
+
return;
|
|
768
|
+
const stubRect = stub.getBoundingClientRect();
|
|
769
|
+
const dialogRect = dialog.getBoundingClientRect();
|
|
770
|
+
const estimatedHeight = dialogRect.height || 400;
|
|
771
|
+
const popupWidth = dialogRect.width || 320;
|
|
772
|
+
const wrapPadding = 8;
|
|
773
|
+
const anchored = placementToAnchored(this.getProperty('placement'));
|
|
774
|
+
const pos = computeAnchoredPosition({
|
|
775
|
+
anchorRect: stubRect,
|
|
776
|
+
popupWidth,
|
|
777
|
+
popupHeight: estimatedHeight,
|
|
778
|
+
gap: 0,
|
|
779
|
+
align: anchored.align,
|
|
780
|
+
side: anchored.side,
|
|
781
|
+
});
|
|
782
|
+
const positionBelow = pos.below;
|
|
783
|
+
const x = pos.x - wrapPadding;
|
|
784
|
+
const y = positionBelow ? pos.topY : pos.bottomY;
|
|
785
|
+
this.style.setProperty('--calendar-x', `${x}px`);
|
|
786
|
+
this.style.setProperty('--calendar-y', `${y}px`);
|
|
787
|
+
this.style.setProperty('--calendar-offset-x', '0px');
|
|
788
|
+
this.style.setProperty('--calendar-offset-y', '0px');
|
|
789
|
+
if (positionBelow) {
|
|
790
|
+
dialog.classList.add('position-below');
|
|
791
|
+
dialog.classList.remove('position-above');
|
|
792
|
+
}
|
|
793
|
+
else {
|
|
794
|
+
dialog.classList.add('position-above');
|
|
795
|
+
dialog.classList.remove('position-below');
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
renderContainer() {
|
|
799
|
+
const container = document.createElement('div');
|
|
800
|
+
container.className = 'dropdown-container';
|
|
801
|
+
const label = this.getProperty('label');
|
|
802
|
+
if (label) {
|
|
803
|
+
const labelEl = document.createElement('label');
|
|
804
|
+
labelEl.className = 'ty-field-label';
|
|
805
|
+
labelEl.id = `date-picker-label-${getElementHash(this)}`;
|
|
806
|
+
labelEl.innerHTML = label + (this.getProperty('required') ? '<span class="required-icon">*</span>' : '');
|
|
807
|
+
container.appendChild(labelEl);
|
|
808
|
+
}
|
|
809
|
+
return container;
|
|
810
|
+
}
|
|
811
|
+
render() {
|
|
812
|
+
if (!this.shadowRoot)
|
|
813
|
+
return;
|
|
814
|
+
this.updateValidity();
|
|
815
|
+
ensureStyles(this.shadowRoot, { css: datePickerStyles, id: 'ty-date-picker' });
|
|
816
|
+
if (isMobileTouch()) {
|
|
817
|
+
const existingNativeInput = this.shadowRoot.querySelector('.native-date-input');
|
|
818
|
+
if (existingNativeInput) {
|
|
819
|
+
this.updateDisplay();
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
this.shadowRoot.innerHTML = '';
|
|
823
|
+
const container = this.renderContainer();
|
|
824
|
+
container.appendChild(this.renderNativeInput());
|
|
825
|
+
this.shadowRoot.appendChild(container);
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
const existingDialog = this.shadowRoot.querySelector('.calendar-dialog');
|
|
829
|
+
const isDialogOpen = existingDialog && existingDialog.open;
|
|
830
|
+
if (isDialogOpen && this._state.open) {
|
|
831
|
+
this.updateDisplay();
|
|
832
|
+
this.calculateCalendarPosition();
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
this.shadowRoot.innerHTML = '';
|
|
836
|
+
const container = this.renderContainer();
|
|
837
|
+
const wrapper = document.createElement('div');
|
|
838
|
+
wrapper.className = 'dropdown-wrapper';
|
|
839
|
+
wrapper.appendChild(this.renderStub());
|
|
840
|
+
wrapper.appendChild(this.renderCalendarDropdown());
|
|
841
|
+
container.appendChild(wrapper);
|
|
842
|
+
this.shadowRoot.appendChild(container);
|
|
843
|
+
this.setupEventListeners();
|
|
844
|
+
}
|
|
845
|
+
updateDisplay() {
|
|
846
|
+
if (!this.shadowRoot)
|
|
847
|
+
return;
|
|
848
|
+
const formattedValue = this.getFormattedValue();
|
|
849
|
+
const placeholder = this.getProperty('placeholder') || 'Select date...';
|
|
850
|
+
const hasValue = this.hasDate();
|
|
851
|
+
const clearable = this.getProperty('clearable');
|
|
852
|
+
const isDisabled = this.getProperty('disabled');
|
|
853
|
+
const stubText = this.shadowRoot.querySelector('.stub-text');
|
|
854
|
+
if (stubText) {
|
|
855
|
+
stubText.textContent = formattedValue || placeholder;
|
|
856
|
+
stubText.classList.toggle('placeholder', !formattedValue);
|
|
857
|
+
}
|
|
858
|
+
const clearButton = this.shadowRoot.querySelector('.stub-clear');
|
|
859
|
+
if (clearButton) {
|
|
860
|
+
clearButton.style.display = (clearable && hasValue && !isDisabled) ? '' : 'none';
|
|
861
|
+
}
|
|
862
|
+
if (isMobileTouch()) {
|
|
863
|
+
const nativeInput = this.shadowRoot.querySelector('.native-date-input');
|
|
864
|
+
if (nativeInput) {
|
|
865
|
+
const localValue = componentsToLocalValue(this.getComponents(), this._state.withTime);
|
|
866
|
+
nativeInput.value = localValue || '';
|
|
867
|
+
}
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
const calendar = this.shadowRoot.querySelector('ty-calendar');
|
|
871
|
+
if (calendar && hasValue) {
|
|
872
|
+
calendar.setAttribute('year', this._state.year.toString());
|
|
873
|
+
calendar.setAttribute('month', this._state.month.toString());
|
|
874
|
+
calendar.setAttribute('day', this._state.day.toString());
|
|
875
|
+
}
|
|
876
|
+
if (calendar) {
|
|
877
|
+
const flavor = this.getProperty('flavor');
|
|
878
|
+
if (flavor && flavor !== 'default')
|
|
879
|
+
calendar.setAttribute('flavor', flavor);
|
|
880
|
+
else
|
|
881
|
+
calendar.removeAttribute('flavor');
|
|
882
|
+
const min = this.getProperty('min');
|
|
883
|
+
const max = this.getProperty('max');
|
|
884
|
+
if (min)
|
|
885
|
+
calendar.setAttribute('min', min);
|
|
886
|
+
else
|
|
887
|
+
calendar.removeAttribute('min');
|
|
888
|
+
if (max)
|
|
889
|
+
calendar.setAttribute('max', max);
|
|
890
|
+
else
|
|
891
|
+
calendar.removeAttribute('max');
|
|
892
|
+
}
|
|
893
|
+
if (this._timeInput && this._state.hour !== undefined && this._state.minute !== undefined) {
|
|
894
|
+
this._timeInput.setTime(this._state.hour, this._state.minute);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
setupEventListeners() {
|
|
898
|
+
if (this._clickListener) {
|
|
899
|
+
document.removeEventListener('click', this._clickListener);
|
|
900
|
+
}
|
|
901
|
+
if (this._keydownListener) {
|
|
902
|
+
document.removeEventListener('keydown', this._keydownListener);
|
|
903
|
+
}
|
|
904
|
+
this._clickListener = (e) => this.handleOutsideClick(e);
|
|
905
|
+
this._keydownListener = (e) => this.handleEscapeKey(e);
|
|
906
|
+
document.addEventListener('click', this._clickListener);
|
|
907
|
+
document.addEventListener('keydown', this._keydownListener);
|
|
908
|
+
const dialog = this.shadowRoot?.querySelector('.calendar-dialog');
|
|
909
|
+
if (dialog) {
|
|
910
|
+
this._dialogClickListener = (e) => this.handleDialogClick(e);
|
|
911
|
+
dialog.addEventListener('click', this._dialogClickListener);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
handleStubClick(event) {
|
|
915
|
+
event.preventDefault();
|
|
916
|
+
const dialog = this.shadowRoot?.querySelector('.calendar-dialog');
|
|
917
|
+
if (this._state.open && (!dialog || !dialog.open)) {
|
|
918
|
+
this.updateState({ open: false }, true);
|
|
919
|
+
unlockScroll(`date-picker-${this.id || this.toString()}`);
|
|
920
|
+
}
|
|
921
|
+
const disabled = this.getProperty('disabled');
|
|
922
|
+
if (!disabled && Date.now() - this._closeTimestamp > 300) {
|
|
923
|
+
this.openDropdown();
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
clearValue() {
|
|
927
|
+
this.updateState({
|
|
928
|
+
year: undefined,
|
|
929
|
+
month: undefined,
|
|
930
|
+
day: undefined,
|
|
931
|
+
hour: undefined,
|
|
932
|
+
minute: undefined,
|
|
933
|
+
}, true);
|
|
934
|
+
this.value = null;
|
|
935
|
+
this.emitChangeEvent(null, 'clear');
|
|
936
|
+
this.render();
|
|
937
|
+
}
|
|
938
|
+
handleClearClick(event) {
|
|
939
|
+
event.preventDefault();
|
|
940
|
+
event.stopPropagation();
|
|
941
|
+
this.clearValue();
|
|
942
|
+
}
|
|
943
|
+
handleCalendarChange(event) {
|
|
944
|
+
event.preventDefault();
|
|
945
|
+
event.stopPropagation();
|
|
946
|
+
const customEvent = event;
|
|
947
|
+
const detail = customEvent.detail;
|
|
948
|
+
const dayContext = detail.dayContext;
|
|
949
|
+
if (!dayContext)
|
|
950
|
+
return;
|
|
951
|
+
const newComponents = {
|
|
952
|
+
year: dayContext.year,
|
|
953
|
+
month: dayContext.month,
|
|
954
|
+
day: dayContext.dayInMonth,
|
|
955
|
+
hour: this._state.hour,
|
|
956
|
+
minute: this._state.minute,
|
|
957
|
+
};
|
|
958
|
+
this.updateState(newComponents, true);
|
|
959
|
+
this.emitChangeEvent(newComponents, 'selection');
|
|
960
|
+
if (!this._state.withTime) {
|
|
961
|
+
this.closeDropdown();
|
|
962
|
+
}
|
|
963
|
+
else {
|
|
964
|
+
requestAnimationFrame(() => {
|
|
965
|
+
if (!this.shadowRoot)
|
|
966
|
+
return;
|
|
967
|
+
const timeInput = this.shadowRoot.querySelector('.time-input');
|
|
968
|
+
if (timeInput) {
|
|
969
|
+
timeInput.focus();
|
|
970
|
+
}
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
handleDialogClick(event) {
|
|
975
|
+
if (!this.shadowRoot)
|
|
976
|
+
return;
|
|
977
|
+
const dialog = this.shadowRoot.querySelector('.calendar-dialog');
|
|
978
|
+
const content = this.shadowRoot.querySelector('.calendar-content');
|
|
979
|
+
if (event.target === dialog && this._state.open && content && !content.contains(event.target)) {
|
|
980
|
+
event.preventDefault();
|
|
981
|
+
event.stopPropagation();
|
|
982
|
+
this.closeDropdown();
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
handleOutsideClick(event) {
|
|
986
|
+
if (!this.shadowRoot)
|
|
987
|
+
return;
|
|
988
|
+
const target = event.target;
|
|
989
|
+
const dialog = this.shadowRoot.querySelector('.calendar-dialog');
|
|
990
|
+
if (this._state.open && dialog && !this.contains(target) && !dialog.contains(target)) {
|
|
991
|
+
this.closeDropdown();
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
handleEscapeKey(event) {
|
|
995
|
+
const keyboardEvent = event;
|
|
996
|
+
if (keyboardEvent.key === 'Escape' && this._state.open) {
|
|
997
|
+
keyboardEvent.preventDefault();
|
|
998
|
+
this.closeDropdown();
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
openDropdown() {
|
|
1002
|
+
if (this._state.open)
|
|
1003
|
+
return;
|
|
1004
|
+
if (isMobileTouch())
|
|
1005
|
+
return;
|
|
1006
|
+
this.updateState({ open: true });
|
|
1007
|
+
this.dispatchEvent(new CustomEvent('open'));
|
|
1008
|
+
this.render();
|
|
1009
|
+
requestAnimationFrame(() => {
|
|
1010
|
+
if (!this.shadowRoot)
|
|
1011
|
+
return;
|
|
1012
|
+
const dialog = this.shadowRoot.querySelector('.calendar-dialog');
|
|
1013
|
+
if (dialog) {
|
|
1014
|
+
const pickerId = `date-picker-${this.id || this.toString()}`;
|
|
1015
|
+
lockScroll(pickerId);
|
|
1016
|
+
dialog.showModal();
|
|
1017
|
+
this.calculateCalendarPosition();
|
|
1018
|
+
dialog.classList.add('open');
|
|
1019
|
+
const stubEl = this.shadowRoot.querySelector('.date-picker-stub');
|
|
1020
|
+
if (stubEl)
|
|
1021
|
+
stubEl.setAttribute('aria-expanded', 'true');
|
|
1022
|
+
const focusedElement = this.shadowRoot.activeElement;
|
|
1023
|
+
if (focusedElement) {
|
|
1024
|
+
focusedElement.blur();
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
closeDropdown() {
|
|
1030
|
+
if (!this._state.open)
|
|
1031
|
+
return;
|
|
1032
|
+
this._closeTimestamp = Date.now();
|
|
1033
|
+
const pickerId = `date-picker-${this.id || this.toString()}`;
|
|
1034
|
+
this.updateState({ open: false }, true);
|
|
1035
|
+
unlockScroll(pickerId);
|
|
1036
|
+
if (this.shadowRoot) {
|
|
1037
|
+
const dialog = this.shadowRoot.querySelector('.calendar-dialog');
|
|
1038
|
+
if (dialog) {
|
|
1039
|
+
dialog.classList.remove('position-above', 'position-below', 'open');
|
|
1040
|
+
dialog.close();
|
|
1041
|
+
}
|
|
1042
|
+
const stubEl = this.shadowRoot.querySelector('.date-picker-stub');
|
|
1043
|
+
if (stubEl)
|
|
1044
|
+
stubEl.setAttribute('aria-expanded', 'false');
|
|
1045
|
+
}
|
|
1046
|
+
this.dispatchEvent(new CustomEvent('close'));
|
|
1047
|
+
this.render();
|
|
1048
|
+
}
|
|
1049
|
+
cleanup() {
|
|
1050
|
+
if (this._clickListener) {
|
|
1051
|
+
document.removeEventListener('click', this._clickListener);
|
|
1052
|
+
}
|
|
1053
|
+
if (this._keydownListener) {
|
|
1054
|
+
document.removeEventListener('keydown', this._keydownListener);
|
|
1055
|
+
}
|
|
1056
|
+
if (this._dialogClickListener && this.shadowRoot) {
|
|
1057
|
+
const dialog = this.shadowRoot.querySelector('.calendar-dialog');
|
|
1058
|
+
if (dialog) {
|
|
1059
|
+
dialog.removeEventListener('click', this._dialogClickListener);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
if (this._state.open) {
|
|
1063
|
+
const pickerId = `date-picker-${this.id || this.toString()}`;
|
|
1064
|
+
unlockScroll(pickerId);
|
|
1065
|
+
}
|
|
1066
|
+
this._clickListener = undefined;
|
|
1067
|
+
this._keydownListener = undefined;
|
|
1068
|
+
this._dialogClickListener = undefined;
|
|
1069
|
+
this._timeInput = undefined;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1590
1072
|
TyDatePicker.properties = {
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
default: null // null when no date selected
|
|
1689
|
-
},
|
|
1690
|
-
// With-time property (high complexity - affects parsing and rendering)
|
|
1691
|
-
'with-time': {
|
|
1692
|
-
type: 'boolean',
|
|
1693
|
-
visual: true,
|
|
1694
|
-
default: false
|
|
1695
|
-
}
|
|
1073
|
+
placement: {
|
|
1074
|
+
type: 'string',
|
|
1075
|
+
visual: true,
|
|
1076
|
+
default: '',
|
|
1077
|
+
},
|
|
1078
|
+
size: {
|
|
1079
|
+
type: 'string',
|
|
1080
|
+
visual: true,
|
|
1081
|
+
default: DEFAULT_SIZE,
|
|
1082
|
+
validate: (v) => ["xs", "sm", "md", "lg", "xl"].includes(v),
|
|
1083
|
+
coerce: (v) => {
|
|
1084
|
+
if (!["xs", "sm", "md", "lg", "xl"].includes(v)) {
|
|
1085
|
+
console.warn(`[ty-date-picker] Invalid size '${v}'. Using '${DEFAULT_SIZE}'.`);
|
|
1086
|
+
return DEFAULT_SIZE;
|
|
1087
|
+
}
|
|
1088
|
+
return v;
|
|
1089
|
+
}
|
|
1090
|
+
},
|
|
1091
|
+
flavor: {
|
|
1092
|
+
type: 'string',
|
|
1093
|
+
visual: true,
|
|
1094
|
+
default: 'default'
|
|
1095
|
+
},
|
|
1096
|
+
label: {
|
|
1097
|
+
type: 'string',
|
|
1098
|
+
visual: true,
|
|
1099
|
+
default: ''
|
|
1100
|
+
},
|
|
1101
|
+
placeholder: {
|
|
1102
|
+
type: 'string',
|
|
1103
|
+
visual: true,
|
|
1104
|
+
default: 'Select date...'
|
|
1105
|
+
},
|
|
1106
|
+
name: {
|
|
1107
|
+
type: 'string',
|
|
1108
|
+
visual: false,
|
|
1109
|
+
default: ''
|
|
1110
|
+
},
|
|
1111
|
+
min: {
|
|
1112
|
+
type: 'string',
|
|
1113
|
+
visual: true,
|
|
1114
|
+
default: ''
|
|
1115
|
+
},
|
|
1116
|
+
max: {
|
|
1117
|
+
type: 'string',
|
|
1118
|
+
visual: true,
|
|
1119
|
+
default: ''
|
|
1120
|
+
},
|
|
1121
|
+
required: {
|
|
1122
|
+
type: 'boolean',
|
|
1123
|
+
visual: true,
|
|
1124
|
+
default: false
|
|
1125
|
+
},
|
|
1126
|
+
disabled: {
|
|
1127
|
+
type: 'boolean',
|
|
1128
|
+
visual: true,
|
|
1129
|
+
default: false
|
|
1130
|
+
},
|
|
1131
|
+
clearable: {
|
|
1132
|
+
type: 'boolean',
|
|
1133
|
+
visual: true,
|
|
1134
|
+
default: true,
|
|
1135
|
+
aliases: {
|
|
1136
|
+
'not-clearable': false
|
|
1137
|
+
}
|
|
1138
|
+
},
|
|
1139
|
+
format: {
|
|
1140
|
+
type: 'string',
|
|
1141
|
+
visual: true,
|
|
1142
|
+
default: 'long',
|
|
1143
|
+
validate: (v) => ['short', 'medium', 'long', 'full'].includes(v),
|
|
1144
|
+
coerce: (v) => {
|
|
1145
|
+
const validFormats = ['short', 'medium', 'long', 'full'];
|
|
1146
|
+
if (!validFormats.includes(v)) {
|
|
1147
|
+
console.warn(`[ty-date-picker] Invalid format '${v}'. Using 'long'.`);
|
|
1148
|
+
return 'long';
|
|
1149
|
+
}
|
|
1150
|
+
return v;
|
|
1151
|
+
}
|
|
1152
|
+
},
|
|
1153
|
+
locale: {
|
|
1154
|
+
type: 'string',
|
|
1155
|
+
visual: true,
|
|
1156
|
+
default: ''
|
|
1157
|
+
},
|
|
1158
|
+
value: {
|
|
1159
|
+
type: 'string',
|
|
1160
|
+
visual: true,
|
|
1161
|
+
formValue: true,
|
|
1162
|
+
emitChange: false,
|
|
1163
|
+
default: null
|
|
1164
|
+
},
|
|
1165
|
+
'with-time': {
|
|
1166
|
+
type: 'boolean',
|
|
1167
|
+
visual: true,
|
|
1168
|
+
default: false
|
|
1169
|
+
}
|
|
1696
1170
|
};
|
|
1697
|
-
/**
|
|
1698
|
-
* Form-associated custom element
|
|
1699
|
-
*/
|
|
1700
1171
|
TyDatePicker.formAssociated = true;
|
|
1701
|
-
// Register custom element
|
|
1702
1172
|
if (!customElements.get('ty-date-picker')) {
|
|
1703
|
-
|
|
1173
|
+
customElements.define('ty-date-picker', TyDatePicker);
|
|
1704
1174
|
}
|
|
1705
|
-
//# sourceMappingURL=date-picker.js.map
|