import { Component, Input, Output, EventEmitter, OnChanges, SimpleChanges, ElementRef, ViewEncapsulation, Renderer, forwardRef } from '@angular/core'; import { IMyDate, IMyMonth, IMyWeek, IMyDayLabels, IMyMonthLabels, IMyOptions } from './interfaces/index'; import { LocaleService } from './services/my-date-picker.locale.service'; import { ValidatorService } from './services/my-date-picker.validator.service'; import { FormControl, ControlValueAccessor, NG_VALUE_ACCESSOR, AbstractControl } from '@angular/forms'; const VALUE_ACCESOR = { provide: NG_VALUE_ACCESSOR, // tslint:disable-next-line:no-forward-ref useExisting: forwardRef(() => DatePicker), multi: true }; @Component({ selector: 'ng2-iq-datepicker', styleUrls: ['./my-date-picker.component.css'], templateUrl: './my-date-picker.component.html', providers: [LocaleService, ValidatorService, VALUE_ACCESOR], encapsulation: ViewEncapsulation.None }) export class DatePicker implements OnChanges, ControlValueAccessor { @Input() options: any; @Input() locale: string; @Input() defaultMonth: string; @Input() selDate: string; @Input() cssClass = 'glyphicon glyphicon-calendar'; @Output() dateChanged: EventEmitter = new EventEmitter(); @Output() inputFieldChanged: EventEmitter = new EventEmitter(); @Output() calendarViewChanged: EventEmitter = new EventEmitter(); showSelector: boolean = false; visibleMonth: IMyMonth = { monthTxt: '', monthNbr: 0, year: 0 }; selectedMonth: IMyMonth = { monthTxt: '', monthNbr: 0, year: 0 }; selectedDate: IMyDate = { year: 0, month: 0, day: 0 }; weekDays: Array = []; dates: Array = []; selectionDayTxt: string = ''; invalidDate: boolean = false; dayIdx: number = 0; today: Date = null; weekDayOpts: Array = ['su', 'mo', 'tu', 'we', 'th', 'fr', 'sa']; editMonth: boolean = false; invalidMonth: boolean = false; editYear: boolean = false; invalidYear: boolean = false; PREV_MONTH: number = 1; CURR_MONTH: number = 2; NEXT_MONTH: number = 3; // Default options opts: IMyOptions = { dayLabels: {}, monthLabels: {}, dateFormat: '', todayBtnTxt: '', firstDayOfWeek: '', sunHighlight: true, markCurrentDay: true, disableUntil: { year: 0, month: 0, day: 0 }, disableSince: { year: 0, month: 0, day: 0 }, disableDays: >[], disableWeekends: false, height: '34px', width: '100%', selectionTxtFontSize: '18px', inline: false, alignSelectorRight: false, indicateInvalidDate: true, showDateFormatPlaceholder: false, editableDateField: true, editableMonthAndYear: true, minYear: 1000, maxYear: 9999, componentDisabled: false }; // tslint:disable-next-line:no-empty propagateChange = (_: any) => {}; constructor(public elem: ElementRef, private renderer: Renderer, private localeService: LocaleService, private validatorService: ValidatorService) { this.setLocaleOptions(); this.today = new Date(); } writeValue(value: string): void { if (value) { let date = new Date(value); this.selectDate({ year: date.getFullYear(), month: date.getMonth() + 1, day: date.getDate() }); } else { this.clearValue(); } } registerOnChange(fn: any): void { this.propagateChange = fn; } // tslint:disable-next-line:no-empty registerOnTouched(value: any): void {} setLocaleOptions(): void { let opts: IMyOptions = this.localeService.getLocaleOptions(this.locale); Object.keys(opts).forEach((k) => { (this.opts)[k] = opts[k]; }); } setOptions(): void { if (this.options !== undefined) { Object.keys(this.options).forEach((k) => { (this.opts)[k] = this.options[k]; }); } if (this.opts.minYear < 1000) { this.opts.minYear = 1000; } if (this.opts.maxYear > 9999) { this.opts.minYear = 9999; } } resetMonthYearEdit(): void { this.editMonth = false; this.editYear = false; this.invalidMonth = false; this.invalidYear = false; } editMonthClicked(event: any): void { event.stopPropagation(); if (this.opts.editableMonthAndYear) { this.editMonth = true; } } editYearClicked(event: any): void { event.stopPropagation(); if (this.opts.editableMonthAndYear) { this.editYear = true; } } userDateInput(event: any): void { this.invalidDate = false; if (event.target.value.length === 0) { this.clearValue(); } else { let date: IMyDate = this.validatorService.isDateValid(event.target.value, this.opts.dateFormat, this.opts.minYear, this.opts.maxYear, this.opts.disableUntil, this.opts.disableSince, this.opts.disableWeekends, this.opts.disableDays, this.opts.monthLabels); if (date.day !== 0 && date.month !== 0 && date.year !== 0) { this.selectDate({ day: date.day, month: date.month, year: date.year }); } else { this.invalidDate = true; } } if (this.invalidDate) { this.inputFieldChanged.emit({ value: event.target.value, dateFormat: this.opts.dateFormat, valid: !(event.target.value.length === 0 || this.invalidDate) }); } } userMonthInput(event: any): void { if (event.keyCode === 37 || event.keyCode === 39) { return; } this.invalidMonth = false; let m: number = this.validatorService.isMonthLabelValid(event.target.value, this.opts.monthLabels); if (m !== -1) { this.editMonth = false; this.visibleMonth = { monthTxt: this.monthText(m), monthNbr: m, year: this.visibleMonth.year }; this.generateCalendar(m, this.visibleMonth.year); } else { this.invalidMonth = true; } } userYearInput(event: any): void { if (event.keyCode === 37 || event.keyCode === 39) { return; } this.invalidYear = false; let y: number = this.validatorService.isYearLabelValid(Number(event.target.value), this.opts.minYear, this.opts.maxYear); if (y !== -1) { this.editYear = false; this.visibleMonth = { monthTxt: this.visibleMonth.monthTxt, monthNbr: this.visibleMonth.monthNbr, year: y }; this.generateCalendar(this.visibleMonth.monthNbr, y); } else { this.invalidYear = true; } } parseOptions(): void { this.setOptions(); if (this.locale) { this.setLocaleOptions(); } this.dayIdx = this.weekDayOpts.indexOf(this.opts.firstDayOfWeek); if (this.dayIdx !== -1) { let idx: number = this.dayIdx; for (let i = 0; i < this.weekDayOpts.length; i++) { this.weekDays.push(this.opts.dayLabels[this.weekDayOpts[idx]]); idx = this.weekDayOpts[idx] === 'sa' ? 0 : idx + 1; } } } ngOnChanges(changes: SimpleChanges): void { if (changes.hasOwnProperty('locale')) { this.locale = changes['locale'].currentValue; } if (changes.hasOwnProperty('options')) { this.options = changes['options'].currentValue; } this.weekDays.length = 0; this.parseOptions(); if (changes.hasOwnProperty('defaultMonth')) { this.selectedMonth = this.parseSelectedMonth((changes['defaultMonth'].currentValue).toString()); } if (changes.hasOwnProperty('selDate')) { this.selectionDayTxt = changes['selDate'].currentValue; if (this.selectionDayTxt !== '') { this.selectedDate = this.parseSelectedDate(this.selectionDayTxt); } else { this.clearValue(); } } if (this.opts.inline) { this.setVisibleMonth(); } } clearValue(): void { // Remove selected date button clicked this.selectionDayTxt = ''; this.selectedDate = { year: 0, month: 0, day: 0 }; this.dateChanged.emit({ date: {}, formatted: this.selectionDayTxt, epoc: 0 }); this.inputFieldChanged.emit({ value: '', dateFormat: this.opts.dateFormat, valid: false }); this.invalidDate = false; this.propagateChange(null); } openBtnClicked(): void { // Open selector button clicked this.showSelector = !this.showSelector; if (this.showSelector) { this.setVisibleMonth(); } } setVisibleMonth(): void { // Sets visible month of calendar let y: number = 0, m: number = 0; if (this.selectedDate.year === 0 && this.selectedDate.month === 0 && this.selectedDate.day === 0) { if (this.selectedMonth.year === 0 && this.selectedMonth.monthNbr === 0) { y = this.today.getFullYear(); m = this.today.getMonth() + 1; } else { y = this.selectedMonth.year; m = this.selectedMonth.monthNbr; } } else { y = this.selectedDate.year; m = this.selectedDate.month; } this.visibleMonth = { monthTxt: this.opts.monthLabels[m], monthNbr: m, year: y }; // Create current month this.generateCalendar(m, y); } prevMonth(ev: any): void { ev.preventDefault(); let d: Date = this.getDate(this.visibleMonth.year, this.visibleMonth.monthNbr, 1); d.setMonth(d.getMonth() - 1); let y: number = d.getFullYear(); let m: number = d.getMonth() + 1; this.visibleMonth = { monthTxt: this.monthText(m), monthNbr: m, year: y }; this.generateCalendar(m, y); } nextMonth(ev: any): void { ev.preventDefault(); let d: Date = this.getDate(this.visibleMonth.year, this.visibleMonth.monthNbr, 1); d.setMonth(d.getMonth() + 1); let y: number = d.getFullYear(); let m: number = d.getMonth() + 1; this.visibleMonth = { monthTxt: this.monthText(m), monthNbr: m, year: y }; this.generateCalendar(m, y); } prevYear(ev: any): void { ev.preventDefault(); if (this.visibleMonth.year - 1 < this.opts.minYear) { return; } this.visibleMonth.year--; this.generateCalendar(this.visibleMonth.monthNbr, this.visibleMonth.year); } nextYear(ev: any): void { ev.preventDefault(); if (this.visibleMonth.year + 1 > this.opts.maxYear) { return; } this.visibleMonth.year++; this.generateCalendar(this.visibleMonth.monthNbr, this.visibleMonth.year); } todayClicked(): void { // Today selected let m: number = this.today.getMonth() + 1; let y: number = this.today.getFullYear(); this.selectDate({ day: this.today.getDate(), month: m, year: y }); if (this.opts.inline) { this.visibleMonth = { monthTxt: this.opts.monthLabels[m], monthNbr: m, year: y }; this.generateCalendar(m, y); } } cellClicked(ev: any, cell: any): void { // Cell clicked on the calendar if (cell.cmo === this.PREV_MONTH) { // Previous month of day this.prevMonth(ev); } else if (cell.cmo === this.CURR_MONTH) { // Current month of day this.selectDate(cell.dateObj); } else if (cell.cmo === this.NEXT_MONTH) { // Next month of day this.nextMonth(ev); } this.resetMonthYearEdit(); } selectDate(date: any): void { this.selectedDate = { day: date.day, month: date.month, year: date.year }; this.selectionDayTxt = this.formatDate(this.selectedDate); this.showSelector = false; this.dateChanged.emit({ date: this.selectedDate, formatted: this.selectionDayTxt, epoc: Math.round(this.getTimeInMilliseconds(this.selectedDate) / 1000.0) }); this.inputFieldChanged.emit({ value: this.selectionDayTxt, dateFormat: this.opts.dateFormat, valid: true }); this.invalidDate = false; this.propagateChange(new Date(date.year, date.month - 1, date.day).toJSON()); } preZero(val: string): string { // Prepend zero if smaller than 10 return parseInt(val) < 10 ? '0' + val : val; } formatDate(val: any): string { // Returns formatted date string, if mmm is part of dateFormat returns month as a string let formatted: string = this.opts.dateFormat.replace('yyyy', val.year).replace('dd', this.preZero(val.day)); return this.opts.dateFormat.indexOf('mmm') !== -1 ? formatted.replace('mmm', this.monthText(val.month)) : formatted.replace('mm', this.preZero(val.month)); } monthText(m: number): string { // Returns month as a text return this.opts.monthLabels[m]; } monthStartIdx(y: number, m: number): number { // Month start index let d = new Date(); d.setDate(1); d.setMonth(m - 1); d.setFullYear(y); let idx = d.getDay() + this.sundayIdx(); return idx >= 7 ? idx - 7 : idx; } daysInMonth(m: number, y: number): number { // Return number of days of current month return new Date(y, m, 0).getDate(); } daysInPrevMonth(m: number, y: number): number { // Return number of days of the previous month let d: Date = this.getDate(y, m, 1); d.setMonth(d.getMonth() - 1); return this.daysInMonth(d.getMonth() + 1, d.getFullYear()); } isCurrDay(d: number, m: number, y: number, cmo: any): boolean { // Check is a given date the current date return d === this.today.getDate() && m === this.today.getMonth() + 1 && y === this.today.getFullYear() && cmo === 2; } getTimeInMilliseconds(date: IMyDate): number { return this.getDate(date.year, date.month, date.day).getTime(); } getDayNumber(date: IMyDate): number { // Get day number: su=0, mo=1, tu=2, we=3 ... let d: Date = this.getDate(date.year, date.month, date.day); return d.getDay(); } getWeekday(date: IMyDate): string { // Get weekday: su, mo, tu, we ... return this.weekDayOpts[this.getDayNumber(date)]; } getDate(year: number, month: number, day: number): Date { return new Date(year, month - 1, day, 0, 0, 0, 0); } sundayIdx(): number { // Index of Sunday day return this.dayIdx > 0 ? 7 - this.dayIdx : 0; } generateCalendar(m: number, y: number): void { this.dates.length = 0; let monthStart: number = this.monthStartIdx(y, m); let dInThisM: number = this.daysInMonth(m, y); let dInPrevM: number = this.daysInPrevMonth(m, y); let dayNbr: number = 1; let cmo: number = this.PREV_MONTH; for (let i = 1; i < 7; i++) { let week: IMyWeek[] = []; if (i === 1) { // First week let pm = dInPrevM - monthStart + 1; // Previous month for (let j = pm; j <= dInPrevM; j++) { let date: IMyDate = { year: y, month: m - 1, day: j }; week.push({ dateObj: date, cmo: cmo, currDay: this.isCurrDay(j, m, y, cmo), dayNbr: this.getDayNumber(date), disabled: this.validatorService.isDisabledDay(date, this.opts.disableUntil, this.opts.disableSince, this.opts.disableWeekends, this.opts.disableDays) }); } cmo = this.CURR_MONTH; // Current month let daysLeft: number = 7 - week.length; for (let j = 0; j < daysLeft; j++) { let date: IMyDate = { year: y, month: m, day: dayNbr }; week.push({ dateObj: date, cmo: cmo, currDay: this.isCurrDay(dayNbr, m, y, cmo), dayNbr: this.getDayNumber(date), disabled: this.validatorService.isDisabledDay(date, this.opts.disableUntil, this.opts.disableSince, this.opts.disableWeekends, this.opts.disableDays) }); dayNbr++; } } else { // Rest of the weeks for (let j = 1; j < 8; j++) { if (dayNbr > dInThisM) { // Next month dayNbr = 1; cmo = this.NEXT_MONTH; } let date: IMyDate = { year: y, month: cmo === this.CURR_MONTH ? m : m + 1, day: dayNbr }; week.push({ dateObj: date, cmo: cmo, currDay: this.isCurrDay(dayNbr, m, y, cmo), dayNbr: this.getDayNumber(date), disabled: this.validatorService.isDisabledDay(date, this.opts.disableUntil, this.opts.disableSince, this.opts.disableWeekends, this.opts.disableDays) }); dayNbr++; } } this.dates.push(week); } // Notify parent this.calendarViewChanged.emit({ year: y, month: m, first: { number: 1, weekday: this.getWeekday({ year: y, month: m, day: 1 }) }, last: { number: dInThisM, weekday: this.getWeekday({ year: y, month: m, day: dInThisM }) } }); } parseSelectedDate(ds: string): IMyDate { let date: IMyDate = { day: 0, month: 0, year: 0 }; if (ds !== '') { date.day = this.validatorService.parseDatePartNumber(this.opts.dateFormat, ds, 'dd'); date.month = this.opts.dateFormat.indexOf('mmm') !== -1 ? this.validatorService.parseDatePartMonthName(this.opts.dateFormat, ds, 'mmm', this.opts.monthLabels) : this.validatorService.parseDatePartNumber(this.opts.dateFormat, ds, 'mm'); date.year = this.validatorService.parseDatePartNumber(this.opts.dateFormat, ds, 'yyyy'); } return date; } parseSelectedMonth(ms: string): IMyMonth { return this.validatorService.parseDefaultMonth(ms); } showCalendar() { this.showSelector = true; this.setVisibleMonth(); } hideCalendar() { this.showSelector = false; } onBlur() { this.hideCalendar(); if (this.invalidDate) { this.clearValue(); } } }