624 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
		
		
			
		
	
	
			624 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
|  | import { | ||
|  |   customElement, | ||
|  |   type TemplateResult, | ||
|  |   property, | ||
|  |   state, | ||
|  | } from '@design.estate/dees-element'; | ||
|  | import { DeesInputBase } from '../dees-input-base.js'; | ||
|  | import { demoFunc } from './demo.js'; | ||
|  | import { datepickerStyles } from './styles.js'; | ||
|  | import { renderDatepicker } from './template.js'; | ||
|  | import type { IDateEvent } from './types.js'; | ||
|  | import '../dees-icon.js'; | ||
|  | import '../dees-label.js'; | ||
|  | 
 | ||
|  | 
 | ||
|  | declare global { | ||
|  |   interface HTMLElementTagNameMap { | ||
|  |     'dees-input-datepicker': DeesInputDatepicker; | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | @customElement('dees-input-datepicker') | ||
|  | export class DeesInputDatepicker extends DeesInputBase<DeesInputDatepicker> { | ||
|  |   public static demo = demoFunc; | ||
|  | 
 | ||
|  |   @property({ type: String }) | ||
|  |   public value: string = ''; | ||
|  | 
 | ||
|  |   @property({ type: Boolean }) | ||
|  |   public enableTime: boolean = false; | ||
|  | 
 | ||
|  |   @property({ type: String }) | ||
|  |   public timeFormat: '24h' | '12h' = '24h'; | ||
|  | 
 | ||
|  |   @property({ type: Number }) | ||
|  |   public minuteIncrement: number = 1; | ||
|  | 
 | ||
|  |   @property({ type: String }) | ||
|  |   public dateFormat: string = 'YYYY-MM-DD'; | ||
|  | 
 | ||
|  |   @property({ type: String }) | ||
|  |   public minDate: string = ''; | ||
|  | 
 | ||
|  |   @property({ type: String }) | ||
|  |   public maxDate: string = ''; | ||
|  | 
 | ||
|  |   @property({ type: Array }) | ||
|  |   public disabledDates: string[] = []; | ||
|  | 
 | ||
|  |   @property({ type: Number }) | ||
|  |   public weekStartsOn: 0 | 1 = 1; // Default to Monday
 | ||
|  | 
 | ||
|  |   @property({ type: String }) | ||
|  |   public placeholder: string = 'YYYY-MM-DD'; | ||
|  | 
 | ||
|  |   @property({ type: Boolean }) | ||
|  |   public enableTimezone: boolean = false; | ||
|  | 
 | ||
|  |   @property({ type: String }) | ||
|  |   public timezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone; | ||
|  | 
 | ||
|  |   @property({ type: Array }) | ||
|  |   public events: IDateEvent[] = []; | ||
|  | 
 | ||
|  |   @state() | ||
|  |   public isOpened: boolean = false; | ||
|  | 
 | ||
|  |   @state() | ||
|  |   public opensToTop: boolean = false; | ||
|  | 
 | ||
|  |   @state() | ||
|  |   public selectedDate: Date | null = null; | ||
|  | 
 | ||
|  |   @state() | ||
|  |   public viewDate: Date = new Date(); | ||
|  | 
 | ||
|  |   @state() | ||
|  |   public selectedHour: number = 0; | ||
|  | 
 | ||
|  |   @state() | ||
|  |   public selectedMinute: number = 0; | ||
|  | 
 | ||
|  |   public static styles = datepickerStyles; | ||
|  | 
 | ||
|  | 
 | ||
|  | 
 | ||
|  |   public getTimezones(): { value: string; label: string }[] { | ||
|  |     // Common timezones with their display names
 | ||
|  |     return [ | ||
|  |       { value: 'UTC', label: 'UTC (Coordinated Universal Time)' }, | ||
|  |       { value: 'America/New_York', label: 'Eastern Time (US & Canada)' }, | ||
|  |       { value: 'America/Chicago', label: 'Central Time (US & Canada)' }, | ||
|  |       { value: 'America/Denver', label: 'Mountain Time (US & Canada)' }, | ||
|  |       { value: 'America/Los_Angeles', label: 'Pacific Time (US & Canada)' }, | ||
|  |       { value: 'America/Phoenix', label: 'Arizona' }, | ||
|  |       { value: 'America/Anchorage', label: 'Alaska' }, | ||
|  |       { value: 'Pacific/Honolulu', label: 'Hawaii' }, | ||
|  |       { value: 'Europe/London', label: 'London' }, | ||
|  |       { value: 'Europe/Paris', label: 'Paris' }, | ||
|  |       { value: 'Europe/Berlin', label: 'Berlin' }, | ||
|  |       { value: 'Europe/Moscow', label: 'Moscow' }, | ||
|  |       { value: 'Asia/Dubai', label: 'Dubai' }, | ||
|  |       { value: 'Asia/Kolkata', label: 'India Standard Time' }, | ||
|  |       { value: 'Asia/Shanghai', label: 'China Standard Time' }, | ||
|  |       { value: 'Asia/Tokyo', label: 'Tokyo' }, | ||
|  |       { value: 'Australia/Sydney', label: 'Sydney' }, | ||
|  |       { value: 'Pacific/Auckland', label: 'Auckland' }, | ||
|  |     ]; | ||
|  |   } | ||
|  | 
 | ||
|  |   public render(): TemplateResult { | ||
|  |     return renderDatepicker(this); | ||
|  |   } | ||
|  | 
 | ||
|  | 
 | ||
|  | 
 | ||
|  |   async connectedCallback() { | ||
|  |     super.connectedCallback(); | ||
|  |     this.handleClickOutside = this.handleClickOutside.bind(this); | ||
|  |   } | ||
|  | 
 | ||
|  |   async disconnectedCallback() { | ||
|  |     await super.disconnectedCallback(); | ||
|  |     document.removeEventListener('click', this.handleClickOutside); | ||
|  |   } | ||
|  | 
 | ||
|  |   async firstUpdated() { | ||
|  |     // Initialize with empty value if not set
 | ||
|  |     if (!this.value) { | ||
|  |       this.value = ''; | ||
|  |     } | ||
|  | 
 | ||
|  |     // Initialize view date and selected time
 | ||
|  |     if (this.value) { | ||
|  |       try { | ||
|  |         const date = new Date(this.value); | ||
|  |         if (!isNaN(date.getTime())) { | ||
|  |           this.selectedDate = date; | ||
|  |           this.viewDate = new Date(date); | ||
|  |           this.selectedHour = date.getHours(); | ||
|  |           this.selectedMinute = date.getMinutes(); | ||
|  |         } | ||
|  |       } catch { | ||
|  |         // Invalid date
 | ||
|  |       } | ||
|  |     } else { | ||
|  |       const now = new Date(); | ||
|  |       this.viewDate = new Date(now); | ||
|  |       this.selectedHour = now.getHours(); | ||
|  |       this.selectedMinute = 0; | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   public formatDate(isoString: string): string { | ||
|  |     if (!isoString) return ''; | ||
|  | 
 | ||
|  |     try { | ||
|  |       const date = new Date(isoString); | ||
|  |       if (isNaN(date.getTime())) return ''; | ||
|  | 
 | ||
|  |       let formatted = this.dateFormat; | ||
|  |        | ||
|  |       // Basic date formatting
 | ||
|  |       const day = date.getDate().toString().padStart(2, '0'); | ||
|  |       const month = (date.getMonth() + 1).toString().padStart(2, '0'); | ||
|  |       const year = date.getFullYear().toString(); | ||
|  |        | ||
|  |       // Replace in correct order to avoid conflicts
 | ||
|  |       formatted = formatted.replace('YYYY', year); | ||
|  |       formatted = formatted.replace('YY', year.slice(-2)); | ||
|  |       formatted = formatted.replace('MM', month); | ||
|  |       formatted = formatted.replace('DD', day); | ||
|  | 
 | ||
|  |       // Time formatting if enabled
 | ||
|  |       if (this.enableTime) { | ||
|  |         const hours24 = date.getHours(); | ||
|  |         const hours12 = hours24 === 0 ? 12 : hours24 > 12 ? hours24 - 12 : hours24; | ||
|  |         const minutes = date.getMinutes().toString().padStart(2, '0'); | ||
|  |         const ampm = hours24 >= 12 ? 'PM' : 'AM'; | ||
|  | 
 | ||
|  |         if (this.timeFormat === '12h') { | ||
|  |           formatted += ` ${hours12}:${minutes} ${ampm}`; | ||
|  |         } else { | ||
|  |           formatted += ` ${hours24.toString().padStart(2, '0')}:${minutes}`; | ||
|  |         } | ||
|  |       } | ||
|  | 
 | ||
|  |       // Timezone formatting if enabled
 | ||
|  |       if (this.enableTimezone) { | ||
|  |         const formatter = new Intl.DateTimeFormat('en-US', { | ||
|  |           timeZoneName: 'short', | ||
|  |           timeZone: this.timezone | ||
|  |         }); | ||
|  |         const parts = formatter.formatToParts(date); | ||
|  |         const tzPart = parts.find(part => part.type === 'timeZoneName'); | ||
|  |         if (tzPart) { | ||
|  |           formatted += ` ${tzPart.value}`; | ||
|  |         } | ||
|  |       } | ||
|  | 
 | ||
|  |       return formatted; | ||
|  |     } catch { | ||
|  |       return ''; | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   private handleClickOutside = (event: MouseEvent) => { | ||
|  |     const path = event.composedPath(); | ||
|  |     if (!path.includes(this)) { | ||
|  |       this.isOpened = false; | ||
|  |       document.removeEventListener('click', this.handleClickOutside); | ||
|  |     } | ||
|  |   }; | ||
|  | 
 | ||
|  |   public async toggleCalendar(): Promise<void> { | ||
|  |     if (this.disabled) return; | ||
|  | 
 | ||
|  |     this.isOpened = !this.isOpened; | ||
|  | 
 | ||
|  |     if (this.isOpened) { | ||
|  |       // Check available space and set position
 | ||
|  |       const inputContainer = this.shadowRoot!.querySelector('.input-container') as HTMLElement; | ||
|  |       const rect = inputContainer.getBoundingClientRect(); | ||
|  |       const spaceBelow = window.innerHeight - rect.bottom; | ||
|  |       const spaceAbove = rect.top; | ||
|  |        | ||
|  |       // Determine if we should open upwards (approximate height of 400px)
 | ||
|  |       this.opensToTop = spaceBelow < 400 && spaceAbove > spaceBelow; | ||
|  | 
 | ||
|  |       // Add click outside listener
 | ||
|  |       setTimeout(() => { | ||
|  |         document.addEventListener('click', this.handleClickOutside); | ||
|  |       }, 0); | ||
|  |     } else { | ||
|  |       document.removeEventListener('click', this.handleClickOutside); | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   public getDaysInMonth(): Date[] { | ||
|  |     const year = this.viewDate.getFullYear(); | ||
|  |     const month = this.viewDate.getMonth(); | ||
|  |     const firstDay = new Date(year, month, 1); | ||
|  |     const lastDay = new Date(year, month + 1, 0); | ||
|  |     const days: Date[] = []; | ||
|  | 
 | ||
|  |     // Adjust for week start
 | ||
|  |     const startOffset = this.weekStartsOn === 1  | ||
|  |       ? (firstDay.getDay() === 0 ? 6 : firstDay.getDay() - 1) | ||
|  |       : firstDay.getDay(); | ||
|  | 
 | ||
|  |     // Add days from previous month
 | ||
|  |     for (let i = startOffset; i > 0; i--) { | ||
|  |       days.push(new Date(year, month, 1 - i)); | ||
|  |     } | ||
|  | 
 | ||
|  |     // Add days of current month
 | ||
|  |     for (let i = 1; i <= lastDay.getDate(); i++) { | ||
|  |       days.push(new Date(year, month, i)); | ||
|  |     } | ||
|  | 
 | ||
|  |     // Add days from next month to complete the grid (6 rows)
 | ||
|  |     const remainingDays = 42 - days.length; | ||
|  |     for (let i = 1; i <= remainingDays; i++) { | ||
|  |       days.push(new Date(year, month + 1, i)); | ||
|  |     } | ||
|  | 
 | ||
|  |     return days; | ||
|  |   } | ||
|  | 
 | ||
|  |   public isToday(date: Date): boolean { | ||
|  |     const today = new Date(); | ||
|  |     return date.getDate() === today.getDate() && | ||
|  |            date.getMonth() === today.getMonth() && | ||
|  |            date.getFullYear() === today.getFullYear(); | ||
|  |   } | ||
|  | 
 | ||
|  |   public isSelected(date: Date): boolean { | ||
|  |     if (!this.selectedDate) return false; | ||
|  |     return date.getDate() === this.selectedDate.getDate() && | ||
|  |            date.getMonth() === this.selectedDate.getMonth() && | ||
|  |            date.getFullYear() === this.selectedDate.getFullYear(); | ||
|  |   } | ||
|  | 
 | ||
|  |   public isDisabled(date: Date): boolean { | ||
|  |     // Check min date
 | ||
|  |     if (this.minDate) { | ||
|  |       const min = new Date(this.minDate); | ||
|  |       if (date < min) return true; | ||
|  |     } | ||
|  | 
 | ||
|  |     // Check max date
 | ||
|  |     if (this.maxDate) { | ||
|  |       const max = new Date(this.maxDate); | ||
|  |       if (date > max) return true; | ||
|  |     } | ||
|  | 
 | ||
|  |     // Check disabled dates
 | ||
|  |     if (this.disabledDates && this.disabledDates.length > 0) { | ||
|  |       return this.disabledDates.some(disabledStr => { | ||
|  |         try { | ||
|  |           const disabled = new Date(disabledStr); | ||
|  |           return date.getDate() === disabled.getDate() && | ||
|  |                  date.getMonth() === disabled.getMonth() && | ||
|  |                  date.getFullYear() === disabled.getFullYear(); | ||
|  |         } catch { | ||
|  |           return false; | ||
|  |         } | ||
|  |       }); | ||
|  |     } | ||
|  | 
 | ||
|  |     return false; | ||
|  |   } | ||
|  | 
 | ||
|  |   public getEventsForDate(date: Date): IDateEvent[] { | ||
|  |     if (!this.events || this.events.length === 0) return []; | ||
|  |      | ||
|  |     const dateStr = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`; | ||
|  |     return this.events.filter(event => event.date === dateStr); | ||
|  |   } | ||
|  | 
 | ||
|  |   public selectDate(date: Date): void { | ||
|  |     this.selectedDate = new Date( | ||
|  |       date.getFullYear(), | ||
|  |       date.getMonth(), | ||
|  |       date.getDate(), | ||
|  |       this.selectedHour, | ||
|  |       this.selectedMinute | ||
|  |     ); | ||
|  |      | ||
|  |     this.value = this.formatValueWithTimezone(this.selectedDate); | ||
|  |     this.changeSubject.next(this); | ||
|  |      | ||
|  |     if (!this.enableTime) { | ||
|  |       this.isOpened = false; | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   public selectToday(): void { | ||
|  |     const today = new Date(); | ||
|  |     this.selectedDate = today; | ||
|  |     this.viewDate = new Date(today); | ||
|  |     this.selectedHour = today.getHours(); | ||
|  |     this.selectedMinute = today.getMinutes(); | ||
|  |      | ||
|  |     this.value = this.formatValueWithTimezone(this.selectedDate); | ||
|  |     this.changeSubject.next(this); | ||
|  |      | ||
|  |     if (!this.enableTime) { | ||
|  |       this.isOpened = false; | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   public clear(): void { | ||
|  |     this.value = ''; | ||
|  |     this.selectedDate = null; | ||
|  |     this.changeSubject.next(this); | ||
|  |     this.isOpened = false; | ||
|  |   } | ||
|  | 
 | ||
|  |   public previousMonth(): void { | ||
|  |     this.viewDate = new Date(this.viewDate.getFullYear(), this.viewDate.getMonth() - 1, 1); | ||
|  |   } | ||
|  | 
 | ||
|  |   public nextMonth(): void { | ||
|  |     this.viewDate = new Date(this.viewDate.getFullYear(), this.viewDate.getMonth() + 1, 1); | ||
|  |   } | ||
|  | 
 | ||
|  |   public handleHourInput(e: InputEvent): void { | ||
|  |     const input = e.target as HTMLInputElement; | ||
|  |     let value = parseInt(input.value) || 0; | ||
|  |      | ||
|  |     if (this.timeFormat === '12h') { | ||
|  |       value = Math.max(1, Math.min(12, value)); | ||
|  |       // Convert to 24h format
 | ||
|  |       if (this.selectedHour >= 12 && value !== 12) { | ||
|  |         this.selectedHour = value + 12; | ||
|  |       } else if (this.selectedHour < 12 && value === 12) { | ||
|  |         this.selectedHour = 0; | ||
|  |       } else { | ||
|  |         this.selectedHour = value; | ||
|  |       } | ||
|  |     } else { | ||
|  |       this.selectedHour = Math.max(0, Math.min(23, value)); | ||
|  |     } | ||
|  |      | ||
|  |     this.updateSelectedDateTime(); | ||
|  |   } | ||
|  | 
 | ||
|  |   public handleMinuteInput(e: InputEvent): void { | ||
|  |     const input = e.target as HTMLInputElement; | ||
|  |     let value = parseInt(input.value) || 0; | ||
|  |     value = Math.max(0, Math.min(59, value)); | ||
|  |      | ||
|  |     if (this.minuteIncrement && this.minuteIncrement > 1) { | ||
|  |       value = Math.round(value / this.minuteIncrement) * this.minuteIncrement; | ||
|  |     } | ||
|  |      | ||
|  |     this.selectedMinute = value; | ||
|  |     this.updateSelectedDateTime(); | ||
|  |   } | ||
|  | 
 | ||
|  |   public setAMPM(period: 'am' | 'pm'): void { | ||
|  |     if (period === 'am' && this.selectedHour >= 12) { | ||
|  |       this.selectedHour -= 12; | ||
|  |     } else if (period === 'pm' && this.selectedHour < 12) { | ||
|  |       this.selectedHour += 12; | ||
|  |     } | ||
|  |     this.updateSelectedDateTime(); | ||
|  |   } | ||
|  | 
 | ||
|  |   private updateSelectedDateTime(): void { | ||
|  |     if (this.selectedDate) { | ||
|  |       this.selectedDate = new Date( | ||
|  |         this.selectedDate.getFullYear(), | ||
|  |         this.selectedDate.getMonth(), | ||
|  |         this.selectedDate.getDate(), | ||
|  |         this.selectedHour, | ||
|  |         this.selectedMinute | ||
|  |       ); | ||
|  |       this.value = this.formatValueWithTimezone(this.selectedDate); | ||
|  |       this.changeSubject.next(this); | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   public handleTimezoneChange(e: Event): void { | ||
|  |     const select = e.target as HTMLSelectElement; | ||
|  |     this.timezone = select.value; | ||
|  |     this.updateSelectedDateTime(); | ||
|  |   } | ||
|  | 
 | ||
|  |   private formatValueWithTimezone(date: Date): string { | ||
|  |     if (!this.enableTimezone) { | ||
|  |       return date.toISOString(); | ||
|  |     } | ||
|  |      | ||
|  |     // Format the date with timezone offset
 | ||
|  |     const formatter = new Intl.DateTimeFormat('en-US', { | ||
|  |       year: 'numeric', | ||
|  |       month: '2-digit', | ||
|  |       day: '2-digit', | ||
|  |       hour: '2-digit', | ||
|  |       minute: '2-digit', | ||
|  |       second: '2-digit', | ||
|  |       hour12: false, | ||
|  |       timeZone: this.timezone, | ||
|  |       timeZoneName: 'short' | ||
|  |     }); | ||
|  |      | ||
|  |     const parts = formatter.formatToParts(date); | ||
|  |     const dateParts: any = {}; | ||
|  |     parts.forEach(part => { | ||
|  |       dateParts[part.type] = part.value; | ||
|  |     }); | ||
|  |      | ||
|  |     // Create ISO-like format with timezone
 | ||
|  |     const isoString = `${dateParts.year}-${dateParts.month}-${dateParts.day}T${dateParts.hour}:${dateParts.minute}:${dateParts.second}`; | ||
|  |      | ||
|  |     // Get timezone offset
 | ||
|  |     const tzOffset = this.getTimezoneOffset(date, this.timezone); | ||
|  |     return `${isoString}${tzOffset}`; | ||
|  |   } | ||
|  | 
 | ||
|  |   private getTimezoneOffset(date: Date, timezone: string): string { | ||
|  |     // Create a date in the target timezone
 | ||
|  |     const tzDate = new Date(date.toLocaleString('en-US', { timeZone: timezone })); | ||
|  |     const utcDate = new Date(date.toLocaleString('en-US', { timeZone: 'UTC' })); | ||
|  |      | ||
|  |     const offsetMinutes = (tzDate.getTime() - utcDate.getTime()) / (1000 * 60); | ||
|  |     const hours = Math.floor(Math.abs(offsetMinutes) / 60); | ||
|  |     const minutes = Math.abs(offsetMinutes) % 60; | ||
|  |     const sign = offsetMinutes >= 0 ? '+' : '-'; | ||
|  |      | ||
|  |     return `${sign}${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`; | ||
|  |   } | ||
|  | 
 | ||
|  |   public handleKeydown(e: KeyboardEvent): void { | ||
|  |     if (e.key === 'Enter' || e.key === ' ') { | ||
|  |       e.preventDefault(); | ||
|  |       this.toggleCalendar(); | ||
|  |     } else if (e.key === 'Escape' && this.isOpened) { | ||
|  |       e.preventDefault(); | ||
|  |       this.isOpened = false; | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   public clearValue(e: Event): void { | ||
|  |     e.stopPropagation(); | ||
|  |     this.value = ''; | ||
|  |     this.selectedDate = null; | ||
|  |     this.changeSubject.next(this); | ||
|  |   } | ||
|  | 
 | ||
|  |   public handleManualInput(e: InputEvent): void { | ||
|  |     const input = e.target as HTMLInputElement; | ||
|  |     const inputValue = input.value.trim(); | ||
|  |      | ||
|  |     if (!inputValue) { | ||
|  |       // Clear the value if input is empty
 | ||
|  |       this.value = ''; | ||
|  |       this.selectedDate = null; | ||
|  |       return; | ||
|  |     } | ||
|  | 
 | ||
|  |     const parsedDate = this.parseManualDate(inputValue); | ||
|  |     if (parsedDate && !isNaN(parsedDate.getTime())) { | ||
|  |       // Update internal state without triggering re-render of input
 | ||
|  |       this.value = parsedDate.toISOString(); | ||
|  |       this.selectedDate = parsedDate; | ||
|  |       this.viewDate = new Date(parsedDate); | ||
|  |       this.selectedHour = parsedDate.getHours(); | ||
|  |       this.selectedMinute = parsedDate.getMinutes(); | ||
|  |       this.changeSubject.next(this); | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   public handleInputBlur(e: FocusEvent): void { | ||
|  |     const input = e.target as HTMLInputElement; | ||
|  |     const inputValue = input.value.trim(); | ||
|  |      | ||
|  |     if (!inputValue) { | ||
|  |       this.value = ''; | ||
|  |       this.selectedDate = null; | ||
|  |       this.changeSubject.next(this); | ||
|  |       return; | ||
|  |     } | ||
|  | 
 | ||
|  |     const parsedDate = this.parseManualDate(inputValue); | ||
|  |     if (parsedDate && !isNaN(parsedDate.getTime())) { | ||
|  |       this.value = parsedDate.toISOString(); | ||
|  |       this.selectedDate = parsedDate; | ||
|  |       this.viewDate = new Date(parsedDate); | ||
|  |       this.selectedHour = parsedDate.getHours(); | ||
|  |       this.selectedMinute = parsedDate.getMinutes(); | ||
|  |       this.changeSubject.next(this); | ||
|  |       // Update the input with formatted date
 | ||
|  |       input.value = this.formatDate(this.value); | ||
|  |     } else { | ||
|  |       // Revert to previous valid value on blur if parsing failed
 | ||
|  |       input.value = this.formatDate(this.value); | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   private parseManualDate(input: string): Date | null { | ||
|  |     if (!input) return null; | ||
|  | 
 | ||
|  |     // Split date and time parts if present
 | ||
|  |     const parts = input.split(' '); | ||
|  |     let datePart = parts[0]; | ||
|  |     let timePart = parts[1] || ''; | ||
|  | 
 | ||
|  |     let parsedDate: Date | null = null; | ||
|  | 
 | ||
|  |     // Try different date formats
 | ||
|  |     // Format 1: YYYY-MM-DD (ISO-like)
 | ||
|  |     const isoMatch = datePart.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/); | ||
|  |     if (isoMatch) { | ||
|  |       const [_, year, month, day] = isoMatch; | ||
|  |       parsedDate = new Date(parseInt(year), parseInt(month) - 1, parseInt(day)); | ||
|  |     } | ||
|  | 
 | ||
|  |     // Format 2: DD.MM.YYYY (European)
 | ||
|  |     if (!parsedDate) { | ||
|  |       const euMatch = datePart.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})$/); | ||
|  |       if (euMatch) { | ||
|  |         const [_, day, month, year] = euMatch; | ||
|  |         parsedDate = new Date(parseInt(year), parseInt(month) - 1, parseInt(day)); | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     // Format 3: MM/DD/YYYY (US)
 | ||
|  |     if (!parsedDate) { | ||
|  |       const usMatch = datePart.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/); | ||
|  |       if (usMatch) { | ||
|  |         const [_, month, day, year] = usMatch; | ||
|  |         parsedDate = new Date(parseInt(year), parseInt(month) - 1, parseInt(day)); | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     // If no date was parsed, return null
 | ||
|  |     if (!parsedDate || isNaN(parsedDate.getTime())) { | ||
|  |       return null; | ||
|  |     } | ||
|  | 
 | ||
|  |     // Parse time if present (HH:MM format)
 | ||
|  |     if (timePart) { | ||
|  |       const timeMatch = timePart.match(/^(\d{1,2}):(\d{2})$/); | ||
|  |       if (timeMatch) { | ||
|  |         const [_, hours, minutes] = timeMatch; | ||
|  |         parsedDate.setHours(parseInt(hours)); | ||
|  |         parsedDate.setMinutes(parseInt(minutes)); | ||
|  |       } | ||
|  |     } else if (!this.enableTime) { | ||
|  |       // If time is not enabled and not provided, use current time
 | ||
|  |       const now = new Date(); | ||
|  |       parsedDate.setHours(now.getHours()); | ||
|  |       parsedDate.setMinutes(now.getMinutes()); | ||
|  |       parsedDate.setSeconds(0); | ||
|  |       parsedDate.setMilliseconds(0); | ||
|  |     } | ||
|  | 
 | ||
|  |     return parsedDate; | ||
|  |   } | ||
|  | 
 | ||
|  |   public getValue(): string { | ||
|  |     return this.value; | ||
|  |   } | ||
|  | 
 | ||
|  |   public setValue(value: string): void { | ||
|  |     this.value = value; | ||
|  |     if (value) { | ||
|  |       try { | ||
|  |         const date = new Date(value); | ||
|  |         if (!isNaN(date.getTime())) { | ||
|  |           this.selectedDate = date; | ||
|  |           this.viewDate = new Date(date); | ||
|  |           this.selectedHour = date.getHours(); | ||
|  |           this.selectedMinute = date.getMinutes(); | ||
|  |         } | ||
|  |       } catch { | ||
|  |         // Invalid date
 | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  | } |