tyrell-components 1.0.0-RC10
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/LICENSE +21 -0
- package/README.md +221 -0
- package/css/tyrell-brand.css +767 -0
- package/css/tyrell.css +1679 -0
- package/dist/tyrell-brand.css +767 -0
- package/dist/tyrell.css +1679 -0
- package/dist/tyrell.js +2 -0
- package/lib/base/ty-component.d.ts +133 -0
- package/lib/base/ty-component.d.ts.map +1 -0
- package/lib/base/ty-component.js +297 -0
- package/lib/base/ty-component.js.map +1 -0
- package/lib/components/button.d.ts +135 -0
- package/lib/components/button.d.ts.map +1 -0
- package/lib/components/button.js +277 -0
- package/lib/components/button.js.map +1 -0
- package/lib/components/calendar-month.d.ts +132 -0
- package/lib/components/calendar-month.d.ts.map +1 -0
- package/lib/components/calendar-month.js +440 -0
- package/lib/components/calendar-month.js.map +1 -0
- package/lib/components/calendar-navigation.d.ts +137 -0
- package/lib/components/calendar-navigation.d.ts.map +1 -0
- package/lib/components/calendar-navigation.js +366 -0
- package/lib/components/calendar-navigation.js.map +1 -0
- package/lib/components/calendar.d.ts +166 -0
- package/lib/components/calendar.d.ts.map +1 -0
- package/lib/components/calendar.js +774 -0
- package/lib/components/calendar.js.map +1 -0
- package/lib/components/checkbox.d.ts +189 -0
- package/lib/components/checkbox.d.ts.map +1 -0
- package/lib/components/checkbox.js +400 -0
- package/lib/components/checkbox.js.map +1 -0
- package/lib/components/copy.d.ts +180 -0
- package/lib/components/copy.d.ts.map +1 -0
- package/lib/components/copy.js +393 -0
- package/lib/components/copy.js.map +1 -0
- package/lib/components/date-picker.d.ts +379 -0
- package/lib/components/date-picker.d.ts.map +1 -0
- package/lib/components/date-picker.js +1586 -0
- package/lib/components/date-picker.js.map +1 -0
- package/lib/components/dropdown.d.ts +424 -0
- package/lib/components/dropdown.d.ts.map +1 -0
- package/lib/components/dropdown.js +1640 -0
- package/lib/components/dropdown.js.map +1 -0
- package/lib/components/file-upload.d.ts +121 -0
- package/lib/components/file-upload.d.ts.map +1 -0
- package/lib/components/file-upload.js +441 -0
- package/lib/components/file-upload.js.map +1 -0
- package/lib/components/icon.d.ts +118 -0
- package/lib/components/icon.d.ts.map +1 -0
- package/lib/components/icon.js +245 -0
- package/lib/components/icon.js.map +1 -0
- package/lib/components/input.d.ts +270 -0
- package/lib/components/input.d.ts.map +1 -0
- package/lib/components/input.js +721 -0
- package/lib/components/input.js.map +1 -0
- package/lib/components/modal.d.ts +78 -0
- package/lib/components/modal.d.ts.map +1 -0
- package/lib/components/modal.js +497 -0
- package/lib/components/modal.js.map +1 -0
- package/lib/components/multiselect.d.ts +397 -0
- package/lib/components/multiselect.d.ts.map +1 -0
- package/lib/components/multiselect.js +1595 -0
- package/lib/components/multiselect.js.map +1 -0
- package/lib/components/option.d.ts +66 -0
- package/lib/components/option.d.ts.map +1 -0
- package/lib/components/option.js +314 -0
- package/lib/components/option.js.map +1 -0
- package/lib/components/popup.d.ts +43 -0
- package/lib/components/popup.d.ts.map +1 -0
- package/lib/components/popup.js +380 -0
- package/lib/components/popup.js.map +1 -0
- package/lib/components/radio.d.ts +198 -0
- package/lib/components/radio.d.ts.map +1 -0
- package/lib/components/radio.js +437 -0
- package/lib/components/radio.js.map +1 -0
- package/lib/components/resize-observer.d.ts +48 -0
- package/lib/components/resize-observer.d.ts.map +1 -0
- package/lib/components/resize-observer.js +108 -0
- package/lib/components/resize-observer.js.map +1 -0
- package/lib/components/scroll-container.d.ts +51 -0
- package/lib/components/scroll-container.d.ts.map +1 -0
- package/lib/components/scroll-container.js +239 -0
- package/lib/components/scroll-container.js.map +1 -0
- package/lib/components/step.d.ts +26 -0
- package/lib/components/step.d.ts.map +1 -0
- package/lib/components/step.js +75 -0
- package/lib/components/step.js.map +1 -0
- package/lib/components/switch.d.ts +111 -0
- package/lib/components/switch.d.ts.map +1 -0
- package/lib/components/switch.js +240 -0
- package/lib/components/switch.js.map +1 -0
- package/lib/components/tab.d.ts +23 -0
- package/lib/components/tab.d.ts.map +1 -0
- package/lib/components/tab.js +76 -0
- package/lib/components/tab.js.map +1 -0
- package/lib/components/tabs.d.ts +93 -0
- package/lib/components/tabs.d.ts.map +1 -0
- package/lib/components/tabs.js +653 -0
- package/lib/components/tabs.js.map +1 -0
- package/lib/components/tag.d.ts +144 -0
- package/lib/components/tag.d.ts.map +1 -0
- package/lib/components/tag.js +316 -0
- package/lib/components/tag.js.map +1 -0
- package/lib/components/textarea.d.ts +241 -0
- package/lib/components/textarea.d.ts.map +1 -0
- package/lib/components/textarea.js +585 -0
- package/lib/components/textarea.js.map +1 -0
- package/lib/components/tooltip.d.ts +40 -0
- package/lib/components/tooltip.d.ts.map +1 -0
- package/lib/components/tooltip.js +439 -0
- package/lib/components/tooltip.js.map +1 -0
- package/lib/components/wizard.d.ts +86 -0
- package/lib/components/wizard.d.ts.map +1 -0
- package/lib/components/wizard.js +636 -0
- package/lib/components/wizard.js.map +1 -0
- package/lib/icons/fontawesome/brands.d.ts +557 -0
- package/lib/icons/fontawesome/brands.d.ts.map +1 -0
- package/lib/icons/fontawesome/brands.js +557 -0
- package/lib/icons/fontawesome/brands.js.map +1 -0
- package/lib/icons/fontawesome/regular.d.ts +281 -0
- package/lib/icons/fontawesome/regular.d.ts.map +1 -0
- package/lib/icons/fontawesome/regular.js +281 -0
- package/lib/icons/fontawesome/regular.js.map +1 -0
- package/lib/icons/fontawesome/solid.d.ts +1992 -0
- package/lib/icons/fontawesome/solid.d.ts.map +1 -0
- package/lib/icons/fontawesome/solid.js +1992 -0
- package/lib/icons/fontawesome/solid.js.map +1 -0
- package/lib/icons/heroicons/micro.d.ts +324 -0
- package/lib/icons/heroicons/micro.d.ts.map +1 -0
- package/lib/icons/heroicons/micro.js +1032 -0
- package/lib/icons/heroicons/micro.js.map +1 -0
- package/lib/icons/heroicons/mini.d.ts +332 -0
- package/lib/icons/heroicons/mini.d.ts.map +1 -0
- package/lib/icons/heroicons/mini.js +1038 -0
- package/lib/icons/heroicons/mini.js.map +1 -0
- package/lib/icons/heroicons/outline.d.ts +332 -0
- package/lib/icons/heroicons/outline.d.ts.map +1 -0
- package/lib/icons/heroicons/outline.js +993 -0
- package/lib/icons/heroicons/outline.js.map +1 -0
- package/lib/icons/heroicons/solid.d.ts +332 -0
- package/lib/icons/heroicons/solid.d.ts.map +1 -0
- package/lib/icons/heroicons/solid.js +1063 -0
- package/lib/icons/heroicons/solid.js.map +1 -0
- package/lib/icons/lucide.d.ts +1872 -0
- package/lib/icons/lucide.d.ts.map +1 -0
- package/lib/icons/lucide.js +28212 -0
- package/lib/icons/lucide.js.map +1 -0
- package/lib/icons/material/filled.d.ts +2180 -0
- package/lib/icons/material/filled.d.ts.map +1 -0
- package/lib/icons/material/filled.js +14003 -0
- package/lib/icons/material/filled.js.map +1 -0
- package/lib/icons/material/outlined.d.ts +2142 -0
- package/lib/icons/material/outlined.d.ts.map +1 -0
- package/lib/icons/material/outlined.js +14545 -0
- package/lib/icons/material/outlined.js.map +1 -0
- package/lib/icons/material/round.d.ts +2147 -0
- package/lib/icons/material/round.d.ts.map +1 -0
- package/lib/icons/material/round.js +14779 -0
- package/lib/icons/material/round.js.map +1 -0
- package/lib/icons/material/sharp.d.ts +2147 -0
- package/lib/icons/material/sharp.d.ts.map +1 -0
- package/lib/icons/material/sharp.js +14189 -0
- package/lib/icons/material/sharp.js.map +1 -0
- package/lib/icons/material/two-tone.d.ts +2185 -0
- package/lib/icons/material/two-tone.d.ts.map +1 -0
- package/lib/icons/material/two-tone.js +17152 -0
- package/lib/icons/material/two-tone.js.map +1 -0
- package/lib/index.d.ts +86 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +78 -0
- package/lib/index.js.map +1 -0
- package/lib/styles/button.d.ts +14 -0
- package/lib/styles/button.d.ts.map +1 -0
- package/lib/styles/button.js +498 -0
- package/lib/styles/button.js.map +1 -0
- package/lib/styles/calendar-month.d.ts +6 -0
- package/lib/styles/calendar-month.d.ts.map +1 -0
- package/lib/styles/calendar-month.js +275 -0
- package/lib/styles/calendar-month.js.map +1 -0
- package/lib/styles/calendar-navigation.d.ts +6 -0
- package/lib/styles/calendar-navigation.d.ts.map +1 -0
- package/lib/styles/calendar-navigation.js +143 -0
- package/lib/styles/calendar-navigation.js.map +1 -0
- package/lib/styles/calendar.d.ts +6 -0
- package/lib/styles/calendar.d.ts.map +1 -0
- package/lib/styles/calendar.js +28 -0
- package/lib/styles/calendar.js.map +1 -0
- package/lib/styles/checkbox.d.ts +9 -0
- package/lib/styles/checkbox.d.ts.map +1 -0
- package/lib/styles/checkbox.js +19 -0
- package/lib/styles/checkbox.js.map +1 -0
- package/lib/styles/copy.d.ts +7 -0
- package/lib/styles/copy.d.ts.map +1 -0
- package/lib/styles/copy.js +94 -0
- package/lib/styles/copy.js.map +1 -0
- package/lib/styles/custom-scrollbar.d.ts +6 -0
- package/lib/styles/custom-scrollbar.d.ts.map +1 -0
- package/lib/styles/custom-scrollbar.js +157 -0
- package/lib/styles/custom-scrollbar.js.map +1 -0
- package/lib/styles/date-picker.d.ts +6 -0
- package/lib/styles/date-picker.d.ts.map +1 -0
- package/lib/styles/date-picker.js +438 -0
- package/lib/styles/date-picker.js.map +1 -0
- package/lib/styles/dropdown.d.ts +12 -0
- package/lib/styles/dropdown.d.ts.map +1 -0
- package/lib/styles/dropdown.js +1081 -0
- package/lib/styles/dropdown.js.map +1 -0
- package/lib/styles/file-upload.d.ts +2 -0
- package/lib/styles/file-upload.d.ts.map +1 -0
- package/lib/styles/file-upload.js +241 -0
- package/lib/styles/file-upload.js.map +1 -0
- package/lib/styles/icon.d.ts +6 -0
- package/lib/styles/icon.d.ts.map +1 -0
- package/lib/styles/icon.js +241 -0
- package/lib/styles/icon.js.map +1 -0
- package/lib/styles/input.d.ts +7 -0
- package/lib/styles/input.d.ts.map +1 -0
- package/lib/styles/input.js +685 -0
- package/lib/styles/input.js.map +1 -0
- package/lib/styles/modal.d.ts +8 -0
- package/lib/styles/modal.d.ts.map +1 -0
- package/lib/styles/modal.js +134 -0
- package/lib/styles/modal.js.map +1 -0
- package/lib/styles/multiselect.d.ts +6 -0
- package/lib/styles/multiselect.d.ts.map +1 -0
- package/lib/styles/multiselect.js +825 -0
- package/lib/styles/multiselect.js.map +1 -0
- package/lib/styles/option.d.ts +6 -0
- package/lib/styles/option.d.ts.map +1 -0
- package/lib/styles/option.js +116 -0
- package/lib/styles/option.js.map +1 -0
- package/lib/styles/popup.d.ts +8 -0
- package/lib/styles/popup.d.ts.map +1 -0
- package/lib/styles/popup.js +95 -0
- package/lib/styles/popup.js.map +1 -0
- package/lib/styles/radio.d.ts +8 -0
- package/lib/styles/radio.d.ts.map +1 -0
- package/lib/styles/radio.js +160 -0
- package/lib/styles/radio.js.map +1 -0
- package/lib/styles/resize-observer.d.ts +6 -0
- package/lib/styles/resize-observer.d.ts.map +1 -0
- package/lib/styles/resize-observer.js +18 -0
- package/lib/styles/resize-observer.js.map +1 -0
- package/lib/styles/scroll-container.d.ts +6 -0
- package/lib/styles/scroll-container.d.ts.map +1 -0
- package/lib/styles/scroll-container.js +198 -0
- package/lib/styles/scroll-container.js.map +1 -0
- package/lib/styles/step.d.ts +5 -0
- package/lib/styles/step.d.ts.map +1 -0
- package/lib/styles/step.js +50 -0
- package/lib/styles/step.js.map +1 -0
- package/lib/styles/switch.d.ts +9 -0
- package/lib/styles/switch.d.ts.map +1 -0
- package/lib/styles/switch.js +100 -0
- package/lib/styles/switch.js.map +1 -0
- package/lib/styles/tab.d.ts +5 -0
- package/lib/styles/tab.d.ts.map +1 -0
- package/lib/styles/tab.js +51 -0
- package/lib/styles/tab.js.map +1 -0
- package/lib/styles/tabs.d.ts +13 -0
- package/lib/styles/tabs.d.ts.map +1 -0
- package/lib/styles/tabs.js +184 -0
- package/lib/styles/tabs.js.map +1 -0
- package/lib/styles/tag.d.ts +6 -0
- package/lib/styles/tag.d.ts.map +1 -0
- package/lib/styles/tag.js +409 -0
- package/lib/styles/tag.js.map +1 -0
- package/lib/styles/textarea.d.ts +6 -0
- package/lib/styles/textarea.d.ts.map +1 -0
- package/lib/styles/textarea.js +350 -0
- package/lib/styles/textarea.js.map +1 -0
- package/lib/styles/tooltip.d.ts +9 -0
- package/lib/styles/tooltip.d.ts.map +1 -0
- package/lib/styles/tooltip.js +133 -0
- package/lib/styles/tooltip.js.map +1 -0
- package/lib/styles/wizard.d.ts +25 -0
- package/lib/styles/wizard.d.ts.map +1 -0
- package/lib/styles/wizard.js +348 -0
- package/lib/styles/wizard.js.map +1 -0
- package/lib/types/common.d.ts +143 -0
- package/lib/types/common.d.ts.map +1 -0
- package/lib/types/common.js +5 -0
- package/lib/types/common.js.map +1 -0
- package/lib/utils/calendar-utils.d.ts +176 -0
- package/lib/utils/calendar-utils.d.ts.map +1 -0
- package/lib/utils/calendar-utils.js +370 -0
- package/lib/utils/calendar-utils.js.map +1 -0
- package/lib/utils/custom-scrollbar.d.ts +82 -0
- package/lib/utils/custom-scrollbar.d.ts.map +1 -0
- package/lib/utils/custom-scrollbar.js +320 -0
- package/lib/utils/custom-scrollbar.js.map +1 -0
- package/lib/utils/icon-registry.d.ts +78 -0
- package/lib/utils/icon-registry.d.ts.map +1 -0
- package/lib/utils/icon-registry.js +304 -0
- package/lib/utils/icon-registry.js.map +1 -0
- package/lib/utils/loader-registry.d.ts +35 -0
- package/lib/utils/loader-registry.d.ts.map +1 -0
- package/lib/utils/loader-registry.js +43 -0
- package/lib/utils/loader-registry.js.map +1 -0
- package/lib/utils/locale.d.ts +136 -0
- package/lib/utils/locale.d.ts.map +1 -0
- package/lib/utils/locale.js +213 -0
- package/lib/utils/locale.js.map +1 -0
- package/lib/utils/mobile.d.ts +14 -0
- package/lib/utils/mobile.d.ts.map +1 -0
- package/lib/utils/mobile.js +21 -0
- package/lib/utils/mobile.js.map +1 -0
- package/lib/utils/number-format.d.ts +83 -0
- package/lib/utils/number-format.d.ts.map +1 -0
- package/lib/utils/number-format.js +143 -0
- package/lib/utils/number-format.js.map +1 -0
- package/lib/utils/parse-boolean.d.ts +39 -0
- package/lib/utils/parse-boolean.d.ts.map +1 -0
- package/lib/utils/parse-boolean.js +58 -0
- package/lib/utils/parse-boolean.js.map +1 -0
- package/lib/utils/positioning.d.ts +143 -0
- package/lib/utils/positioning.d.ts.map +1 -0
- package/lib/utils/positioning.js +308 -0
- package/lib/utils/positioning.js.map +1 -0
- package/lib/utils/property-capture.d.ts +132 -0
- package/lib/utils/property-capture.d.ts.map +1 -0
- package/lib/utils/property-capture.js +152 -0
- package/lib/utils/property-capture.js.map +1 -0
- package/lib/utils/property-manager.d.ts +90 -0
- package/lib/utils/property-manager.d.ts.map +1 -0
- package/lib/utils/property-manager.js +197 -0
- package/lib/utils/property-manager.js.map +1 -0
- package/lib/utils/resize-observer.d.ts +42 -0
- package/lib/utils/resize-observer.d.ts.map +1 -0
- package/lib/utils/resize-observer.js +71 -0
- package/lib/utils/resize-observer.js.map +1 -0
- package/lib/utils/scroll-lock.d.ts +79 -0
- package/lib/utils/scroll-lock.d.ts.map +1 -0
- package/lib/utils/scroll-lock.js +197 -0
- package/lib/utils/scroll-lock.js.map +1 -0
- package/lib/utils/styles.d.ts +27 -0
- package/lib/utils/styles.d.ts.map +1 -0
- package/lib/utils/styles.js +53 -0
- package/lib/utils/styles.js.map +1 -0
- package/lib/version.d.ts +8 -0
- package/lib/version.d.ts.map +1 -0
- package/lib/version.js +11 -0
- package/lib/version.js.map +1 -0
- package/package.json +163 -0
|
@@ -0,0 +1,1586 @@
|
|
|
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
|
+
import { ensureStyles } from '../utils/styles.js';
|
|
50
|
+
import { datePickerStyles } from '../styles/date-picker.js';
|
|
51
|
+
import { lockScroll, unlockScroll } from '../utils/scroll-lock.js';
|
|
52
|
+
import { getEffectiveLocale, observeLocaleChanges } from '../utils/locale.js';
|
|
53
|
+
import { isMobileTouch } from '../utils/mobile.js';
|
|
54
|
+
import { TyComponent } from '../base/ty-component.js';
|
|
55
|
+
// ============================================================================
|
|
56
|
+
// Constants
|
|
57
|
+
// ============================================================================
|
|
58
|
+
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>`;
|
|
59
|
+
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>`;
|
|
60
|
+
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>`;
|
|
61
|
+
// ============================================================================
|
|
62
|
+
// Helper Functions - Date Parsing & Conversion
|
|
63
|
+
// ============================================================================
|
|
64
|
+
/**
|
|
65
|
+
* Parse ANY input format into year/month/day/hour/minute components.
|
|
66
|
+
* Accepts:
|
|
67
|
+
* - UTC strings: '2024-09-21T08:30:00Z' or '2024-09-21T08:30:00.000Z'
|
|
68
|
+
* - Datetime-local: '2024-09-21T10:30'
|
|
69
|
+
* - Date only: '2024-09-21'
|
|
70
|
+
* - Timestamps: milliseconds since epoch
|
|
71
|
+
* - With timezone: '2024-09-21T10:30:00+02:00'
|
|
72
|
+
*
|
|
73
|
+
* Always extracts components in LOCAL timezone for display/editing.
|
|
74
|
+
*
|
|
75
|
+
* PORTED FROM: parse-value in date_picker.cljs
|
|
76
|
+
*/
|
|
77
|
+
function parseValue(value, withTime) {
|
|
78
|
+
if (!value)
|
|
79
|
+
return null;
|
|
80
|
+
let dateObj = null;
|
|
81
|
+
if (typeof value === 'string') {
|
|
82
|
+
// Date-only format: YYYY-MM-DD
|
|
83
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
84
|
+
const [yearStr, monthStr, dayStr] = value.split('-');
|
|
85
|
+
const year = parseInt(yearStr, 10);
|
|
86
|
+
const month = parseInt(monthStr, 10) - 1; // 0-based for JS Date
|
|
87
|
+
const day = parseInt(dayStr, 10);
|
|
88
|
+
// Create at midnight local time
|
|
89
|
+
dateObj = new Date(year, month, day, 0, 0, 0, 0);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
// Let JS Date handle datetime-local, UTC, with timezone
|
|
93
|
+
dateObj = new Date(value);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
else if (typeof value === 'number') {
|
|
97
|
+
// Numeric inputs (milliseconds)
|
|
98
|
+
dateObj = new Date(value);
|
|
99
|
+
}
|
|
100
|
+
if (!dateObj || isNaN(dateObj.getTime())) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
// Extract components in LOCAL timezone for display
|
|
104
|
+
const components = {
|
|
105
|
+
year: dateObj.getFullYear(),
|
|
106
|
+
month: dateObj.getMonth() + 1, // Convert to 1-based
|
|
107
|
+
day: dateObj.getDate(),
|
|
108
|
+
hour: withTime ? dateObj.getHours() : 0,
|
|
109
|
+
minute: withTime ? dateObj.getMinutes() : 0,
|
|
110
|
+
};
|
|
111
|
+
return components;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Convert internal components to Date object.
|
|
115
|
+
*
|
|
116
|
+
* PORTED FROM: components->date-object in date_picker.cljs
|
|
117
|
+
*/
|
|
118
|
+
function componentsToDateObject(components) {
|
|
119
|
+
const { year, month, day, hour, minute } = components;
|
|
120
|
+
if (!year || !month || !day)
|
|
121
|
+
return null;
|
|
122
|
+
// Create local date
|
|
123
|
+
return new Date(year, month - 1, // Convert to 0-based
|
|
124
|
+
day, hour || 0, minute || 0, 0, // seconds
|
|
125
|
+
0 // milliseconds
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Convert internal components to UTC ISO 8601 format.
|
|
130
|
+
*
|
|
131
|
+
* For date+time mode: Outputs full UTC timestamp with milliseconds
|
|
132
|
+
* Example: '2024-09-21T08:30:00.000Z'
|
|
133
|
+
*
|
|
134
|
+
* For date-only mode: Outputs UTC timestamp at midnight local time
|
|
135
|
+
* Example: '2024-09-20T22:00:00.000Z' (midnight Sept 21 CEST = 10pm Sept 20 UTC)
|
|
136
|
+
*
|
|
137
|
+
* Always returns UTC to ensure unambiguous server communication.
|
|
138
|
+
*
|
|
139
|
+
* PORTED FROM: components->output-value in date_picker.cljs
|
|
140
|
+
*/
|
|
141
|
+
function componentsToOutputValue(components) {
|
|
142
|
+
if (!components.year || !components.month || !components.day) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
const dateObj = componentsToDateObject(components);
|
|
146
|
+
return dateObj ? dateObj.toISOString() : null;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Convert internal components to local datetime-local format.
|
|
150
|
+
*
|
|
151
|
+
* For date+time mode: Outputs local datetime without timezone
|
|
152
|
+
* Example: '2024-09-21T10:30'
|
|
153
|
+
*
|
|
154
|
+
* For date-only mode: Outputs date only
|
|
155
|
+
* Example: '2024-09-21'
|
|
156
|
+
*
|
|
157
|
+
* This format matches HTML5 <input type="datetime-local"> and is useful
|
|
158
|
+
* for setting other inputs or displaying local time without timezone conversion.
|
|
159
|
+
*/
|
|
160
|
+
function componentsToLocalValue(components, withTime) {
|
|
161
|
+
if (!components.year || !components.month || !components.day) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
const year = components.year.toString().padStart(4, '0');
|
|
165
|
+
const month = components.month.toString().padStart(2, '0');
|
|
166
|
+
const day = components.day.toString().padStart(2, '0');
|
|
167
|
+
if (withTime) {
|
|
168
|
+
const hour = (components.hour || 0).toString().padStart(2, '0');
|
|
169
|
+
const minute = (components.minute || 0).toString().padStart(2, '0');
|
|
170
|
+
return `${year}-${month}-${day}T${hour}:${minute}`;
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
return `${year}-${month}-${day}`;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Format components for display in input using Intl API.
|
|
178
|
+
*
|
|
179
|
+
* PORTED FROM: format-display-value in date_picker.cljs
|
|
180
|
+
*/
|
|
181
|
+
function formatDisplayValue(components, formatType, locale, withTime) {
|
|
182
|
+
if (!components.year || !components.month || !components.day) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
const dateObj = componentsToDateObject(components);
|
|
186
|
+
if (!dateObj)
|
|
187
|
+
return null;
|
|
188
|
+
const options = {
|
|
189
|
+
dateStyle: formatType || 'long',
|
|
190
|
+
};
|
|
191
|
+
// Add time styling if with-time is enabled
|
|
192
|
+
if (withTime) {
|
|
193
|
+
options.timeStyle = 'short';
|
|
194
|
+
}
|
|
195
|
+
const formatter = new Intl.DateTimeFormat(locale, options);
|
|
196
|
+
return formatter.format(dateObj);
|
|
197
|
+
}
|
|
198
|
+
// ============================================================================
|
|
199
|
+
// Helper Functions - Time Input
|
|
200
|
+
// ============================================================================
|
|
201
|
+
/**
|
|
202
|
+
* Parse hour and minute from raw digits (4 chars: "HHmm")
|
|
203
|
+
*
|
|
204
|
+
* PORTED FROM: parse-time-components in date_picker.cljs
|
|
205
|
+
*/
|
|
206
|
+
function parseTimeComponents(rawDigits) {
|
|
207
|
+
if (!rawDigits || rawDigits.length !== 4)
|
|
208
|
+
return null;
|
|
209
|
+
const hourStr = rawDigits.substring(0, 2);
|
|
210
|
+
const minuteStr = rawDigits.substring(2, 4);
|
|
211
|
+
const hour = parseInt(hourStr, 10);
|
|
212
|
+
const minute = parseInt(minuteStr, 10);
|
|
213
|
+
if (hour < 0 || hour > 23 || minute < 0 || minute > 59) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
return { hour, minute };
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Validate if digit is valid for given position
|
|
220
|
+
* Position 0: first hour digit (0-2)
|
|
221
|
+
* Position 1: second hour digit (0-9, but 0-3 if first is 2)
|
|
222
|
+
* Position 3: first minute digit (0-5)
|
|
223
|
+
* Position 4: second minute digit (0-9)
|
|
224
|
+
*
|
|
225
|
+
* PORTED FROM: validate-time-digit in date_picker.cljs
|
|
226
|
+
*/
|
|
227
|
+
function validateTimeDigit(digit, position, currentDigits) {
|
|
228
|
+
switch (position) {
|
|
229
|
+
case 0:
|
|
230
|
+
return digit <= 2; // First hour digit: 0-2
|
|
231
|
+
case 1: {
|
|
232
|
+
const firstHour = parseInt(currentDigits[0], 10);
|
|
233
|
+
return firstHour === 2 ? digit <= 3 : true; // If hour starts with 2, max 23
|
|
234
|
+
}
|
|
235
|
+
case 3:
|
|
236
|
+
return digit <= 5; // First minute digit: 0-5
|
|
237
|
+
case 4:
|
|
238
|
+
return true; // Second minute digit: 0-9
|
|
239
|
+
default:
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Format hour and minute into "HH:mm" display
|
|
245
|
+
*
|
|
246
|
+
* PORTED FROM: format-time-display in date_picker.cljs
|
|
247
|
+
*/
|
|
248
|
+
function formatTimeDisplay(hour, minute) {
|
|
249
|
+
const hourStr = hour.toString().padStart(2, '0');
|
|
250
|
+
const minuteStr = minute.toString().padStart(2, '0');
|
|
251
|
+
return `${hourStr}:${minuteStr}`;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Find next editable position, skipping delimiter at position 2
|
|
255
|
+
*
|
|
256
|
+
* PORTED FROM: find-next-editable-position in date_picker.cljs
|
|
257
|
+
*/
|
|
258
|
+
function findNextEditablePosition(currentPos) {
|
|
259
|
+
switch (currentPos) {
|
|
260
|
+
case 0: return 1; // 0 -> 1 (within hour)
|
|
261
|
+
case 1: return 3; // 1 -> 3 (skip delimiter, go to minute)
|
|
262
|
+
case 3: return 4; // 3 -> 4 (within minute)
|
|
263
|
+
case 4: return 5; // 4 -> 5 (after last digit)
|
|
264
|
+
case 5: return 5; // 5 -> 5 (stay at end)
|
|
265
|
+
default: return currentPos;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Find previous editable position, skipping delimiter at position 2
|
|
270
|
+
*
|
|
271
|
+
* PORTED FROM: find-prev-editable-position in date_picker.cljs
|
|
272
|
+
*/
|
|
273
|
+
function findPrevEditablePosition(currentPos) {
|
|
274
|
+
switch (currentPos) {
|
|
275
|
+
case 5: return 4; // 5 -> 4 (from after last digit)
|
|
276
|
+
case 4: return 3; // 4 -> 3 (within minute)
|
|
277
|
+
case 3: return 1; // 3 -> 1 (skip delimiter, go to hour)
|
|
278
|
+
case 1: return 0; // 1 -> 0 (within hour)
|
|
279
|
+
case 0: return 0; // 0 -> 0 (stay at start)
|
|
280
|
+
default: return currentPos;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Convert internal position (0,1,3,4) to raw digits index (0,1,2,3)
|
|
285
|
+
*
|
|
286
|
+
* PORTED FROM: position->raw-digit-index in date_picker.cljs
|
|
287
|
+
*/
|
|
288
|
+
function positionToRawDigitIndex(internalPos) {
|
|
289
|
+
switch (internalPos) {
|
|
290
|
+
case 0: return 0; // Position 0 → raw digit 0 (first hour)
|
|
291
|
+
case 1: return 1; // Position 1 → raw digit 1 (second hour)
|
|
292
|
+
case 3: return 2; // Position 3 → raw digit 2 (first minute)
|
|
293
|
+
case 4: return 3; // Position 4 → raw digit 3 (second minute)
|
|
294
|
+
default: return 0;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// ============================================================================
|
|
298
|
+
// TimeInput Helper Class
|
|
299
|
+
// ============================================================================
|
|
300
|
+
/**
|
|
301
|
+
* TimeInput manages the state and behavior of the time input element.
|
|
302
|
+
* Handles smart cursor navigation, digit replacement, and validation.
|
|
303
|
+
*
|
|
304
|
+
* PORTED FROM: Time input state management functions in date_picker.cljs
|
|
305
|
+
*/
|
|
306
|
+
class TimeInput {
|
|
307
|
+
constructor(element, datePickerElement, hour = 0, minute = 0) {
|
|
308
|
+
this.element = element;
|
|
309
|
+
this.datePickerElement = datePickerElement;
|
|
310
|
+
// Initialize state
|
|
311
|
+
const display = formatTimeDisplay(hour, minute);
|
|
312
|
+
const rawDigits = hour.toString().padStart(2, '0') + minute.toString().padStart(2, '0');
|
|
313
|
+
this.state = {
|
|
314
|
+
hour,
|
|
315
|
+
minute,
|
|
316
|
+
caretPosition: 0,
|
|
317
|
+
displayValue: display,
|
|
318
|
+
rawDigits,
|
|
319
|
+
};
|
|
320
|
+
// Set initial value
|
|
321
|
+
this.element.value = display;
|
|
322
|
+
// Bind event handlers
|
|
323
|
+
this.setupEventListeners();
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Setup all event listeners for the time input
|
|
327
|
+
*/
|
|
328
|
+
setupEventListeners() {
|
|
329
|
+
this.element.addEventListener('keydown', (e) => this.handleKeyDown(e));
|
|
330
|
+
this.element.addEventListener('input', (e) => this.handleInput(e));
|
|
331
|
+
this.element.addEventListener('click', (e) => this.handleClick(e));
|
|
332
|
+
this.element.addEventListener('focus', (e) => this.handleFocus(e));
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Handle keydown events
|
|
336
|
+
*/
|
|
337
|
+
handleKeyDown(event) {
|
|
338
|
+
const key = event.key;
|
|
339
|
+
switch (key) {
|
|
340
|
+
case 'ArrowRight':
|
|
341
|
+
this.handleArrowRight(event);
|
|
342
|
+
break;
|
|
343
|
+
case 'ArrowLeft':
|
|
344
|
+
this.handleArrowLeft(event);
|
|
345
|
+
break;
|
|
346
|
+
case 'Backspace':
|
|
347
|
+
this.handleBackspace(event);
|
|
348
|
+
break;
|
|
349
|
+
case 'Delete':
|
|
350
|
+
this.handleDelete(event);
|
|
351
|
+
break;
|
|
352
|
+
case 'Home':
|
|
353
|
+
event.preventDefault();
|
|
354
|
+
this.updateState({ caretPosition: 0 });
|
|
355
|
+
break;
|
|
356
|
+
case 'End':
|
|
357
|
+
event.preventDefault();
|
|
358
|
+
this.updateState({ caretPosition: 5 });
|
|
359
|
+
break;
|
|
360
|
+
case 'Tab':
|
|
361
|
+
// Allow default tab behavior
|
|
362
|
+
break;
|
|
363
|
+
default:
|
|
364
|
+
// Handle digit input
|
|
365
|
+
if (/^\d$/.test(key)) {
|
|
366
|
+
this.handleDigitInput(event, parseInt(key, 10));
|
|
367
|
+
}
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Handle input events (prevent default browser input)
|
|
373
|
+
*/
|
|
374
|
+
handleInput(event) {
|
|
375
|
+
event.preventDefault();
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Handle click events - position cursor at first digit
|
|
379
|
+
*/
|
|
380
|
+
handleClick(event) {
|
|
381
|
+
this.updateState({ caretPosition: 0 });
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Handle focus events - position cursor at first digit
|
|
385
|
+
*/
|
|
386
|
+
handleFocus(event) {
|
|
387
|
+
this.updateState({ caretPosition: 0 });
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Handle arrow right - move to next editable position
|
|
391
|
+
*
|
|
392
|
+
* PORTED FROM: handle-time-arrow-right! in date_picker.cljs
|
|
393
|
+
*/
|
|
394
|
+
handleArrowRight(event) {
|
|
395
|
+
event.preventDefault();
|
|
396
|
+
const nextPos = findNextEditablePosition(this.state.caretPosition);
|
|
397
|
+
this.updateState({ caretPosition: nextPos });
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Handle arrow left - move to previous editable position
|
|
401
|
+
*
|
|
402
|
+
* PORTED FROM: handle-time-arrow-left! in date_picker.cljs
|
|
403
|
+
*/
|
|
404
|
+
handleArrowLeft(event) {
|
|
405
|
+
event.preventDefault();
|
|
406
|
+
const prevPos = findPrevEditablePosition(this.state.caretPosition);
|
|
407
|
+
this.updateState({ caretPosition: prevPos });
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Handle digit input - replace digit at cursor position
|
|
411
|
+
*
|
|
412
|
+
* PORTED FROM: handle-time-digit-input! in date_picker.cljs
|
|
413
|
+
*/
|
|
414
|
+
handleDigitInput(event, digit) {
|
|
415
|
+
event.preventDefault();
|
|
416
|
+
const currentPos = this.state.caretPosition;
|
|
417
|
+
// Only process if at editable position and digit is valid
|
|
418
|
+
if (![0, 1, 3, 4].includes(currentPos))
|
|
419
|
+
return;
|
|
420
|
+
if (!validateTimeDigit(digit, currentPos, this.state.rawDigits))
|
|
421
|
+
return;
|
|
422
|
+
// Replace digit at current position
|
|
423
|
+
const newState = this.replaceDigitAtPosition(currentPos, digit);
|
|
424
|
+
if (!newState)
|
|
425
|
+
return;
|
|
426
|
+
// Move to next position
|
|
427
|
+
const nextPos = findNextEditablePosition(currentPos);
|
|
428
|
+
this.updateState({ ...newState, caretPosition: nextPos });
|
|
429
|
+
// Notify date picker of time change
|
|
430
|
+
this.notifyTimeChange();
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Handle backspace - zero digit and move back
|
|
434
|
+
*
|
|
435
|
+
* PORTED FROM: handle-time-backspace! in date_picker.cljs
|
|
436
|
+
*/
|
|
437
|
+
handleBackspace(event) {
|
|
438
|
+
event.preventDefault();
|
|
439
|
+
const currentPos = this.state.caretPosition;
|
|
440
|
+
// Can't go back from position 0
|
|
441
|
+
if (currentPos === 0)
|
|
442
|
+
return;
|
|
443
|
+
// Find target position to zero
|
|
444
|
+
const targetPos = currentPos === 1 ? 0 :
|
|
445
|
+
currentPos === 3 ? 1 :
|
|
446
|
+
currentPos === 4 ? 3 :
|
|
447
|
+
currentPos === 5 ? 4 : 0;
|
|
448
|
+
const newState = this.zeroDigitAtPosition(targetPos);
|
|
449
|
+
if (!newState)
|
|
450
|
+
return;
|
|
451
|
+
this.updateState({ ...newState, caretPosition: targetPos });
|
|
452
|
+
this.notifyTimeChange();
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Handle delete - zero digit at current position
|
|
456
|
+
*
|
|
457
|
+
* PORTED FROM: handle-time-delete! in date_picker.cljs
|
|
458
|
+
*/
|
|
459
|
+
handleDelete(event) {
|
|
460
|
+
event.preventDefault();
|
|
461
|
+
const currentPos = this.state.caretPosition;
|
|
462
|
+
// Only at editable positions
|
|
463
|
+
if (![0, 1, 3, 4].includes(currentPos))
|
|
464
|
+
return;
|
|
465
|
+
const newState = this.zeroDigitAtPosition(currentPos);
|
|
466
|
+
if (!newState)
|
|
467
|
+
return;
|
|
468
|
+
this.updateState({ ...newState, caretPosition: currentPos });
|
|
469
|
+
this.notifyTimeChange();
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Replace digit at specific position
|
|
473
|
+
*
|
|
474
|
+
* PORTED FROM: replace-digit-at-position in date_picker.cljs
|
|
475
|
+
*/
|
|
476
|
+
replaceDigitAtPosition(position, newDigit) {
|
|
477
|
+
const rawIndex = positionToRawDigitIndex(position);
|
|
478
|
+
const digitsArray = this.state.rawDigits.split('');
|
|
479
|
+
digitsArray[rawIndex] = newDigit.toString();
|
|
480
|
+
const newDigits = digitsArray.join('');
|
|
481
|
+
const parsed = parseTimeComponents(newDigits);
|
|
482
|
+
if (!parsed)
|
|
483
|
+
return null;
|
|
484
|
+
return {
|
|
485
|
+
rawDigits: newDigits,
|
|
486
|
+
hour: parsed.hour,
|
|
487
|
+
minute: parsed.minute,
|
|
488
|
+
displayValue: formatTimeDisplay(parsed.hour, parsed.minute),
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Zero digit at specific position
|
|
493
|
+
*
|
|
494
|
+
* PORTED FROM: zero-digit-at-position in date_picker.cljs
|
|
495
|
+
*/
|
|
496
|
+
zeroDigitAtPosition(position) {
|
|
497
|
+
return this.replaceDigitAtPosition(position, 0);
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Update internal state and refresh display
|
|
501
|
+
*
|
|
502
|
+
* PORTED FROM: update-time-input-state! in date_picker.cljs
|
|
503
|
+
*/
|
|
504
|
+
updateState(updates) {
|
|
505
|
+
this.state = { ...this.state, ...updates };
|
|
506
|
+
// Update display value
|
|
507
|
+
this.element.value = this.state.displayValue;
|
|
508
|
+
// Set cursor position, mapping internal positions to DOM positions
|
|
509
|
+
const caretPos = this.state.caretPosition;
|
|
510
|
+
const actualPos = caretPos === 0 ? 0 :
|
|
511
|
+
caretPos === 1 ? 1 :
|
|
512
|
+
caretPos === 2 ? 3 :
|
|
513
|
+
caretPos === 3 ? 3 :
|
|
514
|
+
caretPos === 4 ? 4 :
|
|
515
|
+
caretPos === 5 ? 5 : caretPos;
|
|
516
|
+
this.element.setSelectionRange(actualPos, actualPos);
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Notify date picker of time change
|
|
520
|
+
*/
|
|
521
|
+
notifyTimeChange() {
|
|
522
|
+
this.datePickerElement.handleTimeInputChange(this.state.hour, this.state.minute);
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Get current time values
|
|
526
|
+
*/
|
|
527
|
+
getTime() {
|
|
528
|
+
return {
|
|
529
|
+
hour: this.state.hour,
|
|
530
|
+
minute: this.state.minute,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Update time from external source
|
|
535
|
+
*/
|
|
536
|
+
setTime(hour, minute) {
|
|
537
|
+
const display = formatTimeDisplay(hour, minute);
|
|
538
|
+
const rawDigits = hour.toString().padStart(2, '0') + minute.toString().padStart(2, '0');
|
|
539
|
+
this.state = {
|
|
540
|
+
...this.state,
|
|
541
|
+
hour,
|
|
542
|
+
minute,
|
|
543
|
+
displayValue: display,
|
|
544
|
+
rawDigits,
|
|
545
|
+
};
|
|
546
|
+
this.element.value = display;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
// ============================================================================
|
|
550
|
+
// TyDatePicker Custom Element
|
|
551
|
+
// ============================================================================
|
|
552
|
+
export class TyDatePicker extends TyComponent {
|
|
553
|
+
// observedAttributes is auto-generated by TyComponent from properties config
|
|
554
|
+
constructor() {
|
|
555
|
+
super(); // TyComponent handles attachShadow and attachInternals
|
|
556
|
+
// ============================================================================
|
|
557
|
+
// INTERNAL STATE
|
|
558
|
+
// ============================================================================
|
|
559
|
+
// Internal state
|
|
560
|
+
this._state = {
|
|
561
|
+
withTime: false,
|
|
562
|
+
open: false,
|
|
563
|
+
};
|
|
564
|
+
// Reopen guard — prevents click event from reopening after pointerdown close
|
|
565
|
+
this._closeTimestamp = 0;
|
|
566
|
+
// Initialize styles in shadow root
|
|
567
|
+
const shadow = this.shadowRoot;
|
|
568
|
+
ensureStyles(shadow, { css: datePickerStyles, id: 'ty-date-picker' });
|
|
569
|
+
}
|
|
570
|
+
// ==========================================================================
|
|
571
|
+
// Lifecycle Hooks (TyComponent)
|
|
572
|
+
// ==========================================================================
|
|
573
|
+
/**
|
|
574
|
+
* Called when component connects to DOM
|
|
575
|
+
* TyComponent handles property capture automatically
|
|
576
|
+
*/
|
|
577
|
+
onConnect() {
|
|
578
|
+
// Initialize component state
|
|
579
|
+
this.initializeState();
|
|
580
|
+
// Initial render
|
|
581
|
+
this.render();
|
|
582
|
+
// Setup locale observer to watch for ancestor lang changes
|
|
583
|
+
this._localeObserver = observeLocaleChanges(this, () => {
|
|
584
|
+
this.render();
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Called when component disconnects from DOM
|
|
589
|
+
*/
|
|
590
|
+
onDisconnect() {
|
|
591
|
+
// Cleanup locale observer
|
|
592
|
+
if (this._localeObserver) {
|
|
593
|
+
this._localeObserver();
|
|
594
|
+
this._localeObserver = undefined;
|
|
595
|
+
}
|
|
596
|
+
// Cleanup event listeners and state
|
|
597
|
+
this.cleanup();
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Called when properties change
|
|
601
|
+
* Handle state updates BEFORE render
|
|
602
|
+
*/
|
|
603
|
+
onPropertiesChanged(changes) {
|
|
604
|
+
for (const { name, newValue } of changes) {
|
|
605
|
+
switch (name) {
|
|
606
|
+
// Simple visual properties - just trigger re-render (handled by TyComponent)
|
|
607
|
+
case 'size':
|
|
608
|
+
case 'flavor':
|
|
609
|
+
case 'label':
|
|
610
|
+
case 'placeholder':
|
|
611
|
+
case 'required':
|
|
612
|
+
case 'disabled':
|
|
613
|
+
case 'clearable':
|
|
614
|
+
case 'format':
|
|
615
|
+
case 'locale':
|
|
616
|
+
// These properties just affect rendering, no internal state to update
|
|
617
|
+
// TyComponent will call render() automatically for visual properties
|
|
618
|
+
break;
|
|
619
|
+
// Complex properties
|
|
620
|
+
case 'value': {
|
|
621
|
+
// Parse the new value using current withTime setting
|
|
622
|
+
const newComponents = parseValue(newValue, this._state.withTime);
|
|
623
|
+
// If newComponents is null, CLEAR the state completely
|
|
624
|
+
if (newComponents === null) {
|
|
625
|
+
// Check if we actually have a date to clear
|
|
626
|
+
const hasDate = this._state.year !== undefined ||
|
|
627
|
+
this._state.month !== undefined ||
|
|
628
|
+
this._state.day !== undefined;
|
|
629
|
+
if (hasDate) {
|
|
630
|
+
// Clear all date components, keeping only withTime and open flags
|
|
631
|
+
this._state = {
|
|
632
|
+
withTime: this._state.withTime,
|
|
633
|
+
open: this._state.open,
|
|
634
|
+
// year, month, day, hour, minute are now undefined
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
break;
|
|
638
|
+
}
|
|
639
|
+
// Check if components actually changed
|
|
640
|
+
const currentComponents = {
|
|
641
|
+
year: this._state.year,
|
|
642
|
+
month: this._state.month,
|
|
643
|
+
day: this._state.day,
|
|
644
|
+
hour: this._state.hour,
|
|
645
|
+
minute: this._state.minute,
|
|
646
|
+
};
|
|
647
|
+
const changed = newComponents?.year !== currentComponents.year ||
|
|
648
|
+
newComponents?.month !== currentComponents.month ||
|
|
649
|
+
newComponents?.day !== currentComponents.day ||
|
|
650
|
+
newComponents?.hour !== currentComponents.hour ||
|
|
651
|
+
newComponents?.minute !== currentComponents.minute;
|
|
652
|
+
if (changed) {
|
|
653
|
+
// Update state with new date components
|
|
654
|
+
this._state = {
|
|
655
|
+
...this._state,
|
|
656
|
+
...newComponents,
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
break;
|
|
660
|
+
}
|
|
661
|
+
case 'with-time': {
|
|
662
|
+
// Update internal withTime flag
|
|
663
|
+
const newWithTime = newValue;
|
|
664
|
+
const oldWithTime = this._state.withTime;
|
|
665
|
+
if (newWithTime !== oldWithTime) {
|
|
666
|
+
this._state.withTime = newWithTime;
|
|
667
|
+
// If we have an existing date, re-sync form value
|
|
668
|
+
// (format changes based on withTime flag)
|
|
669
|
+
const hasDate = this._state.year !== undefined &&
|
|
670
|
+
this._state.month !== undefined &&
|
|
671
|
+
this._state.day !== undefined;
|
|
672
|
+
if (hasDate) {
|
|
673
|
+
this.syncFormValue();
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
break;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
// attributeChangedCallback removed - TyComponent handles all attribute changes via onPropertiesChanged
|
|
682
|
+
/**
|
|
683
|
+
* Get form value - returns UTC string from current state
|
|
684
|
+
* TyComponent calls this automatically when value property changes
|
|
685
|
+
*/
|
|
686
|
+
getFormValue() {
|
|
687
|
+
return componentsToOutputValue(this.getComponents());
|
|
688
|
+
}
|
|
689
|
+
// ==========================================================================
|
|
690
|
+
// Internal Helpers
|
|
691
|
+
// ==========================================================================
|
|
692
|
+
/**
|
|
693
|
+
* Extract current date components from internal state
|
|
694
|
+
*/
|
|
695
|
+
getComponents() {
|
|
696
|
+
return {
|
|
697
|
+
year: this._state.year,
|
|
698
|
+
month: this._state.month,
|
|
699
|
+
day: this._state.day,
|
|
700
|
+
hour: this._state.hour,
|
|
701
|
+
minute: this._state.minute,
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Whether internal state has a complete date
|
|
706
|
+
*/
|
|
707
|
+
hasDate() {
|
|
708
|
+
return !!(this._state.year && this._state.month && this._state.day);
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Format current components for display using Intl API
|
|
712
|
+
*/
|
|
713
|
+
getFormattedValue(components) {
|
|
714
|
+
const c = components || this.getComponents();
|
|
715
|
+
if (!c.year || !c.month || !c.day)
|
|
716
|
+
return null;
|
|
717
|
+
return formatDisplayValue(c, this.getProperty('format') || 'long', getEffectiveLocale(this, this.getProperty('locale')), this._state.withTime);
|
|
718
|
+
}
|
|
719
|
+
// ==========================================================================
|
|
720
|
+
// State Management
|
|
721
|
+
// ==========================================================================
|
|
722
|
+
/**
|
|
723
|
+
* Initialize component state from attributes
|
|
724
|
+
*
|
|
725
|
+
* PORTED FROM: init-component-state! in date_picker.cljs
|
|
726
|
+
*/
|
|
727
|
+
initializeState() {
|
|
728
|
+
const valueAttr = this.getProperty('value');
|
|
729
|
+
const withTime = this.getProperty('with-time');
|
|
730
|
+
const components = parseValue(valueAttr, withTime);
|
|
731
|
+
// If components is null, initialize with empty state
|
|
732
|
+
if (components === null) {
|
|
733
|
+
this._state = {
|
|
734
|
+
withTime,
|
|
735
|
+
open: false,
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
else {
|
|
739
|
+
this._state = {
|
|
740
|
+
...components,
|
|
741
|
+
withTime,
|
|
742
|
+
open: false,
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
// Convert to UTC and sync to form
|
|
746
|
+
this.syncFormValue();
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Update internal state
|
|
750
|
+
*
|
|
751
|
+
* PORTED FROM: update-component-state! in date_picker.cljs
|
|
752
|
+
*/
|
|
753
|
+
updateState(updates, forceSync = false) {
|
|
754
|
+
this._state = { ...this._state, ...updates };
|
|
755
|
+
// STAGING: Only sync attributes if dialog is closed OR force-sync is true
|
|
756
|
+
// This prevents re-renders during time input editing
|
|
757
|
+
const shouldSync = forceSync || !this._state.open || !this._state.withTime;
|
|
758
|
+
if (shouldSync) {
|
|
759
|
+
this.syncFormValue();
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Sync form value with current state
|
|
764
|
+
* Compares before setting to prevent circular triggers
|
|
765
|
+
*/
|
|
766
|
+
syncFormValue() {
|
|
767
|
+
const utcValue = this.getFormValue();
|
|
768
|
+
const currentValue = this.getProperty('value');
|
|
769
|
+
// Only update property if value actually changed (prevents circular triggers)
|
|
770
|
+
if (utcValue !== currentValue) {
|
|
771
|
+
// Use property setter to maintain TyComponent lifecycle
|
|
772
|
+
// This will automatically handle attribute sync and form value update
|
|
773
|
+
if (utcValue) {
|
|
774
|
+
this.setProperty('value', utcValue);
|
|
775
|
+
}
|
|
776
|
+
else {
|
|
777
|
+
this.setProperty('value', null);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
// Form value sync handled automatically by TyComponent (formValue: true)
|
|
781
|
+
}
|
|
782
|
+
// handleValueChange removed - logic moved to onPropertiesChanged hook
|
|
783
|
+
// ==========================================================================
|
|
784
|
+
// Time Input Handling
|
|
785
|
+
// ==========================================================================
|
|
786
|
+
/**
|
|
787
|
+
* Handle time input changes from TimeInput class
|
|
788
|
+
*
|
|
789
|
+
* PORTED FROM: handle-time-change! in date_picker.cljs
|
|
790
|
+
*/
|
|
791
|
+
handleTimeInputChange(hour, minute) {
|
|
792
|
+
if (!this.hasDate())
|
|
793
|
+
return;
|
|
794
|
+
const components = { ...this.getComponents(), hour, minute };
|
|
795
|
+
this.updateState(components, true);
|
|
796
|
+
this.emitChangeEvent(components, 'time-change');
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Emit change event
|
|
800
|
+
*
|
|
801
|
+
* PORTED FROM: emit-change-event! in date_picker.cljs
|
|
802
|
+
*/
|
|
803
|
+
emitChangeEvent(components, source) {
|
|
804
|
+
const utcValue = components ? componentsToOutputValue(components) : null;
|
|
805
|
+
const localValue = components ? componentsToLocalValue(components, this._state.withTime) : null;
|
|
806
|
+
const milliseconds = components ? componentsToDateObject(components)?.getTime() ?? null : null;
|
|
807
|
+
const formatted = components ? this.getFormattedValue(components) : null;
|
|
808
|
+
const detail = {
|
|
809
|
+
value: utcValue,
|
|
810
|
+
localValue,
|
|
811
|
+
milliseconds,
|
|
812
|
+
formatted,
|
|
813
|
+
source,
|
|
814
|
+
};
|
|
815
|
+
const event = new CustomEvent('change', {
|
|
816
|
+
detail,
|
|
817
|
+
bubbles: true,
|
|
818
|
+
cancelable: true,
|
|
819
|
+
});
|
|
820
|
+
this.dispatchEvent(event);
|
|
821
|
+
}
|
|
822
|
+
// ==========================================================================
|
|
823
|
+
// Public API (Properties) - Simple Accessors
|
|
824
|
+
// ==========================================================================
|
|
825
|
+
// String properties
|
|
826
|
+
get size() { return this.getProperty('size'); }
|
|
827
|
+
set size(v) { this.setProperty('size', v); }
|
|
828
|
+
get flavor() { return this.getProperty('flavor'); }
|
|
829
|
+
set flavor(v) { this.setProperty('flavor', v); }
|
|
830
|
+
get label() { return this.getProperty('label'); }
|
|
831
|
+
set label(v) { this.setProperty('label', v); }
|
|
832
|
+
get placeholder() { return this.getProperty('placeholder'); }
|
|
833
|
+
set placeholder(v) { this.setProperty('placeholder', v); }
|
|
834
|
+
get name() { return this.getProperty('name'); }
|
|
835
|
+
set name(v) { this.setProperty('name', v); }
|
|
836
|
+
get format() { return this.getProperty('format'); }
|
|
837
|
+
set format(v) { this.setProperty('format', v); }
|
|
838
|
+
get locale() { return this.getProperty('locale'); }
|
|
839
|
+
set locale(v) { this.setProperty('locale', v); }
|
|
840
|
+
// Boolean properties
|
|
841
|
+
get required() { return this.getProperty('required'); }
|
|
842
|
+
set required(v) { this.setProperty('required', v); }
|
|
843
|
+
get disabled() { return this.getProperty('disabled'); }
|
|
844
|
+
set disabled(v) { this.setProperty('disabled', v); }
|
|
845
|
+
get clearable() { return this.getProperty('clearable'); }
|
|
846
|
+
set clearable(v) { this.setProperty('clearable', v); }
|
|
847
|
+
// With-time property
|
|
848
|
+
get withTime() { return this.getProperty('with-time'); }
|
|
849
|
+
set withTime(v) { this.setProperty('with-time', v); }
|
|
850
|
+
// Value property - Keep custom getter/setter for now (complex UTC parsing logic)
|
|
851
|
+
/**
|
|
852
|
+
* Get current value (UTC ISO string)
|
|
853
|
+
*/
|
|
854
|
+
get value() {
|
|
855
|
+
return this.getProperty('value') || null;
|
|
856
|
+
}
|
|
857
|
+
/**
|
|
858
|
+
* Set value (UTC ISO string, Date object, or null)
|
|
859
|
+
*
|
|
860
|
+
* When set to null/undefined/empty string, the attribute is removed.
|
|
861
|
+
* When set to a valid date, the attribute is set to ISO UTC string.
|
|
862
|
+
*/
|
|
863
|
+
set value(val) {
|
|
864
|
+
if (val === null || val === undefined || val === '') {
|
|
865
|
+
this.setProperty('value', null); // TyComponent will remove attribute
|
|
866
|
+
}
|
|
867
|
+
else {
|
|
868
|
+
const strValue = val instanceof Date ? val.toISOString() : val;
|
|
869
|
+
this.setProperty('value', strValue);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
// ==========================================================================
|
|
873
|
+
// Rendering & DOM Management
|
|
874
|
+
// ==========================================================================
|
|
875
|
+
/**
|
|
876
|
+
* Build CSS classes for the stub element
|
|
877
|
+
*
|
|
878
|
+
* PORTED FROM: build-stub-classes in date_picker.cljs
|
|
879
|
+
*/
|
|
880
|
+
buildStubClasses() {
|
|
881
|
+
const classes = ['date-picker-stub'];
|
|
882
|
+
const size = this.getProperty('size') || 'md';
|
|
883
|
+
const flavor = this.getProperty('flavor');
|
|
884
|
+
classes.push(size);
|
|
885
|
+
if (flavor && flavor !== 'default')
|
|
886
|
+
classes.push(flavor);
|
|
887
|
+
if (this.getProperty('disabled'))
|
|
888
|
+
classes.push('disabled');
|
|
889
|
+
if (this.getProperty('required'))
|
|
890
|
+
classes.push('required');
|
|
891
|
+
if (this._state.open)
|
|
892
|
+
classes.push('open');
|
|
893
|
+
return classes.join(' ');
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* Render the date picker stub (input display)
|
|
897
|
+
*
|
|
898
|
+
* PORTED FROM: render-date-picker-stub in date_picker.cljs
|
|
899
|
+
*/
|
|
900
|
+
renderStub() {
|
|
901
|
+
const stub = document.createElement('div');
|
|
902
|
+
stub.className = this.buildStubClasses();
|
|
903
|
+
const isDisabled = this.getProperty('disabled');
|
|
904
|
+
if (isDisabled) {
|
|
905
|
+
stub.setAttribute('disabled', 'true');
|
|
906
|
+
}
|
|
907
|
+
// Start slot — leading icon (search, calendar variant, etc.)
|
|
908
|
+
const startSlot = document.createElement('slot');
|
|
909
|
+
startSlot.name = 'start';
|
|
910
|
+
// Display text
|
|
911
|
+
const displayText = document.createElement('span');
|
|
912
|
+
displayText.className = 'stub-text';
|
|
913
|
+
const formattedValue = this.getFormattedValue();
|
|
914
|
+
const placeholder = this.getProperty('placeholder') || 'Select date...';
|
|
915
|
+
displayText.textContent = formattedValue || placeholder;
|
|
916
|
+
if (!formattedValue) {
|
|
917
|
+
displayText.classList.add('placeholder');
|
|
918
|
+
}
|
|
919
|
+
// Icons container
|
|
920
|
+
const iconContainer = document.createElement('div');
|
|
921
|
+
iconContainer.className = 'stub-icons';
|
|
922
|
+
// Clear button
|
|
923
|
+
const clearable = this.getProperty('clearable');
|
|
924
|
+
if (clearable && formattedValue && !isDisabled) {
|
|
925
|
+
const clearButton = document.createElement('button');
|
|
926
|
+
clearButton.className = 'stub-clear';
|
|
927
|
+
clearButton.type = 'button';
|
|
928
|
+
clearButton.innerHTML = CLEAR_ICON_SVG;
|
|
929
|
+
clearButton.addEventListener('click', (e) => this.handleClearClick(e));
|
|
930
|
+
iconContainer.appendChild(clearButton);
|
|
931
|
+
}
|
|
932
|
+
// Calendar icon
|
|
933
|
+
const calendarIcon = document.createElement('span');
|
|
934
|
+
calendarIcon.className = 'stub-arrow';
|
|
935
|
+
calendarIcon.innerHTML = CALENDAR_ICON_SVG;
|
|
936
|
+
iconContainer.appendChild(calendarIcon);
|
|
937
|
+
// Stub click handler
|
|
938
|
+
stub.addEventListener('click', (e) => this.handleStubClick(e));
|
|
939
|
+
stub.appendChild(startSlot);
|
|
940
|
+
stub.appendChild(displayText);
|
|
941
|
+
stub.appendChild(iconContainer);
|
|
942
|
+
return stub;
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Create time input element
|
|
946
|
+
*
|
|
947
|
+
* PORTED FROM: create-time-input in date_picker.cljs
|
|
948
|
+
*/
|
|
949
|
+
createTimeInputElement() {
|
|
950
|
+
const input = document.createElement('input');
|
|
951
|
+
input.type = 'text';
|
|
952
|
+
input.className = 'time-input';
|
|
953
|
+
input.placeholder = 'HH:mm';
|
|
954
|
+
input.autocomplete = 'off';
|
|
955
|
+
input.maxLength = 5;
|
|
956
|
+
return input;
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* Render time input section
|
|
960
|
+
*/
|
|
961
|
+
renderTimeSection() {
|
|
962
|
+
const timeSection = document.createElement('div');
|
|
963
|
+
timeSection.className = 'time-section';
|
|
964
|
+
const timeLabel = document.createElement('label');
|
|
965
|
+
timeLabel.className = 'time-label';
|
|
966
|
+
timeLabel.textContent = 'Time:';
|
|
967
|
+
const timeInputElement = this.createTimeInputElement();
|
|
968
|
+
// Create TimeInput instance
|
|
969
|
+
const hour = this._state.hour || 0;
|
|
970
|
+
const minute = this._state.minute || 0;
|
|
971
|
+
this._timeInput = new TimeInput(timeInputElement, this, hour, minute);
|
|
972
|
+
const timeIcon = document.createElement('span');
|
|
973
|
+
timeIcon.className = 'time-icon';
|
|
974
|
+
timeIcon.innerHTML = SCHEDULE_ICON_SVG;
|
|
975
|
+
timeSection.appendChild(timeLabel);
|
|
976
|
+
timeSection.appendChild(timeInputElement);
|
|
977
|
+
timeSection.appendChild(timeIcon);
|
|
978
|
+
return timeSection;
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* Render calendar dropdown with dialog
|
|
982
|
+
*
|
|
983
|
+
* PORTED FROM: render-calendar-dropdown in date_picker.cljs
|
|
984
|
+
*/
|
|
985
|
+
renderCalendarDropdown() {
|
|
986
|
+
const dialog = document.createElement('dialog');
|
|
987
|
+
dialog.className = 'calendar-dialog';
|
|
988
|
+
const contentWrapper = document.createElement('div');
|
|
989
|
+
contentWrapper.className = 'calendar-content';
|
|
990
|
+
// Create ty-calendar
|
|
991
|
+
const calendar = document.createElement('ty-calendar');
|
|
992
|
+
// Set current date if available
|
|
993
|
+
if (this._state.year && this._state.month && this._state.day) {
|
|
994
|
+
calendar.setAttribute('year', this._state.year.toString());
|
|
995
|
+
calendar.setAttribute('month', this._state.month.toString());
|
|
996
|
+
calendar.setAttribute('day', this._state.day.toString());
|
|
997
|
+
}
|
|
998
|
+
const locale = getEffectiveLocale(this, this.getProperty('locale'));
|
|
999
|
+
if (locale) {
|
|
1000
|
+
calendar.setAttribute('locale', locale);
|
|
1001
|
+
}
|
|
1002
|
+
// Add calendar change handler
|
|
1003
|
+
calendar.addEventListener('change', (e) => this.handleCalendarChange(e));
|
|
1004
|
+
contentWrapper.appendChild(calendar);
|
|
1005
|
+
// Add time section if with-time enabled
|
|
1006
|
+
if (this._state.withTime) {
|
|
1007
|
+
contentWrapper.appendChild(this.renderTimeSection());
|
|
1008
|
+
}
|
|
1009
|
+
dialog.appendChild(contentWrapper);
|
|
1010
|
+
// Dialog close handler
|
|
1011
|
+
dialog.addEventListener('close', () => {
|
|
1012
|
+
this.updateState({ open: false });
|
|
1013
|
+
});
|
|
1014
|
+
return dialog;
|
|
1015
|
+
}
|
|
1016
|
+
/**
|
|
1017
|
+
* Render native date input for mobile touch devices.
|
|
1018
|
+
* Uses <input type="date"> or <input type="datetime-local"> when with-time.
|
|
1019
|
+
* Reuses .date-picker-stub styling and existing event/state infrastructure.
|
|
1020
|
+
*/
|
|
1021
|
+
renderNativeInput() {
|
|
1022
|
+
const wrapper = document.createElement('div');
|
|
1023
|
+
wrapper.className = 'dropdown-wrapper';
|
|
1024
|
+
const stub = document.createElement('div');
|
|
1025
|
+
stub.className = this.buildStubClasses();
|
|
1026
|
+
const isDisabled = this.getProperty('disabled');
|
|
1027
|
+
if (isDisabled) {
|
|
1028
|
+
stub.setAttribute('disabled', 'true');
|
|
1029
|
+
}
|
|
1030
|
+
// Native input (hidden, activated via picker indicator)
|
|
1031
|
+
const input = document.createElement('input');
|
|
1032
|
+
input.className = 'native-date-input';
|
|
1033
|
+
input.type = this._state.withTime ? 'datetime-local' : 'date';
|
|
1034
|
+
if (isDisabled)
|
|
1035
|
+
input.disabled = true;
|
|
1036
|
+
if (this.getProperty('required'))
|
|
1037
|
+
input.required = true;
|
|
1038
|
+
// Pre-fill value
|
|
1039
|
+
const localValue = componentsToLocalValue(this.getComponents(), this._state.withTime);
|
|
1040
|
+
if (localValue) {
|
|
1041
|
+
input.value = localValue;
|
|
1042
|
+
}
|
|
1043
|
+
// Placeholder span (native date inputs ignore placeholder attr)
|
|
1044
|
+
const placeholder = this.getProperty('placeholder') || 'Select date...';
|
|
1045
|
+
const formattedValue = this.getFormattedValue();
|
|
1046
|
+
const placeholderEl = document.createElement('span');
|
|
1047
|
+
placeholderEl.className = 'stub-text' + (formattedValue ? '' : ' placeholder');
|
|
1048
|
+
placeholderEl.textContent = formattedValue || placeholder;
|
|
1049
|
+
// Change listener
|
|
1050
|
+
input.addEventListener('change', () => {
|
|
1051
|
+
if (!input.value) {
|
|
1052
|
+
this.clearValue();
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
const parsed = parseValue(input.value, this._state.withTime);
|
|
1056
|
+
if (parsed) {
|
|
1057
|
+
this.updateState(parsed, true);
|
|
1058
|
+
this.emitChangeEvent(parsed, 'selection');
|
|
1059
|
+
}
|
|
1060
|
+
});
|
|
1061
|
+
// Start slot — leading icon
|
|
1062
|
+
const startSlot = document.createElement('slot');
|
|
1063
|
+
startSlot.name = 'start';
|
|
1064
|
+
stub.appendChild(startSlot);
|
|
1065
|
+
stub.appendChild(input);
|
|
1066
|
+
stub.appendChild(placeholderEl);
|
|
1067
|
+
// Icons container (clear + calendar icon)
|
|
1068
|
+
const iconContainer = document.createElement('div');
|
|
1069
|
+
iconContainer.className = 'stub-icons';
|
|
1070
|
+
const clearable = this.getProperty('clearable');
|
|
1071
|
+
if (clearable && localValue && !isDisabled) {
|
|
1072
|
+
const clearButton = document.createElement('button');
|
|
1073
|
+
clearButton.className = 'stub-clear';
|
|
1074
|
+
clearButton.type = 'button';
|
|
1075
|
+
clearButton.innerHTML = CLEAR_ICON_SVG;
|
|
1076
|
+
clearButton.addEventListener('click', (e) => this.handleClearClick(e));
|
|
1077
|
+
iconContainer.appendChild(clearButton);
|
|
1078
|
+
}
|
|
1079
|
+
const calendarIcon = document.createElement('span');
|
|
1080
|
+
calendarIcon.className = 'stub-arrow';
|
|
1081
|
+
calendarIcon.innerHTML = CALENDAR_ICON_SVG;
|
|
1082
|
+
iconContainer.appendChild(calendarIcon);
|
|
1083
|
+
stub.appendChild(iconContainer);
|
|
1084
|
+
wrapper.appendChild(stub);
|
|
1085
|
+
return wrapper;
|
|
1086
|
+
}
|
|
1087
|
+
/**
|
|
1088
|
+
* Calculate calendar position
|
|
1089
|
+
*
|
|
1090
|
+
* PORTED FROM: calculate-calendar-position! in date_picker.cljs
|
|
1091
|
+
*/
|
|
1092
|
+
calculateCalendarPosition() {
|
|
1093
|
+
if (!this.shadowRoot)
|
|
1094
|
+
return;
|
|
1095
|
+
// Mobile uses native input, no positioning needed
|
|
1096
|
+
if (isMobileTouch())
|
|
1097
|
+
return;
|
|
1098
|
+
const stub = this.shadowRoot.querySelector('.date-picker-stub');
|
|
1099
|
+
const dialog = this.shadowRoot.querySelector('.calendar-dialog');
|
|
1100
|
+
if (!stub || !dialog)
|
|
1101
|
+
return;
|
|
1102
|
+
const stubRect = stub.getBoundingClientRect();
|
|
1103
|
+
const viewportHeight = window.innerHeight;
|
|
1104
|
+
const viewportWidth = window.innerWidth;
|
|
1105
|
+
// Calendar-specific height estimation
|
|
1106
|
+
const estimatedHeight = 400;
|
|
1107
|
+
const padding = 0;
|
|
1108
|
+
// Available space calculations
|
|
1109
|
+
const spaceBelow = viewportHeight - stubRect.bottom;
|
|
1110
|
+
const spaceAbove = stubRect.top;
|
|
1111
|
+
const spaceRight = viewportWidth - stubRect.left;
|
|
1112
|
+
// Smart direction logic
|
|
1113
|
+
const positionBelow = spaceBelow >= estimatedHeight + padding;
|
|
1114
|
+
const fitsHorizontally = spaceRight >= stubRect.width;
|
|
1115
|
+
const wrapPadding = 8;
|
|
1116
|
+
// Calculate position coordinates
|
|
1117
|
+
const x = fitsHorizontally
|
|
1118
|
+
? stubRect.left - wrapPadding
|
|
1119
|
+
: Math.max(padding, viewportWidth - stubRect.width - padding);
|
|
1120
|
+
const y = positionBelow
|
|
1121
|
+
? stubRect.bottom
|
|
1122
|
+
: spaceAbove >= estimatedHeight + padding
|
|
1123
|
+
? viewportHeight - stubRect.top
|
|
1124
|
+
: Math.max(padding, Math.min(stubRect.bottom, viewportHeight - estimatedHeight - padding));
|
|
1125
|
+
// Set CSS variables for positioning
|
|
1126
|
+
this.style.setProperty('--calendar-x', `${x}px`);
|
|
1127
|
+
this.style.setProperty('--calendar-y', `${y}px`);
|
|
1128
|
+
this.style.setProperty('--calendar-offset-x', '0px');
|
|
1129
|
+
this.style.setProperty('--calendar-offset-y', '0px');
|
|
1130
|
+
// Set direction classes
|
|
1131
|
+
if (positionBelow) {
|
|
1132
|
+
dialog.classList.add('position-below');
|
|
1133
|
+
dialog.classList.remove('position-above');
|
|
1134
|
+
}
|
|
1135
|
+
else {
|
|
1136
|
+
dialog.classList.add('position-above');
|
|
1137
|
+
dialog.classList.remove('position-below');
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* Render container structure
|
|
1142
|
+
*
|
|
1143
|
+
* PORTED FROM: render-container-structure in date_picker.cljs
|
|
1144
|
+
*/
|
|
1145
|
+
renderContainer() {
|
|
1146
|
+
const container = document.createElement('div');
|
|
1147
|
+
container.className = 'dropdown-container';
|
|
1148
|
+
const label = this.getProperty('label');
|
|
1149
|
+
if (label) {
|
|
1150
|
+
const labelEl = document.createElement('label');
|
|
1151
|
+
labelEl.className = 'ty-field-label';
|
|
1152
|
+
labelEl.innerHTML = label + (this.getProperty('required') ? '<span class="required-icon">*</span>' : '');
|
|
1153
|
+
container.appendChild(labelEl);
|
|
1154
|
+
}
|
|
1155
|
+
return container;
|
|
1156
|
+
}
|
|
1157
|
+
/**
|
|
1158
|
+
* Main render method
|
|
1159
|
+
*
|
|
1160
|
+
* PORTED FROM: render! in date_picker.cljs
|
|
1161
|
+
*/
|
|
1162
|
+
render() {
|
|
1163
|
+
if (!this.shadowRoot)
|
|
1164
|
+
return;
|
|
1165
|
+
ensureStyles(this.shadowRoot, { css: datePickerStyles, id: 'ty-date-picker' });
|
|
1166
|
+
// Mobile: native input, no dialog/event listeners/scroll lock
|
|
1167
|
+
if (isMobileTouch()) {
|
|
1168
|
+
const existingNativeInput = this.shadowRoot.querySelector('.native-date-input');
|
|
1169
|
+
if (existingNativeInput) {
|
|
1170
|
+
// PARTIAL UPDATE: don't destroy DOM while native picker may be open
|
|
1171
|
+
this.updateDisplay();
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
// FIRST RENDER: build from scratch
|
|
1175
|
+
this.shadowRoot.innerHTML = '';
|
|
1176
|
+
const container = this.renderContainer();
|
|
1177
|
+
container.appendChild(this.renderNativeInput());
|
|
1178
|
+
this.shadowRoot.appendChild(container);
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
// Check if dialog is currently open
|
|
1182
|
+
const existingDialog = this.shadowRoot.querySelector('.calendar-dialog');
|
|
1183
|
+
const isDialogOpen = existingDialog && existingDialog.open;
|
|
1184
|
+
if (isDialogOpen && this._state.open) {
|
|
1185
|
+
// PARTIAL UPDATE: Dialog is open - just update display
|
|
1186
|
+
this.updateDisplay();
|
|
1187
|
+
this.calculateCalendarPosition();
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
// FULL REBUILD: Dialog is closed or doesn't exist
|
|
1191
|
+
this.shadowRoot.innerHTML = '';
|
|
1192
|
+
const container = this.renderContainer();
|
|
1193
|
+
const wrapper = document.createElement('div');
|
|
1194
|
+
wrapper.className = 'dropdown-wrapper';
|
|
1195
|
+
wrapper.appendChild(this.renderStub());
|
|
1196
|
+
wrapper.appendChild(this.renderCalendarDropdown());
|
|
1197
|
+
container.appendChild(wrapper);
|
|
1198
|
+
this.shadowRoot.appendChild(container);
|
|
1199
|
+
// Setup document-level event listeners
|
|
1200
|
+
this.setupEventListeners();
|
|
1201
|
+
}
|
|
1202
|
+
/**
|
|
1203
|
+
* Update display without destroying DOM (for open dialog)
|
|
1204
|
+
*
|
|
1205
|
+
* PORTED FROM: update-display! in date_picker.cljs
|
|
1206
|
+
*/
|
|
1207
|
+
updateDisplay() {
|
|
1208
|
+
if (!this.shadowRoot)
|
|
1209
|
+
return;
|
|
1210
|
+
const formattedValue = this.getFormattedValue();
|
|
1211
|
+
const placeholder = this.getProperty('placeholder') || 'Select date...';
|
|
1212
|
+
const hasValue = this.hasDate();
|
|
1213
|
+
const clearable = this.getProperty('clearable');
|
|
1214
|
+
const isDisabled = this.getProperty('disabled');
|
|
1215
|
+
// Update stub text (shared between mobile and desktop)
|
|
1216
|
+
const stubText = this.shadowRoot.querySelector('.stub-text');
|
|
1217
|
+
if (stubText) {
|
|
1218
|
+
stubText.textContent = formattedValue || placeholder;
|
|
1219
|
+
stubText.classList.toggle('placeholder', !formattedValue);
|
|
1220
|
+
}
|
|
1221
|
+
// Update clear button visibility
|
|
1222
|
+
const clearButton = this.shadowRoot.querySelector('.stub-clear');
|
|
1223
|
+
if (clearButton) {
|
|
1224
|
+
clearButton.style.display = (clearable && hasValue && !isDisabled) ? '' : 'none';
|
|
1225
|
+
}
|
|
1226
|
+
// Mobile: also sync native input value
|
|
1227
|
+
if (isMobileTouch()) {
|
|
1228
|
+
const nativeInput = this.shadowRoot.querySelector('.native-date-input');
|
|
1229
|
+
if (nativeInput) {
|
|
1230
|
+
const localValue = componentsToLocalValue(this.getComponents(), this._state.withTime);
|
|
1231
|
+
nativeInput.value = localValue || '';
|
|
1232
|
+
}
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
// Desktop: update calendar and time input
|
|
1236
|
+
const calendar = this.shadowRoot.querySelector('ty-calendar');
|
|
1237
|
+
if (calendar && hasValue) {
|
|
1238
|
+
calendar.setAttribute('year', this._state.year.toString());
|
|
1239
|
+
calendar.setAttribute('month', this._state.month.toString());
|
|
1240
|
+
calendar.setAttribute('day', this._state.day.toString());
|
|
1241
|
+
}
|
|
1242
|
+
if (this._timeInput && this._state.hour !== undefined && this._state.minute !== undefined) {
|
|
1243
|
+
this._timeInput.setTime(this._state.hour, this._state.minute);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
/**
|
|
1247
|
+
* Setup document-level event listeners
|
|
1248
|
+
*/
|
|
1249
|
+
setupEventListeners() {
|
|
1250
|
+
// Remove old listeners if they exist
|
|
1251
|
+
if (this._clickListener) {
|
|
1252
|
+
document.removeEventListener('click', this._clickListener);
|
|
1253
|
+
}
|
|
1254
|
+
if (this._keydownListener) {
|
|
1255
|
+
document.removeEventListener('keydown', this._keydownListener);
|
|
1256
|
+
}
|
|
1257
|
+
// Create new listeners
|
|
1258
|
+
this._clickListener = (e) => this.handleOutsideClick(e);
|
|
1259
|
+
this._keydownListener = (e) => this.handleEscapeKey(e);
|
|
1260
|
+
// Add document listeners
|
|
1261
|
+
document.addEventListener('click', this._clickListener);
|
|
1262
|
+
document.addEventListener('keydown', this._keydownListener);
|
|
1263
|
+
// Add dialog click listener
|
|
1264
|
+
const dialog = this.shadowRoot?.querySelector('.calendar-dialog');
|
|
1265
|
+
if (dialog) {
|
|
1266
|
+
this._dialogClickListener = (e) => this.handleDialogClick(e);
|
|
1267
|
+
dialog.addEventListener('click', this._dialogClickListener);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
// ==========================================================================
|
|
1271
|
+
// Event Handlers
|
|
1272
|
+
// ==========================================================================
|
|
1273
|
+
handleStubClick(event) {
|
|
1274
|
+
event.preventDefault();
|
|
1275
|
+
const disabled = this.getProperty('disabled');
|
|
1276
|
+
// FIXME: Timestamp guard is a workaround. Calendar day cells use pointerdown,
|
|
1277
|
+
// which closes the dialog before the click event fires — the click then hits the
|
|
1278
|
+
// stub and reopens. Proper fix: switch calendar-month day cells from pointerdown
|
|
1279
|
+
// to click so the event is consumed before the dialog closes.
|
|
1280
|
+
if (!disabled && Date.now() - this._closeTimestamp > 300) {
|
|
1281
|
+
this.openDropdown();
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
/**
|
|
1285
|
+
* Clear the date value, sync state, emit event, and re-render.
|
|
1286
|
+
*/
|
|
1287
|
+
clearValue() {
|
|
1288
|
+
this.updateState({
|
|
1289
|
+
year: undefined,
|
|
1290
|
+
month: undefined,
|
|
1291
|
+
day: undefined,
|
|
1292
|
+
hour: undefined,
|
|
1293
|
+
minute: undefined,
|
|
1294
|
+
}, true);
|
|
1295
|
+
this.value = null;
|
|
1296
|
+
this.emitChangeEvent(null, 'clear');
|
|
1297
|
+
this.render();
|
|
1298
|
+
}
|
|
1299
|
+
handleClearClick(event) {
|
|
1300
|
+
event.preventDefault();
|
|
1301
|
+
event.stopPropagation();
|
|
1302
|
+
this.clearValue();
|
|
1303
|
+
}
|
|
1304
|
+
/**
|
|
1305
|
+
* Handle calendar date selection
|
|
1306
|
+
*
|
|
1307
|
+
* PORTED FROM: handle-calendar-change! in date_picker.cljs
|
|
1308
|
+
*/
|
|
1309
|
+
handleCalendarChange(event) {
|
|
1310
|
+
event.preventDefault();
|
|
1311
|
+
event.stopPropagation();
|
|
1312
|
+
const customEvent = event;
|
|
1313
|
+
const detail = customEvent.detail;
|
|
1314
|
+
const dayContext = detail.dayContext;
|
|
1315
|
+
if (!dayContext)
|
|
1316
|
+
return;
|
|
1317
|
+
// Update state with new date
|
|
1318
|
+
const newComponents = {
|
|
1319
|
+
year: dayContext.year,
|
|
1320
|
+
month: dayContext.month,
|
|
1321
|
+
day: dayContext.dayInMonth,
|
|
1322
|
+
hour: this._state.hour,
|
|
1323
|
+
minute: this._state.minute,
|
|
1324
|
+
};
|
|
1325
|
+
// Force sync when calendar date changes
|
|
1326
|
+
this.updateState(newComponents, true);
|
|
1327
|
+
this.emitChangeEvent(newComponents, 'selection');
|
|
1328
|
+
// Close calendar if time is not required
|
|
1329
|
+
if (!this._state.withTime) {
|
|
1330
|
+
this.closeDropdown();
|
|
1331
|
+
}
|
|
1332
|
+
else {
|
|
1333
|
+
// Auto-focus time input after date selection
|
|
1334
|
+
requestAnimationFrame(() => {
|
|
1335
|
+
if (!this.shadowRoot)
|
|
1336
|
+
return;
|
|
1337
|
+
const timeInput = this.shadowRoot.querySelector('.time-input');
|
|
1338
|
+
if (timeInput) {
|
|
1339
|
+
timeInput.focus();
|
|
1340
|
+
}
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
/**
|
|
1345
|
+
* Handle dialog backdrop clicks
|
|
1346
|
+
*
|
|
1347
|
+
* PORTED FROM: handle-dialog-click! in date_picker.cljs
|
|
1348
|
+
*/
|
|
1349
|
+
handleDialogClick(event) {
|
|
1350
|
+
if (!this.shadowRoot)
|
|
1351
|
+
return;
|
|
1352
|
+
const dialog = this.shadowRoot.querySelector('.calendar-dialog');
|
|
1353
|
+
const content = this.shadowRoot.querySelector('.calendar-content');
|
|
1354
|
+
// Close if clicking on dialog backdrop (not calendar content)
|
|
1355
|
+
if (event.target === dialog && this._state.open && content && !content.contains(event.target)) {
|
|
1356
|
+
event.preventDefault();
|
|
1357
|
+
event.stopPropagation();
|
|
1358
|
+
this.closeDropdown();
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
/**
|
|
1362
|
+
* Handle clicks outside the date picker
|
|
1363
|
+
*
|
|
1364
|
+
* PORTED FROM: handle-outside-click! in date_picker.cljs
|
|
1365
|
+
*/
|
|
1366
|
+
handleOutsideClick(event) {
|
|
1367
|
+
if (!this.shadowRoot)
|
|
1368
|
+
return;
|
|
1369
|
+
const target = event.target;
|
|
1370
|
+
const dialog = this.shadowRoot.querySelector('.calendar-dialog');
|
|
1371
|
+
// Check if click is truly outside everything
|
|
1372
|
+
if (this._state.open && dialog && !this.contains(target) && !dialog.contains(target)) {
|
|
1373
|
+
this.closeDropdown();
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
/**
|
|
1377
|
+
* Handle Escape key press
|
|
1378
|
+
*
|
|
1379
|
+
* PORTED FROM: handle-escape-key! in date_picker.cljs
|
|
1380
|
+
*/
|
|
1381
|
+
handleEscapeKey(event) {
|
|
1382
|
+
const keyboardEvent = event;
|
|
1383
|
+
if (keyboardEvent.key === 'Escape' && this._state.open) {
|
|
1384
|
+
keyboardEvent.preventDefault();
|
|
1385
|
+
this.closeDropdown();
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
/**
|
|
1389
|
+
* Open calendar dropdown
|
|
1390
|
+
*
|
|
1391
|
+
* PORTED FROM: open-dropdown! in date_picker.cljs
|
|
1392
|
+
*/
|
|
1393
|
+
openDropdown() {
|
|
1394
|
+
if (this._state.open)
|
|
1395
|
+
return;
|
|
1396
|
+
if (isMobileTouch())
|
|
1397
|
+
return;
|
|
1398
|
+
this.updateState({ open: true });
|
|
1399
|
+
// Dispatch open event
|
|
1400
|
+
this.dispatchEvent(new CustomEvent('open', {
|
|
1401
|
+
bubbles: true,
|
|
1402
|
+
composed: true
|
|
1403
|
+
}));
|
|
1404
|
+
this.render();
|
|
1405
|
+
requestAnimationFrame(() => {
|
|
1406
|
+
if (!this.shadowRoot)
|
|
1407
|
+
return;
|
|
1408
|
+
const dialog = this.shadowRoot.querySelector('.calendar-dialog');
|
|
1409
|
+
if (dialog) {
|
|
1410
|
+
const pickerId = `date-picker-${this.id || this.toString()}`;
|
|
1411
|
+
lockScroll(pickerId);
|
|
1412
|
+
dialog.showModal();
|
|
1413
|
+
this.calculateCalendarPosition();
|
|
1414
|
+
dialog.classList.add('open');
|
|
1415
|
+
// Remove focus from any focused elements to prevent the blue outline
|
|
1416
|
+
const focusedElement = this.shadowRoot.activeElement;
|
|
1417
|
+
if (focusedElement) {
|
|
1418
|
+
focusedElement.blur();
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
});
|
|
1422
|
+
}
|
|
1423
|
+
/**
|
|
1424
|
+
* Close calendar dropdown
|
|
1425
|
+
*
|
|
1426
|
+
* PORTED FROM: close-dropdown! in date_picker.cljs
|
|
1427
|
+
*/
|
|
1428
|
+
closeDropdown() {
|
|
1429
|
+
if (!this._state.open)
|
|
1430
|
+
return;
|
|
1431
|
+
this._closeTimestamp = Date.now();
|
|
1432
|
+
const pickerId = `date-picker-${this.id || this.toString()}`;
|
|
1433
|
+
// Force sync any staged updates when closing
|
|
1434
|
+
this.updateState({ open: false }, true);
|
|
1435
|
+
unlockScroll(pickerId);
|
|
1436
|
+
if (this.shadowRoot) {
|
|
1437
|
+
const dialog = this.shadowRoot.querySelector('.calendar-dialog');
|
|
1438
|
+
if (dialog) {
|
|
1439
|
+
dialog.classList.remove('position-above', 'position-below', 'open');
|
|
1440
|
+
dialog.close();
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
this.dispatchEvent(new CustomEvent('close', {
|
|
1444
|
+
bubbles: true,
|
|
1445
|
+
composed: true
|
|
1446
|
+
}));
|
|
1447
|
+
this.render();
|
|
1448
|
+
}
|
|
1449
|
+
// ==========================================================================
|
|
1450
|
+
// Cleanup
|
|
1451
|
+
// ==========================================================================
|
|
1452
|
+
/**
|
|
1453
|
+
* Clean up event listeners and state
|
|
1454
|
+
*
|
|
1455
|
+
* PORTED FROM: cleanup! in date_picker.cljs
|
|
1456
|
+
*/
|
|
1457
|
+
cleanup() {
|
|
1458
|
+
// Remove document listeners
|
|
1459
|
+
if (this._clickListener) {
|
|
1460
|
+
document.removeEventListener('click', this._clickListener);
|
|
1461
|
+
}
|
|
1462
|
+
if (this._keydownListener) {
|
|
1463
|
+
document.removeEventListener('keydown', this._keydownListener);
|
|
1464
|
+
}
|
|
1465
|
+
// Remove dialog listener
|
|
1466
|
+
if (this._dialogClickListener && this.shadowRoot) {
|
|
1467
|
+
const dialog = this.shadowRoot.querySelector('.calendar-dialog');
|
|
1468
|
+
if (dialog) {
|
|
1469
|
+
dialog.removeEventListener('click', this._dialogClickListener);
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
// Unlock scroll if open
|
|
1473
|
+
if (this._state.open) {
|
|
1474
|
+
const pickerId = `date-picker-${this.id || this.toString()}`;
|
|
1475
|
+
unlockScroll(pickerId);
|
|
1476
|
+
}
|
|
1477
|
+
// Clear references
|
|
1478
|
+
this._clickListener = undefined;
|
|
1479
|
+
this._keydownListener = undefined;
|
|
1480
|
+
this._dialogClickListener = undefined;
|
|
1481
|
+
this._timeInput = undefined;
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
// ============================================================================
|
|
1485
|
+
// PROPERTY CONFIGURATION - Single source of truth
|
|
1486
|
+
// ============================================================================
|
|
1487
|
+
TyDatePicker.properties = {
|
|
1488
|
+
// String properties
|
|
1489
|
+
size: {
|
|
1490
|
+
type: 'string',
|
|
1491
|
+
visual: true,
|
|
1492
|
+
default: 'md',
|
|
1493
|
+
validate: (v) => ['xs', 'sm', 'md', 'lg', 'xl'].includes(v),
|
|
1494
|
+
coerce: (v) => {
|
|
1495
|
+
if (!['xs', 'sm', 'md', 'lg', 'xl'].includes(v)) {
|
|
1496
|
+
console.warn(`[ty-date-picker] Invalid size '${v}'. Using 'md'.`);
|
|
1497
|
+
return 'md';
|
|
1498
|
+
}
|
|
1499
|
+
return v;
|
|
1500
|
+
}
|
|
1501
|
+
},
|
|
1502
|
+
flavor: {
|
|
1503
|
+
type: 'string',
|
|
1504
|
+
visual: true,
|
|
1505
|
+
default: 'default'
|
|
1506
|
+
},
|
|
1507
|
+
label: {
|
|
1508
|
+
type: 'string',
|
|
1509
|
+
visual: true,
|
|
1510
|
+
default: ''
|
|
1511
|
+
},
|
|
1512
|
+
placeholder: {
|
|
1513
|
+
type: 'string',
|
|
1514
|
+
visual: true,
|
|
1515
|
+
default: 'Select date...'
|
|
1516
|
+
},
|
|
1517
|
+
name: {
|
|
1518
|
+
type: 'string',
|
|
1519
|
+
visual: false, // Non-visual, just for form field name
|
|
1520
|
+
default: ''
|
|
1521
|
+
},
|
|
1522
|
+
// Boolean properties
|
|
1523
|
+
required: {
|
|
1524
|
+
type: 'boolean',
|
|
1525
|
+
visual: true,
|
|
1526
|
+
default: false
|
|
1527
|
+
},
|
|
1528
|
+
disabled: {
|
|
1529
|
+
type: 'boolean',
|
|
1530
|
+
visual: true,
|
|
1531
|
+
default: false
|
|
1532
|
+
},
|
|
1533
|
+
clearable: {
|
|
1534
|
+
type: 'boolean',
|
|
1535
|
+
visual: true,
|
|
1536
|
+
default: true,
|
|
1537
|
+
aliases: {
|
|
1538
|
+
'not-clearable': false
|
|
1539
|
+
}
|
|
1540
|
+
},
|
|
1541
|
+
// Format property (medium complexity)
|
|
1542
|
+
format: {
|
|
1543
|
+
type: 'string',
|
|
1544
|
+
visual: true,
|
|
1545
|
+
default: 'long',
|
|
1546
|
+
validate: (v) => ['short', 'medium', 'long', 'full'].includes(v),
|
|
1547
|
+
coerce: (v) => {
|
|
1548
|
+
const validFormats = ['short', 'medium', 'long', 'full'];
|
|
1549
|
+
if (!validFormats.includes(v)) {
|
|
1550
|
+
console.warn(`[ty-date-picker] Invalid format '${v}'. Using 'long'.`);
|
|
1551
|
+
return 'long';
|
|
1552
|
+
}
|
|
1553
|
+
return v;
|
|
1554
|
+
}
|
|
1555
|
+
},
|
|
1556
|
+
// Locale property (medium complexity - has observer)
|
|
1557
|
+
locale: {
|
|
1558
|
+
type: 'string',
|
|
1559
|
+
visual: true,
|
|
1560
|
+
default: ''
|
|
1561
|
+
},
|
|
1562
|
+
// Value property (high complexity - UTC parsing, date components)
|
|
1563
|
+
// Note: Custom getter/setter will handle the complex logic
|
|
1564
|
+
value: {
|
|
1565
|
+
type: 'string',
|
|
1566
|
+
visual: true,
|
|
1567
|
+
formValue: true, // Syncs to form
|
|
1568
|
+
emitChange: false, // We emit custom 'change' events manually with full detail
|
|
1569
|
+
default: null // null when no date selected
|
|
1570
|
+
},
|
|
1571
|
+
// With-time property (high complexity - affects parsing and rendering)
|
|
1572
|
+
'with-time': {
|
|
1573
|
+
type: 'boolean',
|
|
1574
|
+
visual: true,
|
|
1575
|
+
default: false
|
|
1576
|
+
}
|
|
1577
|
+
};
|
|
1578
|
+
/**
|
|
1579
|
+
* Form-associated custom element
|
|
1580
|
+
*/
|
|
1581
|
+
TyDatePicker.formAssociated = true;
|
|
1582
|
+
// Register custom element
|
|
1583
|
+
if (!customElements.get('ty-date-picker')) {
|
|
1584
|
+
customElements.define('ty-date-picker', TyDatePicker);
|
|
1585
|
+
}
|
|
1586
|
+
//# sourceMappingURL=date-picker.js.map
|