import { customElement, type TemplateResult, property, html, css, cssManager, state, } from '@design.estate/dees-element'; import { DeesInputBase } from './dees-input-base.js'; import { demoFunc } from './dees-input-datepicker.demo.js'; import './dees-icon.js'; import './dees-label.js'; export interface IDateEvent { date: string; // ISO date string (YYYY-MM-DD) title?: string; description?: string; type?: 'info' | 'warning' | 'success' | 'error'; count?: number; // Number of events on this day } declare global { interface HTMLElementTagNameMap { 'dees-input-datepicker': DeesInputDatepicker; } } @customElement('dees-input-datepicker') export class DeesInputDatepicker extends DeesInputBase { 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() private isOpened: boolean = false; @state() private opensToTop: boolean = false; @state() private selectedDate: Date | null = null; @state() private viewDate: Date = new Date(); @state() private selectedHour: number = 0; @state() private selectedMinute: number = 0; public static styles = [ ...DeesInputBase.baseStyles, cssManager.defaultStyles, css` :host { display: block; position: relative; } .input-container { position: relative; width: 100%; } .date-input { width: 100%; height: 40px; padding: 0 12px; background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(224 71.4% 4.1%)')}; border: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')}; border-radius: 6px; font-size: 14px; line-height: 1.5; color: ${cssManager.bdTheme('hsl(224 71.4% 4.1%)', 'hsl(210 20% 98%)')}; cursor: pointer; transition: all 0.2s ease; outline: none; font-family: inherit; } .date-input::placeholder { color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')}; } .date-input:hover:not(:disabled) { border-color: ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')}; background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')}; } .date-input:focus, .date-input.open { border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 11.2%)', 'hsl(210 20% 98%)')}; outline: 2px solid transparent; outline-offset: 2px; box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(224 71.4% 4.1%)')}, 0 0 0 4px ${cssManager.bdTheme('hsl(222.2 47.4% 11.2% / 0.1)', 'hsl(210 20% 98% / 0.1)')}; } .date-input:disabled { background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')}; color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')}; cursor: not-allowed; opacity: 0.5; } /* Icon container using flexbox for better positioning */ .icon-container { position: absolute; right: 0; top: 0; bottom: 0; display: flex; align-items: center; gap: 4px; padding: 0 12px; pointer-events: none; } .icon-container > * { pointer-events: auto; } .calendar-icon { color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')}; pointer-events: none; display: flex; align-items: center; justify-content: center; } .clear-button { width: 20px; height: 20px; border: none; background: transparent; cursor: pointer; display: flex; align-items: center; justify-content: center; border-radius: 4px; color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')}; transition: opacity 0.2s ease, background-color 0.2s ease; padding: 0; flex-shrink: 0; } .clear-button:hover { background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')}; color: ${cssManager.bdTheme('hsl(224 71.4% 4.1%)', 'hsl(210 20% 98%)')}; } .clear-button:disabled { display: none; } /* Calendar Popup Styles */ .calendar-popup { will-change: transform, opacity; pointer-events: none; transition: all 0.2s ease; opacity: 0; transform: translateY(-4px); background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(224 71.4% 4.1%)')}; border: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')}; box-shadow: ${cssManager.bdTheme( '0 10px 15px -3px hsl(0 0% 0% / 0.1), 0 4px 6px -4px hsl(0 0% 0% / 0.1)', '0 10px 15px -3px hsl(0 0% 0% / 0.2), 0 4px 6px -4px hsl(0 0% 0% / 0.2)' )}; border-radius: 6px; padding: 12px; position: absolute; user-select: none; margin-top: 4px; z-index: 50; left: 0; min-width: 280px; } .calendar-popup.top { bottom: calc(100% + 4px); top: auto; margin-top: 0; margin-bottom: 4px; transform: translateY(4px); } .calendar-popup.bottom { top: 100%; } .calendar-popup.show { pointer-events: all; transform: translateY(0); opacity: 1; } /* Calendar Header */ .calendar-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; gap: 8px; } .month-year-display { font-weight: 500; font-size: 14px; color: ${cssManager.bdTheme('hsl(224 71.4% 4.1%)', 'hsl(210 20% 98%)')}; flex: 1; text-align: center; } .nav-button { width: 28px; height: 28px; border: none; background: transparent; cursor: pointer; border-radius: 6px; display: flex; align-items: center; justify-content: center; color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')}; transition: all 0.2s ease; } .nav-button:hover { background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')}; color: ${cssManager.bdTheme('hsl(224 71.4% 4.1%)', 'hsl(210 20% 98%)')}; } .nav-button:active { background: ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')}; } /* Weekday headers */ .weekdays { display: grid; grid-template-columns: repeat(7, 1fr); gap: 0; margin-bottom: 4px; } .weekday { text-align: center; font-size: 12px; font-weight: 400; color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')}; padding: 0 0 8px 0; } /* Days grid */ .days-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; } .day { aspect-ratio: 1; display: flex; align-items: center; justify-content: center; cursor: pointer; border-radius: 6px; font-size: 14px; transition: all 0.2s ease; color: ${cssManager.bdTheme('hsl(224 71.4% 4.1%)', 'hsl(210 20% 98%)')}; border: none; width: 36px; height: 36px; background: transparent; } .day:hover:not(.disabled) { background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')}; } .day.other-month { color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')}; opacity: 0.5; } .day.today { background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')}; font-weight: 500; } .day.selected { background: ${cssManager.bdTheme('hsl(222.2 47.4% 11.2%)', 'hsl(210 20% 98%)')}; color: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(222.2 47.4% 11.2%)')}; font-weight: 500; } .day.disabled { color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')}; cursor: not-allowed; opacity: 0.3; } /* Event indicators */ .day.has-event { position: relative; } .event-indicator { position: absolute; bottom: 4px; left: 50%; transform: translateX(-50%); display: flex; gap: 2px; justify-content: center; } .event-dot { width: 4px; height: 4px; border-radius: 50%; background: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')}; } .event-dot.info { background: ${cssManager.bdTheme('hsl(211 70% 52%)', 'hsl(211 70% 62%)')}; } .event-dot.warning { background: ${cssManager.bdTheme('hsl(45 90% 45%)', 'hsl(45 90% 55%)')}; } .event-dot.success { background: ${cssManager.bdTheme('hsl(142 69% 45%)', 'hsl(142 69% 55%)')}; } .event-dot.error { background: ${cssManager.bdTheme('hsl(0 72% 51%)', 'hsl(0 72% 61%)')}; } .event-count { position: absolute; top: 2px; right: 2px; min-width: 16px; height: 16px; padding: 0 4px; background: ${cssManager.bdTheme('hsl(0 72% 51%)', 'hsl(0 72% 61%)')}; color: white; border-radius: 8px; font-size: 10px; font-weight: 600; display: flex; align-items: center; justify-content: center; line-height: 1; } /* Tooltip for event details */ .event-tooltip { position: absolute; bottom: calc(100% + 8px); left: 50%; transform: translateX(-50%); background: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')}; color: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 0%)')}; padding: 8px 12px; border-radius: 6px; font-size: 12px; white-space: nowrap; pointer-events: none; opacity: 0; transition: opacity 0.2s ease; z-index: 10; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); } .event-tooltip::after { content: ''; position: absolute; top: 100%; left: 50%; transform: translateX(-50%); border: 4px solid transparent; border-top-color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')}; } .day.has-event:hover .event-tooltip { opacity: 1; } /* Time selector */ .time-selector { margin-top: 12px; padding-top: 12px; border-top: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')}; } .time-selector-title { font-size: 12px; font-weight: 500; margin-bottom: 8px; color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')}; } .time-inputs { display: flex; gap: 8px; align-items: center; } .time-input { width: 65px; height: 36px; border: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')}; border-radius: 6px; padding: 0 12px; font-size: 14px; text-align: center; background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(224 71.4% 4.1%)')}; color: ${cssManager.bdTheme('hsl(224 71.4% 4.1%)', 'hsl(210 20% 98%)')}; transition: all 0.2s ease; } .time-input:hover { border-color: ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')}; background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')}; } .time-input:focus { outline: none; border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 11.2%)', 'hsl(210 20% 98%)')}; box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(222.2 47.4% 11.2% / 0.1)', 'hsl(210 20% 98% / 0.1)')}; } .time-separator { font-size: 14px; font-weight: 500; color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')}; } .am-pm-selector { display: flex; gap: 4px; margin-left: 8px; } .am-pm-button { padding: 6px 12px; border: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')}; background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(224 71.4% 4.1%)')}; border-radius: 6px; font-size: 12px; font-weight: 500; cursor: pointer; transition: all 0.2s ease; color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')}; } .am-pm-button.selected { background: ${cssManager.bdTheme('hsl(222.2 47.4% 11.2%)', 'hsl(210 20% 98%)')}; color: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(222.2 47.4% 11.2%)')}; border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 11.2%)', 'hsl(210 20% 98%)')}; } .am-pm-button:hover:not(.selected) { background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')}; border-color: ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')}; } /* Action buttons */ .calendar-actions { display: flex; gap: 8px; margin-top: 12px; padding-top: 12px; border-top: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')}; } .action-button { flex: 1; height: 36px; border: none; border-radius: 6px; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s ease; display: flex; align-items: center; justify-content: center; } .today-button { background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(224 71.4% 4.1%)')}; border: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')}; color: ${cssManager.bdTheme('hsl(224 71.4% 4.1%)', 'hsl(210 20% 98%)')}; } .today-button:hover { background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')}; border-color: ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')}; } .today-button:active { background: ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')}; } .clear-button { background: transparent; border: 1px solid transparent; color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')}; } .clear-button:hover { background: ${cssManager.bdTheme('hsl(0 72.2% 50.6% / 0.1)', 'hsl(0 62.8% 30.6% / 0.1)')}; color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 30.6%)')}; } .clear-button:active { background: ${cssManager.bdTheme('hsl(0 72.2% 50.6% / 0.2)', 'hsl(0 62.8% 30.6% / 0.2)')}; } /* Timezone selector */ .timezone-selector { margin-top: 12px; padding-top: 12px; border-top: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')}; } .timezone-selector-title { font-size: 12px; font-weight: 500; margin-bottom: 8px; color: ${cssManager.bdTheme('hsl(220 8.9% 46.1%)', 'hsl(215 20.2% 65.1%)')}; } .timezone-select { width: 100%; height: 36px; border: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')}; border-radius: 6px; padding: 0 12px; font-size: 14px; background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(224 71.4% 4.1%)')}; color: ${cssManager.bdTheme('hsl(224 71.4% 4.1%)', 'hsl(210 20% 98%)')}; cursor: pointer; transition: all 0.2s ease; } .timezone-select:hover { border-color: ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(217.2 32.6% 17.5%)')}; background: ${cssManager.bdTheme('hsl(210 20% 98%)', 'hsl(215 27.9% 16.9%)')}; } .timezone-select:focus { outline: none; border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 11.2%)', 'hsl(210 20% 98%)')}; box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(222.2 47.4% 11.2% / 0.1)', 'hsl(210 20% 98% / 0.1)')}; } `, ]; private 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' }, ]; } render(): TemplateResult { const monthNames = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ]; const weekDays = this.weekStartsOn === 1 ? ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'] : ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']; const days = this.getDaysInMonth(); const isAM = this.selectedHour < 12; const timezones = this.getTimezones(); return html`
${this.value && !this.disabled ? html` ` : ''}
${monthNames[this.viewDate.getMonth()]} ${this.viewDate.getFullYear()}
${weekDays.map(day => html`
${day}
`)}
${days.map(day => { const isToday = this.isToday(day); const isSelected = this.isSelected(day); const isOtherMonth = day.getMonth() !== this.viewDate.getMonth(); const isDisabled = this.isDisabled(day); const dayEvents = this.getEventsForDate(day); const hasEvents = dayEvents.length > 0; const totalEventCount = dayEvents.reduce((sum, event) => sum + (event.count || 1), 0); return html`
${day.getDate()} ${hasEvents ? html` ${totalEventCount > 3 ? html`
${totalEventCount}
` : html`
${dayEvents.slice(0, 3).map(event => html`
`)}
`} ${dayEvents[0].title ? html`
${dayEvents[0].title} ${totalEventCount > 1 ? html` (+${totalEventCount - 1} more)` : ''}
` : ''} ` : ''}
`; })}
${this.enableTime ? html`
Time
: ${this.timeFormat === '12h' ? html`
` : ''}
` : ''} ${this.enableTimezone ? html`
Timezone
` : ''}
`; } 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); } }; private async toggleCalendar(): Promise { 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); } } private 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; } private isToday(date: Date): boolean { const today = new Date(); return date.getDate() === today.getDate() && date.getMonth() === today.getMonth() && date.getFullYear() === today.getFullYear(); } private 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(); } private 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; } private 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); } private 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; } } private 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; } } private clear(): void { this.value = ''; this.selectedDate = null; this.changeSubject.next(this); this.isOpened = false; } private previousMonth(): void { this.viewDate = new Date(this.viewDate.getFullYear(), this.viewDate.getMonth() - 1, 1); } private nextMonth(): void { this.viewDate = new Date(this.viewDate.getFullYear(), this.viewDate.getMonth() + 1, 1); } private 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(); } private 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(); } private 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); } } private 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')}`; } private 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; } } private clearValue(e: Event): void { e.stopPropagation(); this.value = ''; this.selectedDate = null; this.changeSubject.next(this); } private 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); } } private 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 } } } }