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'; 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 = 'DD/MM/YYYY'; @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 = 'Select date'; @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; } /* 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)')}; } `, ]; 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; 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); return html`
${day.getDate()}
`; })}
${this.enableTime ? html`
Time
: ${this.timeFormat === '12h' ? html`
` : ''}
` : ''}
`; } 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; } } private 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(); formatted = formatted.replace('DD', day); formatted = formatted.replace('MM', month); formatted = formatted.replace('YYYY', year); formatted = formatted.replace('YY', year.slice(-2)); // 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}`; } } 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 selectDate(date: Date): void { this.selectedDate = new Date( date.getFullYear(), date.getMonth(), date.getDate(), this.selectedHour, this.selectedMinute ); this.value = this.selectedDate.toISOString(); 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.selectedDate.toISOString(); 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.selectedDate.toISOString(); this.changeSubject.next(this); } } 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); } 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 } } } }