UNPKG

ardhrizk-ngx-daterangepicker-bootstrap

Version:

Date Range Picker - Forced to Angular 16 and Bootstrap 5

1,158 lines 263 kB
import { Component, EventEmitter, forwardRef, Input, Output, ViewChild, ViewEncapsulation } from '@angular/core'; import { NG_VALUE_ACCESSOR } from "@angular/forms"; import dayjs from "dayjs"; import localeData from "dayjs/plugin/localeData"; import LocalizedFormat from 'dayjs/plugin/localizedFormat'; import isoWeek from 'dayjs/plugin/isoWeek'; import week from 'dayjs/plugin/weekOfYear'; import customParseFormat from 'dayjs/plugin/customParseFormat'; import * as i0 from "@angular/core"; import * as i1 from "./ngx-daterangepicker-locale.service"; import * as i2 from "@angular/common"; import * as i3 from "@angular/forms"; dayjs.extend(localeData); dayjs.extend(LocalizedFormat); dayjs.extend(isoWeek); dayjs.extend(week); dayjs.extend(customParseFormat); export var SideEnum; (function (SideEnum) { SideEnum["left"] = "left"; SideEnum["right"] = "right"; })(SideEnum || (SideEnum = {})); export class NgxDaterangepickerBootstrapComponent { set minDate(value) { if (dayjs.isDayjs(value)) { this._minDate = value; } else { this._minDate = dayjs(value); } } set maxDate(value) { if (dayjs.isDayjs(value)) { this._maxDate = value; } else { this._maxDate = dayjs(value); } } set locale(value) { this._locale = { ...this._localeService.config, ...value }; } set ranges(value) { this._ranges = value; this.renderRanges(); } getMinDate() { return this._minDate; } getMaxDate() { return this._maxDate; } get locale() { return this._locale; } get ranges() { return this._ranges; } constructor(el, _ref, _localeService) { this.el = el; this._ref = _ref; this._localeService = _localeService; this.calendarVariables = { left: {}, right: {} }; this.timepickerVariables = { left: {}, right: {} }; this.applyBtn = { disabled: false }; this.sideEnum = SideEnum; // used in template for compile time support of enum values. this.rangesArray = []; this.isShown = false; this.inline = true; this.showCalInRanges = false; this.tooltiptext = []; // for storing tooltiptext this.leftCalendar = {}; this.rightCalendar = {}; this.nowHoveredDate = null; this.pickingDate = false; this._old = { start: null, end: null }; this._locale = {}; this._ranges = {}; this.startDate = dayjs().startOf('day'); this.endDate = dayjs().endOf('day'); this.dateLimit = null; this.autoApply = false; this.singleDatePicker = false; this.showDropdowns = false; this.showWeekNumbers = false; this.showISOWeekNumbers = false; this.linkedCalendars = false; this.autoUpdateInput = true; this.alwaysShowCalendars = false; this.maxSpan = false; this.lockStartDate = false; this.timePicker = false; this.timePicker24Hour = false; this.timePickerIncrement = 1; this.timePickerSeconds = false; this.showClearButton = false; this.firstMonthDayClass = null; this.lastMonthDayClass = null; this.emptyWeekRowClass = null; this.emptyWeekColumnClass = null; this.firstDayOfNextMonthClass = null; this.lastDayOfPreviousMonthClass = null; this.showCancel = false; this.keepCalendarOpeningWithRange = false; this.showRangeLabelOnInput = false; this.customRangeDirection = false; this.closeOnAutoApply = true; this.choosedDate = new EventEmitter(); this.rangeClicked = new EventEmitter(); this.datesUpdated = new EventEmitter(); this.startDateChanged = new EventEmitter(); this.endDateChanged = new EventEmitter(); this.cancelClicked = new EventEmitter(); this.clearClicked = new EventEmitter(); } ngOnChanges(changes) { if ((changes['startDate'] || changes['endDate']) && this.inline) { this.updateView(); } } ngOnInit() { this._buildLocale(); const daysOfWeek = [...this.locale.daysOfWeek]; this.locale.firstDay = this.locale.firstDay % 7; if (this.locale.firstDay !== 0) { let iterator = this.locale.firstDay; while (iterator > 0) { daysOfWeek.push(daysOfWeek.shift()); iterator--; } } this.locale.daysOfWeek = daysOfWeek; if (this.inline) { this.applyBtn.disabled = true; this._old.start = this.startDate?.clone(); this._old.end = this.endDate?.clone(); } if (this.startDate && this.timePicker) { this.setStartDate(this.startDate); this.renderTimePicker(SideEnum.left); } if (this.endDate && this.timePicker) { this.setEndDate(this.endDate); this.renderTimePicker(SideEnum.right); } this.updateMonthsInView(); this.renderCalendar(SideEnum.left); this.renderCalendar(SideEnum.right); this.renderRanges(); } renderRanges() { this.rangesArray = []; let start, end; if (typeof this.ranges === 'object') { for (const range in this.ranges) { if (this.ranges[range]) { if (typeof this.ranges[range][0] === 'string') { start = dayjs(this.ranges[range][0], this.locale.format); } else { start = dayjs(this.ranges[range][0]); } if (typeof this.ranges[range][1] === 'string') { end = dayjs(this.ranges[range][1], this.locale.format); } else { end = dayjs(this.ranges[range][1]); } // If the start or end date exceed those allowed by the minDate or maxSpan // options, shorten the range to the allowable period. if (this.getMinDate() && start.isBefore(this.getMinDate())) { start = this.getMinDate()?.clone(); } let maxDate = this.getMaxDate(); if (this.maxSpan && maxDate && start.clone().add(this.maxSpan).isAfter(maxDate)) { maxDate = start.clone().add(this.maxSpan); } if (maxDate && end.isAfter(maxDate)) { end = maxDate.clone(); } // If the end of the range is before the minimum or the start of the range is // after the maximum, don't display this range option at all. if ((this.getMinDate() && end.isBefore(this.getMinDate(), this.timePicker ? 'minute' : 'day')) || (maxDate && start.isAfter(maxDate, this.timePicker ? 'minute' : 'day'))) { continue; } // Support unicode chars in the range names. const elem = document.createElement('textarea'); elem.innerHTML = range; const rangeHtml = elem.value; this.ranges[rangeHtml] = [start, end]; } } for (const range in this.ranges) { if (this.ranges[range]) { this.rangesArray.push(range); } } if (this.showCustomRangeLabel) { this.rangesArray.push(this.locale.customRangeLabel); } this.showCalInRanges = (!this.rangesArray.length) || this.alwaysShowCalendars; if (!this.timePicker) { this.startDate = this.startDate?.startOf('day'); this.endDate = this.endDate?.endOf('day'); } } } renderTimePicker(side) { let selected, minDate; const maxDate = this.getMaxDate(); if (side === SideEnum.left) { selected = this.startDate?.clone(); minDate = this.getMinDate(); } else if (side === SideEnum.right && this.endDate) { selected = this.endDate.clone(); minDate = this.startDate; } else if (side === SideEnum.right && !this.endDate) { // don't have an end date, use the start date then put the selected time for the right side as the time selected = this._getDateWithTime(this.startDate, SideEnum.right); if (selected.isBefore(this.startDate)) { selected = this.startDate?.clone(); // set it back to the start date the time was backwards } minDate = this.startDate; } const start = this.timePicker24Hour ? 0 : 1; const end = this.timePicker24Hour ? 23 : 12; this.timepickerVariables[side] = { hours: [], minutes: [], minutesLabel: [], seconds: [], secondsLabel: [], disabledHours: [], disabledMinutes: [], disabledSeconds: [], selectedHour: 0, selectedMinute: 0, selectedSecond: 0 }; // generate hours for (let i = start; i <= end; i++) { let i_in_24 = i; if (!this.timePicker24Hour) { i_in_24 = selected.hour() >= 12 ? (i === 12 ? 12 : i + 12) : (i === 12 ? 0 : i); } const time = selected.clone().hour(i_in_24); let disabled = false; if (minDate && time.minute(59).isBefore(minDate)) { disabled = true; } if (maxDate && time.minute(0).isAfter(maxDate)) { disabled = true; } this.timepickerVariables[side].hours.push(i); if (i_in_24 === selected.hour() && !disabled) { this.timepickerVariables[side].selectedHour = i; } else if (disabled) { this.timepickerVariables[side].disabledHours.push(i); } } // generate minutes for (let i = 0; i < 60; i += this.timePickerIncrement) { const padded = i < 10 ? '0' + i : i; const time = selected.clone().minute(i); let disabled = false; if (minDate && time.second(59).isBefore(minDate)) { disabled = true; } if (maxDate && time.second(0).isAfter(maxDate)) { disabled = true; } this.timepickerVariables[side].minutes.push(i); this.timepickerVariables[side].minutesLabel.push(padded); if (selected.minute() === i && !disabled) { this.timepickerVariables[side].selectedMinute = i; } else if (disabled) { this.timepickerVariables[side].disabledMinutes.push(i); } } // generate seconds if (this.timePickerSeconds) { for (let i = 0; i < 60; i++) { const padded = i < 10 ? '0' + i : i; const time = selected.clone().second(i); let disabled = false; if (minDate && time.isBefore(minDate)) { disabled = true; } if (maxDate && time.isAfter(maxDate)) { disabled = true; } this.timepickerVariables[side].seconds.push(i); this.timepickerVariables[side].secondsLabel.push(padded); if (selected.second() === i && !disabled) { this.timepickerVariables[side].selectedSecond = i; } else if (disabled) { this.timepickerVariables[side].disabledSeconds.push(i); } } } // generate AM/PM if (!this.timePicker24Hour) { if (minDate && selected.clone().hour(12).minute(0).second(0).isBefore(minDate)) { this.timepickerVariables[side].amDisabled = true; } if (maxDate && selected.clone().hour(0).minute(0).second(0).isAfter(maxDate)) { this.timepickerVariables[side].pmDisabled = true; } if (selected.hour() >= 12) { this.timepickerVariables[side].ampmModel = 'PM'; } else { this.timepickerVariables[side].ampmModel = 'AM'; } } this.timepickerVariables[side].selected = selected; } renderCalendar(side) { const mainCalendar = (side === SideEnum.left) ? this.leftCalendar : this.rightCalendar; const month = mainCalendar.month.month(); const year = mainCalendar.month.year(); const hour = mainCalendar.month.hour(); const minute = mainCalendar.month.minute(); const second = mainCalendar.month.second(); const daysInMonth = dayjs(new Date(year, month)).daysInMonth(); const firstDay = dayjs(new Date(year, month, 1)); const lastDay = dayjs(new Date(year, month, daysInMonth)); const lastMonth = dayjs(firstDay).subtract(1, 'month').month(); const lastYear = dayjs(firstDay).subtract(1, 'month').year(); const daysInLastMonth = dayjs(new Date(lastYear, lastMonth)).daysInMonth(); const dayOfWeek = firstDay.day(); // initialize a 6 rows x 7 columns array for the calendar const calendar = []; calendar.firstDay = firstDay; calendar.lastDay = lastDay; for (let i = 0; i < 6; i++) calendar[i] = []; // populate the calendar with date objects let startDay = daysInLastMonth - dayOfWeek + this.locale.firstDay + 1; if (startDay > daysInLastMonth) startDay -= 7; if (dayOfWeek === this.locale.firstDay) startDay = daysInLastMonth - 6; let curDate = dayjs(new Date(lastYear, lastMonth, startDay, 12, minute, second)); for (let i = 0, col = 0, row = 0; i < 42; i++, col++, curDate = dayjs(curDate).add(24, 'hour')) { if (i > 0 && col % 7 === 0) { col = 0; row++; } calendar[row][col] = curDate.clone().hour(hour).minute(minute).second(second); curDate = curDate.hour(12); if (this.getMinDate() && calendar[row][col].format('YYYY-MM-DD') === this.getMinDate()?.format('YYYY-MM-DD') && calendar[row][col].isBefore(this.getMinDate()) && side === 'left') { calendar[row][col] = this.getMinDate()?.clone(); } if (this.getMaxDate() && calendar[row][col].format('YYYY-MM-DD') === this.getMaxDate()?.format('YYYY-MM-DD') && calendar[row][col].isAfter(this.getMaxDate()) && side === 'right') { calendar[row][col] = this.getMaxDate()?.clone(); } } // make the calendar object available to hoverDate/clickDate if (side === SideEnum.left) { this.leftCalendar.calendar = calendar; } else { this.rightCalendar.calendar = calendar; } // // Display the calendar // let minDate = side === 'left' ? this.getMinDate() : this.startDate; let maxDate = this.getMaxDate(); // adjust maxDate to reflect the dateLimit setting in order to // grey out end dates beyond the dateLimit if (this.endDate === null && this.dateLimit) { const maxLimit = this.startDate?.clone().add(this.dateLimit, 'day').endOf('day'); if (!maxDate || maxLimit?.isBefore(maxDate)) maxDate = maxLimit; if (this.customRangeDirection) { minDate = this.getMinDate(); const minLimit = this.startDate?.clone().subtract(this.dateLimit, 'day').endOf('day'); if (!minDate || minLimit?.isAfter(minDate)) { minDate = minLimit; } } } this.calendarVariables[side] = { month: month, year: year, hour: hour, minute: minute, second: second, daysInMonth: daysInMonth, firstDay: firstDay, lastDay: lastDay, lastMonth: lastMonth, lastYear: lastYear, daysInLastMonth: daysInLastMonth, dayOfWeek: dayOfWeek, // other vars calRows: Array.from(Array(6).keys()), calCols: Array.from(Array(7).keys()), classes: {}, minDate: minDate, maxDate: maxDate, calendar: calendar }; if (this.showDropdowns) { const currentMonth = calendar[1][1].month(); const currentYear = calendar[1][1].year(); const realCurrentYear = dayjs().year(); const maxYear = (maxDate && maxDate.year()) || (realCurrentYear + 5); const minYear = (minDate && minDate.year()) || (realCurrentYear - 50); const inMinYear = currentYear === minYear; const inMaxYear = currentYear === maxYear; const years = []; for (let y = minYear; y <= maxYear; y++) years.push(y); this.calendarVariables[side].dropdowns = { currentMonth: currentMonth, currentYear: currentYear, maxYear: maxYear, minYear: minYear, inMinYear: inMinYear, inMaxYear: inMaxYear, monthArrays: Array.from(Array(12).keys()), yearArrays: years }; } this._buildCells(calendar, side); } setStartDate(startDate) { if (typeof startDate === 'string') this.startDate = dayjs(startDate, this.locale.format); if (typeof startDate === 'object') { this.pickingDate = true; this.startDate = dayjs(startDate); } if (!this.timePicker) { this.pickingDate = true; this.startDate = this.startDate?.startOf('day'); } if (this.timePicker && this.timePickerIncrement) { this.startDate = this.startDate?.minute(Math.round(this.startDate.minute() / this.timePickerIncrement) * this.timePickerIncrement); } if (this.getMinDate() && this.startDate?.isBefore(this.getMinDate())) { this.startDate = this.getMinDate()?.clone(); if (this.timePicker && this.timePickerIncrement) { this.startDate = this.startDate?.minute(Math.round(this.startDate.minute() / this.timePickerIncrement) * this.timePickerIncrement); } } if (this.getMaxDate() && this.startDate?.isAfter(this.getMaxDate())) { this.startDate = this.getMaxDate()?.clone(); if (this.timePicker && this.timePickerIncrement) { this.startDate = this.startDate?.minute(Math.floor(this.startDate.minute() / this.timePickerIncrement) * this.timePickerIncrement); } } if (!this.isShown) { this.updateElement(); } this.startDateChanged.emit({ startDate: this.startDate }); this.updateMonthsInView(); } setEndDate(endDate) { if (typeof endDate === 'string') { this.endDate = dayjs(endDate, this.locale.format); } if (typeof endDate === 'object') { this.pickingDate = false; this.endDate = dayjs(endDate); } if (!this.timePicker) { this.pickingDate = false; this.endDate = this.endDate?.add(1, 'd').startOf('day').subtract(1, 'second'); } if (this.timePicker && this.timePickerIncrement) { this.endDate?.minute(Math.round(this.endDate.minute() / this.timePickerIncrement) * this.timePickerIncrement); } if (this.endDate?.isBefore(this.startDate)) { this.endDate = this.startDate?.clone(); } if (this.getMaxDate() && this.endDate?.isAfter(this.getMaxDate())) { this.endDate = this.getMaxDate()?.clone(); } if (this.dateLimit && this.startDate?.clone().add(this.dateLimit, 'day').isBefore(this.endDate)) { this.endDate = this.startDate.clone().add(this.dateLimit, 'day'); } if (!this.isShown) { // this.updateElement(); } this.endDateChanged.emit({ endDate: this.endDate }); this.updateMonthsInView(); } isInvalidDate(date) { return false; } isCustomDate(date) { return false; } isTooltipDate(date) { return null; } updateView() { if (this.timePicker) { this.renderTimePicker(SideEnum.left); this.renderTimePicker(SideEnum.right); } this.updateMonthsInView(); this.updateCalendars(); } updateMonthsInView() { if (this.endDate) { // if both dates are visible already, do nothing if (!this.singleDatePicker && this.leftCalendar.month && this.rightCalendar.month && ((this.startDate && this.leftCalendar && this.startDate.format('YYYY-MM') === this.leftCalendar.month.format('YYYY-MM')) || (this.startDate && this.rightCalendar && this.startDate.format('YYYY-MM') === this.rightCalendar.month.format('YYYY-MM'))) && (this.endDate.format('YYYY-MM') === this.leftCalendar.month.format('YYYY-MM') || this.endDate.format('YYYY-MM') === this.rightCalendar.month.format('YYYY-MM'))) { return; } if (this.startDate) { this.leftCalendar.month = this.startDate.clone().date(2); if (!this.linkedCalendars && (this.endDate.month() !== this.startDate.month() || this.endDate.year() !== this.startDate.year())) { this.rightCalendar.month = this.endDate.clone().date(2); } else { this.rightCalendar.month = this.startDate.clone().date(2).add(1, 'month'); } } } else { if (this.leftCalendar.month.format('YYYY-MM') !== this.startDate?.format('YYYY-MM') && this.rightCalendar.month.format('YYYY-MM') !== this.startDate?.format('YYYY-MM')) { this.leftCalendar.month = this.startDate?.clone().date(2); this.rightCalendar.month = this.startDate?.clone().date(2).add(1, 'month'); } } if (this.getMaxDate() && this.linkedCalendars && !this.singleDatePicker && this.rightCalendar.month > this.getMaxDate()) { this.rightCalendar.month = this.getMaxDate()?.clone().date(2); this.leftCalendar.month = this.getMaxDate()?.clone().date(2).subtract(1, 'month'); } } /** * This is responsible for updating the calendars */ updateCalendars() { this.renderCalendar(SideEnum.left); this.renderCalendar(SideEnum.right); if (this.endDate === null) { return; } this.calculateChosenLabel(); } updateElement() { const format = this.locale.displayFormat ? this.locale.displayFormat : this.locale.format; if (!this.singleDatePicker && this.autoUpdateInput) { if (this.startDate && this.endDate) { // if we use ranges and should show range label on input if (this.rangesArray.length && this.showRangeLabelOnInput && this.chosenRange && this.locale.customRangeLabel !== this.chosenRange) { this.chosenLabel = this.chosenRange; } else { this.chosenLabel = this.startDate.format(format) + this.locale.separator + this.endDate.format(format); } } } else if (this.autoUpdateInput) { this.chosenLabel = this.startDate?.format(format); } } remove() { this.isShown = false; } /** * this should calculate the label */ calculateChosenLabel() { if (!this.locale || !this.locale.separator) { this._buildLocale(); } let customRange = true; let i = 0; if (this.rangesArray.length > 0) { for (const range in this.ranges) { if (this.ranges[range]) { if (this.timePicker) { const format = this.timePickerSeconds ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm'; // ignore times when comparing dates if time picker seconds is not enabled if (this.startDate?.format(format) === this.ranges[range][0].format(format) && this.endDate?.format(format) === this.ranges[range][1].format(format)) { customRange = false; this.chosenRange = this.rangesArray[i]; break; } } else { // ignore times when comparing dates if time picker is not enabled if (this.startDate?.format('YYYY-MM-DD') === this.ranges[range][0].format('YYYY-MM-DD') && this.endDate?.format('YYYY-MM-DD') === this.ranges[range][1].format('YYYY-MM-DD')) { customRange = false; this.chosenRange = this.rangesArray[i]; break; } } i++; } } if (customRange) { if (this.showCustomRangeLabel) { this.chosenRange = this.locale.customRangeLabel; } else { this.chosenRange = null; } // if custom label: show calendar this.showCalInRanges = true; } } this.updateElement(); } clickApply(e) { if (this.inline) this.applyBtn.disabled = true; if (!this.singleDatePicker && this.startDate && !this.endDate) { this.endDate = this._getDateWithTime(this.startDate, SideEnum.right); this.calculateChosenLabel(); } if (this.startDate && this.endDate) { // get if there are invalid date between range let d = this.startDate.clone(); while (d.isBefore(this.endDate)) { if (this.isInvalidDate(d)) { this.endDate = d.subtract(1, 'days'); this.calculateChosenLabel(); break; } d = d.add(1, 'days'); } } if (this.chosenLabel) { this.choosedDate.emit({ chosenLabel: this.chosenLabel, startDate: this.startDate, endDate: this.endDate }); } this.datesUpdated.emit({ startDate: this.startDate, endDate: this.endDate, label: this.chosenRange }); if (e || (this.closeOnAutoApply && !e)) { this.hide(); } } clickCancel(e) { this.startDate = this._old.start; this.endDate = this._old.end; if (this.inline) { this.updateView(); } this.cancelClicked.emit(); this.hide(); this.clearIncompleteDateSelection(); } /** * called when month is changed * @param monthEvent get value in event.target.value * @param side left or right */ monthChanged(monthEvent, side) { const year = this.calendarVariables[side].dropdowns.currentYear; const month = parseInt(monthEvent.target.value, 10); this.monthOrYearChanged(month, year, side); } /** * called when year is changed * @param yearEvent get value in event.target.value * @param side left or right */ yearChanged(yearEvent, side) { const month = this.calendarVariables[side].dropdowns.currentMonth; const year = parseInt(yearEvent.target.value, 10); this.monthOrYearChanged(month, year, side); } /** * called when time is changed * @param timeEvent an event * @param side left or right */ timeChanged(timeEvent, side) { let hour = parseInt(this.timepickerVariables[side].selectedHour, 10); const minute = parseInt(this.timepickerVariables[side].selectedMinute, 10); const second = this.timePickerSeconds ? parseInt(this.timepickerVariables[side].selectedSecond, 10) : 0; if (!this.timePicker24Hour) { const ampm = this.timepickerVariables[side].ampmModel; if (ampm === 'PM' && hour < 12) { hour += 12; } if (ampm === 'AM' && hour === 12) { hour = 0; } } if (side === SideEnum.left) { let start = this.startDate?.clone(); start = start?.hour(hour); start = start?.minute(minute); start = start?.second(second); this.setStartDate(start); if (this.singleDatePicker) { this.endDate = this.startDate?.clone(); } else if (this.endDate && this.endDate.format('YYYY-MM-DD') === start?.format('YYYY-MM-DD') && this.endDate.isBefore(start)) { this.setEndDate(start.clone()); } else if (!this.endDate && this.timePicker) { const startClone = this._getDateWithTime(start, SideEnum.right); if (startClone.isBefore(start)) { this.timepickerVariables[SideEnum.right].selectedHour = hour; this.timepickerVariables[SideEnum.right].selectedMinute = minute; this.timepickerVariables[SideEnum.right].selectedSecond = second; } } } else if (this.endDate) { let end = this.endDate.clone(); end = end.hour(hour); end = end.minute(minute); end = end.second(second); this.setEndDate(end); } // update the calendars so all clickable dates reflect the new time component this.updateCalendars(); // re-render the time pickers because changing one selection can affect what's enabled in another this.renderTimePicker(SideEnum.left); this.renderTimePicker(SideEnum.right); if (this.autoApply) { this.clickApply(); } } /** * call when month or year changed * @param month month number 0 -11 * @param year year eg: 1995 * @param side left or right */ monthOrYearChanged(month, year, side) { const isLeft = side === SideEnum.left; if (!isLeft) { if (year < this.startDate.year() || (year === this.startDate.year() && month < this.startDate.month())) { month = this.startDate.month(); year = this.startDate.year(); } } if (this.getMinDate()) { if (year < this.getMinDate().year() || (year === this.getMinDate().year() && month < this.getMinDate().month())) { month = this.getMinDate().month(); year = this.getMinDate().year(); } } if (this.getMaxDate()) { if (year > this.getMaxDate().year() || (year === this.getMaxDate().year() && month > this.getMaxDate().month())) { month = this.getMaxDate().month(); year = this.getMaxDate().year(); } } this.calendarVariables[side].dropdowns.currentYear = year; this.calendarVariables[side].dropdowns.currentMonth = month; if (isLeft) { this.leftCalendar.month = this.leftCalendar.month.month(month).year(year); if (this.linkedCalendars) { this.rightCalendar.month = this.leftCalendar.month.clone().add(1, 'month'); } } else { this.rightCalendar.month = this.rightCalendar.month.month(month).year(year); if (this.linkedCalendars) { this.leftCalendar.month = this.rightCalendar.month.clone().subtract(1, 'month'); } } this.updateCalendars(); } /** * Click on previous month * @param side left or right calendar */ clickPrev(side) { if (side === SideEnum.left) { this.leftCalendar.month = this.leftCalendar.month.subtract(1, 'month'); if (this.linkedCalendars) { this.rightCalendar.month = this.rightCalendar.month.subtract(1, 'month'); } } else { this.rightCalendar.month = this.rightCalendar.month.subtract(1, 'month'); } this.updateCalendars(); } /** * Click on next month * @param side left or right calendar */ clickNext(side) { if (side === SideEnum.left) { this.leftCalendar.month = this.leftCalendar.month.add(1, 'month'); } else { this.rightCalendar.month = this.rightCalendar.month.add(1, 'month'); if (this.linkedCalendars) { this.leftCalendar.month = this.leftCalendar.month.add(1, 'month'); } } this.updateCalendars(); } /** * When hovering a date * @param e event: get value by e.target.value * @param side left or right * @param row row position of the current date clicked * @param col col position of the current date clicked */ hoverDate(e, side, row, col) { const leftCalDate = this.calendarVariables.left.calendar[row][col]; const rightCalDate = this.calendarVariables.right.calendar[row][col]; if (this.pickingDate) { this.nowHoveredDate = side === SideEnum.left ? leftCalDate : rightCalDate; this.renderCalendar(SideEnum.left); this.renderCalendar(SideEnum.right); } const tooltip = side === SideEnum.left ? this.tooltiptext[leftCalDate] : this.tooltiptext[rightCalDate]; if (tooltip.length > 0) { e.target.setAttribute('title', tooltip); } } /** * When selecting a date * @param e event: get value by e.target.value * @param side left or right * @param row row position of the current date clicked * @param col col position of the current date clicked */ clickDate(e, side, row, col) { if (e.target.tagName === 'TD') { if (!e.target.classList.contains('available')) return; } else if (e.target.tagName === 'SPAN') { if (!e.target.parentElement.classList.contains('available')) return; } if (this.rangesArray.length) { this.chosenRange = this.locale.customRangeLabel; } let date = side === SideEnum.left ? this.leftCalendar.calendar[row][col] : this.rightCalendar.calendar[row][col]; if ((this.endDate || (date.isBefore(this.startDate, 'day') && !this.customRangeDirection)) && !this.lockStartDate) { // picking start this.applyBtn.disabled = true; if (this.timePicker) { date = this._getDateWithTime(date, SideEnum.left); } this.endDate = null; this.setStartDate(date.clone()); } else if (!this.endDate && date.isBefore(this.startDate) && !this.customRangeDirection) { // special case: clicking the same date for start/end, // but the time of the end date is before the start date this.setEndDate(this.startDate?.clone()); } else { // picking end this.applyBtn.disabled = false; if (this.timePicker) { date = this._getDateWithTime(date, SideEnum.right); } if (date.isBefore(this.startDate, 'day') === true && this.customRangeDirection) { this.setEndDate(this.startDate); this.setStartDate(date.clone()); } else { this.setEndDate(date.clone()); } if (this.autoApply) { this.calculateChosenLabel(); } } if (this.singleDatePicker) { this.applyBtn.disabled = false; this.setEndDate(this.startDate); this.updateElement(); if (this.autoApply) { this.clickApply(); } } this.updateView(); if (this.autoApply && this.startDate && this.endDate) { this.clickApply(); } // This is to cancel the blur event handler if the mouse was in one of the inputs e.stopPropagation(); } /** * Click on the custom range * @param e: Event * @param label */ clickRange(e, label) { this.chosenRange = label; if (label === this.locale.customRangeLabel) { this.isShown = true; // show calendars this.showCalInRanges = true; } else { const dates = this.ranges[label]; this.startDate = dates[0].clone(); this.endDate = dates[1].clone(); if (this.showRangeLabelOnInput && label !== this.locale.customRangeLabel) { this.chosenLabel = label; } else { this.calculateChosenLabel(); } this.showCalInRanges = (!this.rangesArray.length) || this.alwaysShowCalendars; if (!this.timePicker) { this.startDate = this.startDate?.startOf('day'); this.endDate = this.endDate?.endOf('day'); } if (!this.alwaysShowCalendars) { this.isShown = false; // hide calendars } this.rangeClicked.emit({ label: label, dates: dates }); if (!this.keepCalendarOpeningWithRange || this.autoApply) { this.clickApply(); } else { if (!this.alwaysShowCalendars) { return this.clickApply(); } if (this.getMaxDate() && this.getMaxDate()?.isSame(dates[0], 'month')) { this.rightCalendar.month = this.rightCalendar.month.month(dates[0].month()); this.rightCalendar.month = this.rightCalendar.month.year(dates[0].year()); this.leftCalendar.month = this.leftCalendar.month.month(dates[0].month() - 1); this.leftCalendar.month = this.leftCalendar.month.year(dates[1].year()); } else { this.leftCalendar.month = this.leftCalendar.month.month(dates[0].month()); this.leftCalendar.month = this.leftCalendar.month.year(dates[0].year()); // get the next year const nextMonth = dates[0].clone().add(1, 'month'); this.rightCalendar.month = this.rightCalendar.month.month(nextMonth.month()); this.rightCalendar.month = this.rightCalendar.month.year(nextMonth.year()); } this.updateCalendars(); if (this.timePicker) { this.renderTimePicker(SideEnum.left); this.renderTimePicker(SideEnum.right); } } } } show(e) { if (this.isShown) return; this._old.start = this.startDate?.clone(); this._old.end = this.endDate?.clone(); this.isShown = true; this.applyBtn.disabled = true; this.updateView(); } hide(e) { if (!this.isShown) return; // incomplete date selection, revert to last values if (!this.endDate) { if (this._old.start) this.startDate = this._old.start.clone(); if (this._old.end) this.endDate = this._old.end.clone(); this.clearIncompleteDateSelection(); } // if a new date range was selected, invoke the user callback function if (!this.startDate?.isSame(this._old.start) || !this.endDate?.isSame(this._old.end)) { // this.callback(this.startDate, this.endDate, this.chosenLabel); } // if picker is attached to a text input, update it this.updateElement(); this.isShown = false; this.applyBtn.disabled = true; this._ref.detectChanges(); } clearIncompleteDateSelection() { this.nowHoveredDate = null; this.pickingDate = false; } /** * handle click on all element in the component, useful for outside of click * @param e event */ handleInternalClick(e) { e.stopPropagation(); } /** * update the locale options * @param locale */ updateLocale(locale) { for (const key in locale) { if (locale.hasOwnProperty(key)) { this.locale[key] = locale[key]; if (key === 'customRangeLabel') { this.renderRanges(); } } } } /** * clear the daterange picker */ clear() { this.startDate = dayjs().startOf('day'); this.endDate = dayjs().endOf('day'); this.choosedDate.emit({ chosenLabel: '', startDate: null, endDate: null }); this.datesUpdated.emit({ startDate: null, endDate: null }); this.clearClicked.emit(); this.hide(); } /** * Find out if the selected range should be disabled if it doesn't * fit into minDate and maxDate limitations. */ disableRange(range) { if (range === this.locale.customRangeLabel) { return false; } const rangeMarkers = this.ranges[range]; const areBothBefore = rangeMarkers.every((date) => { if (!this.getMinDate()) { return false; } return date.isBefore(this.getMinDate()); }); const areBothAfter = rangeMarkers.every((date) => { if (!this.getMaxDate()) { return false; } return date.isAfter(this.getMaxDate()); }); return (areBothBefore || areBothAfter); } /** * * @param date the date to add time * @param side left or right */ _getDateWithTime(date, side) { let hour = parseInt(this.timepickerVariables[side].selectedHour, 10); if (!this.timePicker24Hour) { const ampm = this.timepickerVariables[side].ampmModel; if (ampm === 'PM' && hour < 12) hour += 12; if (ampm === 'AM' && hour === 12) hour = 0; } const minute = parseInt(this.timepickerVariables[side].selectedMinute, 10); const second = this.timePickerSeconds ? parseInt(this.timepickerVariables[side].selectedSecond, 10) : 0; return date.clone().hour(hour).minute(minute).second(second); } /** * build the locale config */ _buildLocale() { this.locale = { ...this._localeService.config, ...this.locale }; if (!this.locale.format) { if (this.timePicker) { this.locale.format = dayjs.localeData().longDateFormat('lll'); } else { this.locale.format = dayjs.localeData().longDateFormat('L'); } } } _buildCells(calendar, side) { for (let row = 0; row < 6; row++) { this.calendarVariables[side].classes[row] = {}; const rowClasses = []; if (this.emptyWeekRowClass && Array.from(Array(7).keys()).some(i => calendar[row][i].month() !== this.calendarVariables[side].month)) { rowClasses.push(this.emptyWeekRowClass); } for (let col = 0; col < 7; col++) { const classes = []; // empty week row class if (this.emptyWeekColumnClass) { if (calendar[row][col].month() !== this.calendarVariables[side].month) { classes.push(this.emptyWeekColumnClass); } } // highlight today's date if (calendar[row][col].isSame(new Date(), 'day')) classes.push('today'); // highlight weekends if (calendar[row][col].isoWeekday() > 5) classes.push('weekend'); // grey out the dates in other months displayed at beginning and end of this calendar if (calendar[row][col].month() !== calendar[1][1].month()) { classes.push('off'); // mark the last day of the previous month in this calendar if (this.lastDayOfPreviousMonthClass && (calendar[row][col].month() < calendar[1][1].month() || calendar[1][1].month() === 0) && calendar[row][col].date() === this.calendarVariables[side].daysInLastMonth) { classes.push(this.lastDayOfPreviousMonthClass); } // mark the first day of the next month in this calendar if (this.firstDayOfNextMonthClass && (calendar[row][col].month() > calendar[1][1].month() || calendar[row][col].month() === 0) && calendar[row][col].date() === 1) { classes.push(this.firstDayOfNextMonthClass); } } // mark the first day of the current month with a custom class if (this.firstMonthDayClass && calendar[row][col].month() === calendar[1][1].month() && calendar[row][col].date() === calendar.firstDay.date()) { classes.push(this.firstMonthDayClass); } // mark the last day of the current month with a custom class if (this.lastMonthDayClass && calendar[row][col].month() === calendar[1][1].month() && calendar[row][col].date() === calendar.lastDay.date()) { classes.push(this.lastMonthDayClass); } // don't allow selection of dates before the minimum date if (this.getMinDate() && calendar[row][col].isBefore(this.getMinDate(), 'day')) { classes.push('off', 'disabled'); } // don't allow selection of dates after the maximum date if (this.calendarVariables[side].maxDate && calendar[row][col].isAfter(this.calendarVariables[side].maxDate, 'day')) { classes.push('off', 'disabled'); } // don't allow selection of date if a custom function decides it's invalid if (this.isInvalidDate(calendar[row][col])) { classes.push('off', 'disabled', 'invalid'); } // highlight the currently selected start date if (this.startDate && calendar[row][col].format('YYYY-MM-DD') === this.startDate.format('YYYY-MM-DD')) { classes.push('active', 'start-date'); } // highlight the currently selected end date if (this.endDate != null && calendar[row][col].format('YYYY-MM-DD') === this.endDate.format('YYYY-MM-DD')) { classes.push('active', 'end-date'); } // highlight dates in-between the selected dates if (((this.nowHoveredDate != null && this.pickingDate) || this.endDate != null) && (calendar[row][col] > this.startDate && (calendar[row][col] < this.endDate || (calendar[row][col] < this.nowHoveredDate && this.pickingDate))) && (!classes.find(el => el === 'off'))) { classes.push('in-range'); } // apply custom classes for this date const isCustom = this.isCustomDate(calendar[row][col]); if (isCustom !== false) { if (typeof isCustom === 'string') { classes.p