import { customElement, type TemplateResult, property, state, } from '@design.estate/dees-element'; import { DeesInputBase } from '../dees-input-base/dees-input-base.js'; import { demoFunc } from './demo.js'; import { datepickerStyles } from './styles.js'; import { renderDatepicker } from './template.js'; import { DeesInputDatepickerPopup } from './datepicker-popup.js'; import '../../00group-utility/dees-icon/dees-icon.js'; import '../../00group-layout/dees-label/dees-label.js'; declare global { interface HTMLElementTagNameMap { 'dees-input-datepicker': DeesInputDatepicker; } } @customElement('dees-input-datepicker') export class DeesInputDatepicker extends DeesInputBase { public static demo = demoFunc; public static demoGroups = ['Input']; @property({ type: String }) accessor value: string = ''; @property({ type: Boolean }) accessor enableTime: boolean = false; @property({ type: String }) accessor timeFormat: '24h' | '12h' = '24h'; @property({ type: Number }) accessor minuteIncrement: number = 1; @property({ type: String }) accessor dateFormat: string = 'YYYY-MM-DD'; @property({ type: String }) accessor minDate: string = ''; @property({ type: String }) accessor maxDate: string = ''; @property({ type: Array }) accessor disabledDates: string[] = []; @property({ type: Number }) accessor weekStartsOn: 0 | 1 = 1; @property({ type: String }) accessor placeholder: string = 'YYYY-MM-DD'; @property({ type: Boolean }) accessor enableTimezone: boolean = false; @property({ type: String }) accessor timezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone; @property({ type: Array }) accessor events: import('./types.js').IDateEvent[] = []; @state() accessor isOpened: boolean = false; @state() accessor selectedDate: Date | null = null; @state() accessor viewDate: Date = new Date(); @state() accessor selectedHour: number = 0; @state() accessor selectedMinute: number = 0; private popupInstance: DeesInputDatepickerPopup | null = null; public static styles = datepickerStyles; public render(): TemplateResult { return renderDatepicker(this); } async firstUpdated() { if (!this.value) { this.value = ''; } 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; const day = date.getDate().toString().padStart(2, '0'); const month = (date.getMonth() + 1).toString().padStart(2, '0'); const year = date.getFullYear().toString(); formatted = formatted.replace('YYYY', year); formatted = formatted.replace('YY', year.slice(-2)); formatted = formatted.replace('MM', month); formatted = formatted.replace('DD', day); 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}`; } } 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 ''; } } public async toggleCalendar(): Promise { if (this.disabled) return; if (this.isOpened) { this.closePopup(); return; } this.isOpened = true; const inputContainer = this.shadowRoot!.querySelector('.input-container') as HTMLElement; const rect = inputContainer.getBoundingClientRect(); const spaceBelow = window.innerHeight - rect.bottom; const spaceAbove = rect.top; const opensToTop = spaceBelow < 400 && spaceAbove > spaceBelow; if (!this.popupInstance) { this.popupInstance = new DeesInputDatepickerPopup(); } // Configure popup this.popupInstance.triggerRect = rect; this.popupInstance.ownerComponent = this; this.popupInstance.opensToTop = opensToTop; this.popupInstance.enableTime = this.enableTime; this.popupInstance.timeFormat = this.timeFormat; this.popupInstance.minuteIncrement = this.minuteIncrement; this.popupInstance.weekStartsOn = this.weekStartsOn; this.popupInstance.minDate = this.minDate; this.popupInstance.maxDate = this.maxDate; this.popupInstance.disabledDates = this.disabledDates; this.popupInstance.enableTimezone = this.enableTimezone; this.popupInstance.timezone = this.timezone; this.popupInstance.events = this.events; this.popupInstance.selectedDate = this.selectedDate; this.popupInstance.viewDate = new Date(this.viewDate); this.popupInstance.selectedHour = this.selectedHour; this.popupInstance.selectedMinute = this.selectedMinute; // Listen for popup events this.popupInstance.addEventListener('date-selected', this.handleDateSelected); this.popupInstance.addEventListener('date-cleared', this.handleDateCleared); this.popupInstance.addEventListener('close-request', this.handleCloseRequest); this.popupInstance.addEventListener('reposition-request', this.handleRepositionRequest); await this.popupInstance.show(); } private closePopup(): void { this.isOpened = false; if (this.popupInstance) { this.popupInstance.removeEventListener('date-selected', this.handleDateSelected); this.popupInstance.removeEventListener('date-cleared', this.handleDateCleared); this.popupInstance.removeEventListener('close-request', this.handleCloseRequest); this.popupInstance.removeEventListener('reposition-request', this.handleRepositionRequest); this.popupInstance.hide(); } } private handleDateSelected = (event: Event): void => { const date = (event as CustomEvent).detail as Date; this.selectedDate = date; this.selectedHour = date.getHours(); this.selectedMinute = date.getMinutes(); this.viewDate = new Date(date); this.value = this.formatValueWithTimezone(date); this.changeSubject.next(this); }; private handleDateCleared = (): void => { this.value = ''; this.selectedDate = null; this.changeSubject.next(this); }; private handleCloseRequest = (): void => { this.closePopup(); }; private handleRepositionRequest = (): void => { if (!this.popupInstance || !this.isOpened) return; const inputContainer = this.shadowRoot!.querySelector('.input-container') as HTMLElement; if (!inputContainer) return; const rect = inputContainer.getBoundingClientRect(); if (rect.bottom < 0 || rect.top > window.innerHeight) { this.closePopup(); return; } const spaceBelow = window.innerHeight - rect.bottom; const spaceAbove = rect.top; this.popupInstance.opensToTop = spaceBelow < 400 && spaceAbove > spaceBelow; this.popupInstance.triggerRect = rect; }; 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.closePopup(); } } 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) { this.value = ''; this.selectedDate = null; 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); } } 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); input.value = this.formatDate(this.value); } else { input.value = this.formatDate(this.value); } } private parseManualDate(input: string): Date | null { if (!input) return null; const parts = input.split(' '); let datePart = parts[0]; let timePart = parts[1] || ''; let parsedDate: Date | null = null; 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)); } 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)); } } 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 (!parsedDate || isNaN(parsedDate.getTime())) return null; 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) { const now = new Date(); parsedDate.setHours(now.getHours()); parsedDate.setMinutes(now.getMinutes()); parsedDate.setSeconds(0); parsedDate.setMilliseconds(0); } return parsedDate; } private formatValueWithTimezone(date: Date): string { if (!this.enableTimezone) return date.toISOString(); 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; }); const isoString = `${dateParts.year}-${dateParts.month}-${dateParts.day}T${dateParts.hour}:${dateParts.minute}:${dateParts.second}`; const tzOffset = this.getTimezoneOffset(date, this.timezone); return `${isoString}${tzOffset}`; } private getTimezoneOffset(date: Date, timezone: string): string { 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 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 } } } async disconnectedCallback() { await super.disconnectedCallback(); if (this.popupInstance) { this.popupInstance.removeEventListener('date-selected', this.handleDateSelected); this.popupInstance.removeEventListener('date-cleared', this.handleDateCleared); this.popupInstance.removeEventListener('close-request', this.handleCloseRequest); this.popupInstance.removeEventListener('reposition-request', this.handleRepositionRequest); this.popupInstance.hide(); this.popupInstance = null; } } }