import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ChangeDetectorRef, HostListener, ElementRef, inject, OnInit, OnChanges, SimpleChanges, } from '@angular/core'; import { CommonModule } from '@angular/common'; import 'mayvio-ui/datepicker/css'; const MONTH_NAMES = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December', ]; const DAY_NAMES = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']; interface DayObj { date: Date; isCurrentMonth: boolean; } @Component({ selector: 'mv-datepicker', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `
`, }) export class DatePickerComponent implements OnInit, OnChanges { @Input() value?: Date; @Input() disabled = false; @Input() placeholder = 'Select date'; @Input() minDate?: Date; @Input() maxDate?: Date; @Input() className = ''; @Output() dateChange = new EventEmitter(); isOpen = false; viewDate: Date = new Date(); dayNames = DAY_NAMES; days: DayObj[] = []; private el = inject(ElementRef); private cdr = inject(ChangeDetectorRef); ngOnInit() { this.viewDate = this.value || new Date(); this.generateCalendar(); } ngOnChanges(changes: SimpleChanges) { if (changes['value'] && !this.isOpen) { if (this.value) { this.viewDate = new Date(this.value); } this.generateCalendar(); } } get formattedDate(): string { if (!this.value) return this.placeholder; return `${MONTH_NAMES[this.value.getMonth()]} ${this.value.getDate()}, ${this.value.getFullYear()}`; } get currentMonthName(): string { return MONTH_NAMES[this.viewDate.getMonth()]; } get currentYear(): number { return this.viewDate.getFullYear(); } generateCalendar() { const year = this.viewDate.getFullYear(); const month = this.viewDate.getMonth(); const daysInMonth = new Date(year, month + 1, 0).getDate(); const firstDay = new Date(year, month, 1).getDay(); const daysInPrevMonth = new Date(year, month, 0).getDate(); const newDays: DayObj[] = []; for (let i = firstDay - 1; i >= 0; i--) { newDays.push({ date: new Date(year, month - 1, daysInPrevMonth - i), isCurrentMonth: false, }); } for (let i = 1; i <= daysInMonth; i++) { newDays.push({ date: new Date(year, month, i), isCurrentMonth: true, }); } const remainingCells = 42 - newDays.length; for (let i = 1; i <= remainingCells; i++) { newDays.push({ date: new Date(year, month + 1, i), isCurrentMonth: false, }); } this.days = newDays; } toggleMenu() { if (this.disabled) return; this.isOpen = !this.isOpen; if (this.isOpen) { this.viewDate = this.value || new Date(); this.generateCalendar(); } this.cdr.markForCheck(); } prevMonth(e: Event) { e.stopPropagation(); this.viewDate = new Date(this.viewDate.getFullYear(), this.viewDate.getMonth() - 1, 1); this.generateCalendar(); this.cdr.markForCheck(); } nextMonth(e: Event) { e.stopPropagation(); this.viewDate = new Date(this.viewDate.getFullYear(), this.viewDate.getMonth() + 1, 1); this.generateCalendar(); this.cdr.markForCheck(); } selectDate(d: Date) { if (this.disabled || this.isDisabled(d)) return; this.value = d; this.dateChange.emit(this.value); this.isOpen = false; this.cdr.markForCheck(); } isToday(d: Date): boolean { const today = new Date(); return ( d.getDate() === today.getDate() && d.getMonth() === today.getMonth() && d.getFullYear() === today.getFullYear() ); } isSelected(d: Date): boolean { if (!this.value) return false; return ( d.getDate() === this.value.getDate() && d.getMonth() === this.value.getMonth() && d.getFullYear() === this.value.getFullYear() ); } isDisabled(d: Date): boolean { if (this.minDate && d < this.minDate) return true; if (this.maxDate && d > this.maxDate) return true; return false; } @HostListener('document:mousedown', ['$event.target']) onClickOutside(target: EventTarget | null) { if (this.isOpen && !this.el.nativeElement.contains(target as Node)) { this.isOpen = false; this.cdr.markForCheck(); } } }