@excelwebzone/symfony-admin-ui
Version:
Symfony Admin UI is a simple set of UI behaviors and components used with your [symfony-admin](https://github.com/excelwebzone/symfony-admin-bundle) application.
1,346 lines (1,089 loc) • 58.7 kB
JavaScript
import $ from 'jquery';
import moment from 'moment';
export default class DateRangePicker {
constructor(pickerEl, options, cb) {
this.parentEl = 'body';
this.$picker = $(pickerEl);
this.startDate = moment().startOf('day');
this.endDate = moment().endOf('day');
this.minDate = false;
this.maxDate = false;
this.maxSpan = false;
this.autoApply = false;
this.singleDatePicker = false;
this.showDropdowns = false;
this.minYear = moment().subtract(100, 'year').format('YYYY');
this.maxYear = moment().add(100, 'year').format('YYYY');
this.showWeekNumbers = false;
this.showISOWeekNumbers = false;
this.showCustomRangeLabel = true;
this.timePicker = false;
this.timePicker24Hour = false;
this.timePickerIncrement = 1;
this.timePickerHours = [];
this.linkedCalendars = true;
this.autoUpdateInput = true;
this.alwaysShowCalendars = false;
this.useModal = false;
this.ignoreMove = false;
this.isEmbedded = false;
this.ranges = {};
this.opens = 'right';
if (this.$picker.hasClass('float-right'))
this.opens = 'left';
this.drops = 'down';
if (this.$picker.hasClass('drop-up'))
this.drops = 'up';
this.locale = {
direction: 'ltr',
format: moment.localeData().longDateFormat('L'),
dateFormat: moment.localeData().longDateFormat('L'),
separator: ' - ',
applyLabel: 'Apply',
clearLabel: 'Clear',
cancelLabel: 'Cancel',
weekLabel: 'W',
customRangeLabel: 'Custom Range',
daysOfWeek: moment.weekdaysShort(),
monthNames: moment.monthsShort(),
firstDay: moment.localeData().firstDayOfWeek()
};
this.callback = function() {};
// some state information
this.isShowing = false;
this.isApply = false;
this.leftCalendar = {};
this.rightCalendar = {};
// custom options from user
if (typeof options !== 'object' || options === null)
options = {};
if (typeof options.useModal !== 'boolean')
options.useModal = false;
if (typeof options.isEmbedded !== 'boolean' || options.useModal)
options.isEmbedded = false;
// allow setting options with data attributes
// data-api options will be overwritten with custom javascript options
options = $.extend(this.$picker.data(), options);
// html template for the picker UI
if (typeof options.template !== 'string' && !(options.template instanceof $))
if (options.useModal)
options.template = `
<div class="modal modal-calendar">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-body">
<div class="range-select range-select-date-range">
<div class="range-select-content">
<div class="filter-calendar from-date-selector">
<div class="calendar left"></div>
<div class="range-select-range-field">
<div class="has-floating-label">
<select class="time-select"></select>
<input type="text" class="date-input input-text ignore-input" placeholder=" " maxlength="10" />
<label>From:</label>
</div>
</div>
</div>
<div class="filter-calendar to-date-selector">
<div class="calendar right"></div>
<div class="range-select-range-field">
<div class="has-floating-label">
<select class="time-select"></select>
<input type="text" class="date-input input-text ignore-input" placeholder=" " maxlength="10" />
<label>To:</label>
</div>
</div>
</div>
<div class="filter-calendar-button-panel">
<div class="ranges"></div>
</div>
</div>
</div>
</div>
<div class="modal-footer range-select-footer">
<div class="range-select-button-section">
<button type="button" class="btn btn-flat-default cancel-button" data-dismiss="modal"></button>
<button type="button" class="btn btn-flat-secondary clear-button"></button>
<button type="button" class="btn btn-primary apply-button"></button>
</div>
</div>
</div>
</div>
</div>
`;
else
options.template = `
<div class="range-select-container ${options.isEmbedded ? 'is-embedded' : ''}">
<div class="range-select range-select-date-range">
<div class="range-select-content">
<div class="filter-calendar from-date-selector">
<div class="calendar left"></div>
<div class="range-select-range-field">
<div class="has-floating-label">
<select class="time-select"></select>
<input type="text" class="date-input input-text ignore-input" placeholder=" " maxlength="10" />
<label>From:</label>
</div>
</div>
</div>
<div class="filter-calendar to-date-selector">
<div class="calendar right"></div>
<div class="range-select-range-field">
<div class="has-floating-label">
<select class="time-select"></select>
<input type="text" class="date-input input-text ignore-input" placeholder=" " maxlength="10" />
<label>To:</label>
</div>
</div>
</div>
<div class="filter-calendar-button-panel">
<div class="ranges"></div>
</div>
</div>
<div class="range-select-footer">
<div class="range-select-button-section">
<button type="button" class="btn btn-flat-default cancel-button"></button>
<button type="button" class="btn btn-flat-secondary clear-button"></button>
<button type="button" class="btn btn-primary apply-button"></button>
</div>
</div>
</div>
<div class="range-select-nub"></div>
</div>
`;
this.parentEl = (options.parentEl && $(options.parentEl).length) ? $(options.parentEl) : $(this.parentEl);
this.$container = $(options.template).appendTo(this.parentEl);
//
// handle all the possible options overriding defaults
//
if (typeof options.locale === 'object') {
if (typeof options.locale.direction === 'string')
this.locale.direction = options.locale.direction;
if (typeof options.locale.format === 'string')
this.locale.format = options.locale.format;
if (typeof options.locale.dateFormat === 'string')
this.locale.format = options.locale.dateFormat;
if (typeof options.locale.separator === 'string')
this.locale.separator = options.locale.separator;
if (typeof options.locale.daysOfWeek === 'object')
this.locale.daysOfWeek = options.locale.daysOfWeek.slice();
if (typeof options.locale.monthNames === 'object')
this.locale.monthNames = options.locale.monthNames.slice();
if (typeof options.locale.firstDay === 'number')
this.locale.firstDay = options.locale.firstDay;
if (typeof options.locale.applyLabel === 'string')
this.locale.applyLabel = options.locale.applyLabel;
if (typeof options.locale.clearLabel === 'string')
this.locale.clearLabel = options.locale.clearLabel;
if (typeof options.locale.cancelLabel === 'string')
this.locale.cancelLabel = options.locale.cancelLabel;
if (typeof options.locale.weekLabel === 'string')
this.locale.weekLabel = options.locale.weekLabel;
if (typeof options.locale.customRangeLabel === 'string') {
// support unicode chars in the custom range name.
let elem = document.createElement('textarea');
elem.innerHTML = options.locale.customRangeLabel;
let rangeHtml = elem.value;
this.locale.customRangeLabel = rangeHtml;
}
}
this.$container.addClass(this.locale.direction);
if (typeof options.startDate === 'string')
this.startDate = moment(options.startDate, this.locale.format);
if (typeof options.endDate === 'string')
this.endDate = moment(options.endDate, this.locale.format);
if (typeof options.minDate === 'string')
this.minDate = moment(options.minDate, this.locale.format);
if (typeof options.maxDate === 'string')
this.maxDate = moment(options.maxDate, this.locale.format);
if (typeof options.startDate === 'object')
this.startDate = moment(options.startDate);
if (typeof options.endDate === 'object')
this.endDate = moment(options.endDate);
if (typeof options.minDate === 'object')
this.minDate = moment(options.minDate);
if (typeof options.maxDate === 'object')
this.maxDate = moment(options.maxDate);
// sanity check for bad options
if (this.minDate && this.startDate.isBefore(this.minDate))
this.startDate = this.minDate.clone();
// sanity check for bad options
if (this.maxDate && this.endDate.isAfter(this.maxDate))
this.endDate = this.maxDate.clone();
if (typeof options.maxSpan === 'object')
this.maxSpan = options.maxSpan;
if (typeof options.dateLimit === 'object') // backwards compat
this.maxSpan = options.dateLimit;
if (typeof options.opens === 'string')
this.opens = options.opens;
if (typeof options.drops === 'string')
this.drops = options.drops;
if (typeof options.showWeekNumbers === 'boolean')
this.showWeekNumbers = options.showWeekNumbers;
if (typeof options.showISOWeekNumbers === 'boolean')
this.showISOWeekNumbers = options.showISOWeekNumbers;
if (typeof options.showDropdowns === 'boolean')
this.showDropdowns = options.showDropdowns;
if (typeof options.minYear === 'number')
this.minYear = options.minYear;
if (typeof options.maxYear === 'number')
this.maxYear = options.maxYear;
if (typeof options.showCustomRangeLabel === 'boolean')
this.showCustomRangeLabel = options.showCustomRangeLabel;
if (typeof options.singleDatePicker === 'boolean') {
this.singleDatePicker = options.singleDatePicker;
if (this.singleDatePicker)
this.endDate = this.startDate.clone();
}
if (typeof options.timePicker === 'boolean')
this.timePicker = options.timePicker;
if (typeof options.timePicker24Hour === 'boolean')
this.timePicker24Hour = options.timePicker24Hour;
if (typeof options.timePickerIncrement === 'number')
this.timePickerIncrement = options.timePickerIncrement;
if (typeof options.timePickerHours === 'object')
this.timePickerHours = options.timePickerHours;
if (typeof options.autoApply === 'boolean')
this.autoApply = options.autoApply;
if (typeof options.autoUpdateInput === 'boolean')
this.autoUpdateInput = options.autoUpdateInput;
if (typeof options.linkedCalendars === 'boolean')
this.linkedCalendars = options.linkedCalendars;
if (typeof options.isInvalidDate === 'function')
this.isInvalidDate = options.isInvalidDate;
if (typeof options.isCustomDate === 'function')
this.isCustomDate = options.isCustomDate;
if (typeof options.alwaysShowCalendars === 'boolean')
this.alwaysShowCalendars = options.alwaysShowCalendars;
if (typeof options.useModal === 'boolean')
this.useModal = options.useModal;
if (typeof options.ignoreMove === 'boolean')
this.ignoreMove = options.ignoreMove;
// update day names order to firstDay
if (this.locale.firstDay !== 0) {
let iterator = this.locale.firstDay;
while (iterator > 0) {
this.locale.daysOfWeek.push(this.locale.daysOfWeek.shift());
iterator--;
}
}
let start, end, range;
// if no start/end dates set, check if an input element contains initial values
if (typeof options.startDate === 'undefined' && typeof options.endDate === 'undefined') {
if ($(this.$picker).is(':text')) {
let val = $(this.$picker).val();
let split = val.split(this.locale.separator);
start = end = null;
if (split.length === 2) {
start = moment(split[0], this.locale.format);
end = moment(split[1], this.locale.format);
} else if (this.singleDatePicker && val !== '') {
start = moment(val, this.locale.format);
end = moment(val, this.locale.format);
}
if (start !== null && end !== null) {
this.setStartDate(start);
this.setEndDate(end);
}
}
}
if (typeof options.ranges === 'object') {
for (range in options.ranges) {
if (typeof options.ranges[range][0] === 'string')
start = moment(options.ranges[range][0], this.locale.format);
else
start = moment(options.ranges[range][0]);
if (typeof options.ranges[range][1] === 'string')
end = moment(options.ranges[range][1], this.locale.format);
else
end = moment(options.ranges[range][1]);
if (this.timePicker) {
// round to nearest timePickerIncrement minutes and force zero seconds
start.minute(Math.round(start.minute() / this.timePickerIncrement) * this.timePickerIncrement).second(0);
end.minute(Math.round(end.minute() / this.timePickerIncrement) * this.timePickerIncrement).second(0);
}
// if the start or end date exceed those allowed by the minDate or maxSpan
// options, shorten the range to the allowable period.
if (this.minDate && start.isBefore(this.minDate))
start = this.minDate.clone();
let maxDate = this.maxDate;
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.minDate && end.isBefore(this.minDate, this.timepicker ? 'minute' : 'day'))
|| (maxDate && start.isAfter(maxDate, this.timepicker ? 'minute' : 'day')))
continue;
// support unicode chars in the range names.
let elem = document.createElement('textarea');
elem.innerHTML = range;
let rangeHtml = elem.value;
this.ranges[rangeHtml] = [start, end];
}
let list = '<ul>';
for (range in this.ranges) {
list += `<li><button type="button" class="btn btn-flat-default" data-range-key="${range}">${range}</button></li>`;
}
if (this.showCustomRangeLabel) {
list += `<li><button type="button" class="btn btn-flat-default" data-range-key="${this.locale.customRangeLabel}">${this.locale.customRangeLabel}</button></li>`;
}
list += '</ul>';
this.$container.find('.ranges').prepend(list);
}
if (typeof cb === 'function') {
this.callback = cb;
}
if (this.autoApply && typeof options.ranges !== 'object') {
this.$container.find('.ranges').hide();
} else if (this.autoApply) {
this.$container.find('.apply-button, .clear-button').hide();
}
if (!this.timePicker) {
this.startDate = this.startDate.startOf('day');
this.endDate = this.endDate.endOf('day');
this.$container.find('.time-select').hide();
}
// can't be used together for now
if (this.timePicker && this.autoApply)
this.autoApply = false;
if (this.singleDatePicker) {
this.$container.addClass('single');
this.$container.find('.from-date-selector').addClass('single');
this.$container.find('.from-date-selector').show();
this.$container.find('.to-date-selector').hide();
this.$container.find('.filter-calendar-button-panel').hide();
if (!this.timePicker) {
this.$container.find('.range-select-range-field').hide();
this.$container.find('.range-select-footer').hide();
}
}
if ((typeof options.ranges === 'undefined' && !this.singleDatePicker) || this.alwaysShowCalendars) {
this.$container.addClass('show-calendar');
}
this.$container.addClass(`opens-${this.opens}`);
// swap the position of the predefined ranges if opens right
if (typeof options.ranges !== 'undefined' && this.opens === 'right') {
this.$container.find('.filter-calendar-button-panel').prependTo(this.$container.find('.range-select-content'));
}
// apply labels to buttons
this.$container.find('.apply-button').html(this.locale.applyLabel);
this.$container.find('.clear-button').html(this.locale.clearLabel);
this.$container.find('.cancel-button').html(this.locale.cancelLabel);
//
// event listeners
//
this.$container.find('.calendar')
.on('click.daterangepicker', '.ui-datepicker-prev', (e) => this.clickPrev(e))
.on('click.daterangepicker', '.ui-datepicker-next', (e) => this.clickNext(e))
.on('mousedown.daterangepicker', 'td.ui-datepicker-current-day', (e) => this.clickDate(e))
.on('mouseenter.daterangepicker', 'td.ui-datepicker-current-day', (e) => this.hoverDate(e))
.on('mouseleave.daterangepicker', 'td.ui-datepicker-current-day', () => this.updateFormInputs())
.on('change.daterangepicker', 'select.year-select', (e) => this.monthOrYearChanged(e))
.on('change.daterangepicker', 'select.month-select', (e) => this.monthOrYearChanged(e));
this.$container.find('select.time-select')
.on('change.daterangepicker', (e) => this.timeChanged(e));
this.$container.find('.range-select-range-field')
.on('click.daterangepicker', 'input', () => this.showCalendars())
.on('focus.daterangepicker', 'input', (e) => this.formInputsFocused(e))
.on('blur.daterangepicker', 'input', () => this.formInputsBlurred())
.on('change.daterangepicker', 'input', (e) => this.formInputsChanged(e))
.on('keydown.daterangepicker', 'input', (e) => this.formInputsKeydown(e));
this.$container.find('.ranges')
.on('click.daterangepicker', 'button', (e) => this.clickRange(e))
.on('mouseenter.daterangepicker', 'button', (e) => this.hoverRange(e))
.on('mouseleave.daterangepicker', 'button', () => this.updateFormInputs());
this.$container
.on('click.daterangepicker', 'button.apply-button', () => this.clickApply())
.on('click.daterangepicker', 'button.clear-button', () => this.clickClear())
.on('click.daterangepicker', 'button.cancel-button', () => this.clickCancel());
if (this.$picker.is('input') || this.$picker.is('button')) {
this.$picker.on({
'click.daterangepicker': () => this.show(),
'focus.daterangepicker': () => this.show(),
'keyup.daterangepicker': () => this.elementChanged(),
'keydown.daterangepicker': (e) => this.keydown(e) // IE 11 compatibility
});
} else {
this.$picker.on('click.daterangepicker', () => this.toggle());
this.$picker.on('keydown.daterangepicker', () => this.toggle());
}
// if attached to a text input, set the initial value
/* if (this.$picker.val())
this.updateElement(); */
}
setStartDate(startDate) {
if (typeof startDate === 'string')
this.startDate = moment(startDate, this.locale.format);
if (typeof startDate === 'object')
this.startDate = moment(startDate);
if (!this.timePicker)
this.startDate = this.startDate.startOf('day');
if (this.timePicker && this.timePickerIncrement)
this.startDate.minute(Math.round(this.startDate.minute() / this.timePickerIncrement) * this.timePickerIncrement);
if (this.minDate && this.startDate.isBefore(this.minDate)) {
this.startDate = this.minDate.clone();
if (this.timePicker && this.timePickerIncrement)
this.startDate.minute(Math.round(this.startDate.minute() / this.timePickerIncrement) * this.timePickerIncrement);
}
if (this.maxDate && this.startDate.isAfter(this.maxDate)) {
this.startDate = this.maxDate.clone();
if (this.timePicker && this.timePickerIncrement)
this.startDate.minute(Math.floor(this.startDate.minute() / this.timePickerIncrement) * this.timePickerIncrement);
}
/* if (!this.isShowing)
this.updateElement(); */
this.updateMonthsInView();
}
setEndDate(endDate) {
if (typeof endDate === 'string')
this.endDate = moment(endDate, this.locale.format);
if (typeof endDate === 'object')
this.endDate = moment(endDate);
if (!this.timePicker)
this.endDate = this.endDate.endOf('day');
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.maxDate && this.endDate.isAfter(this.maxDate))
this.endDate = this.maxDate.clone();
if (this.maxSpan && this.startDate.clone().add(this.maxSpan).isBefore(this.endDate))
this.endDate = this.startDate.clone().add(this.maxSpan);
this.previousRightTime = this.endDate.clone();
/* if (!this.isShowing)
this.updateElement(); */
this.updateMonthsInView();
}
isInvalidDate() {
return false;
}
isCustomDate() {
return false;
}
updateView() {
if (this.timePicker) {
this.renderTimePicker('left');
this.renderTimePicker('right');
if (!this.endDate) {
this.$container.find('.to-date-selector .time-select').disable();
} else {
this.$container.find('.to-date-selector .time-select').enable();
}
}
if (!this.endDate) {
this.$container.find('.to-date-selector .date-input').disable();
} else {
this.$container.find('.to-date-selector .date-input').enable();
}
this.updateMonthsInView();
this.updateCalendars();
this.updateFormInputs();
}
updateMonthsInView() {
if (this.endDate) {
// if both dates are visible already, do nothing
if (!this.singleDatePicker && this.leftCalendar.month && this.rightCalendar.month
&& (this.startDate.format('YYYY-MM') === this.leftCalendar.month.format('YYYY-MM') || 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;
}
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.maxDate && this.linkedCalendars && !this.singleDatePicker && this.rightCalendar.month > this.maxDate) {
this.rightCalendar.month = this.maxDate.clone().date(2);
this.leftCalendar.month = this.maxDate.clone().date(2).subtract(1, 'month');
}
}
updateCalendars() {
if (this.timePicker) {
let time = this.$container.find(`.${this.endDate ? 'from' : 'to'}-date-selector .time-select`).val();
let hour = time ? parseInt(time.split(':')[0]) : 0;
let minute = time ? parseInt(time.split(':')[1]) : 0;
this.leftCalendar.month.hour(hour).minute(minute).second(0);
this.rightCalendar.month.hour(hour).minute(minute).second(0);
}
this.renderCalendar('left');
this.renderCalendar('right');
// highlight any predefined range matching the current start and end dates
this.$container.find('.ranges button').removeClass('btn-primary').addClass('btn-flat-default');
if (this.endDate === null) return;
this.calculateChosenLabel();
}
/**
* Build the matrix of dates that will populate the calendar
*/
renderCalendar(side) {
let calendar = side === 'left' ? this.leftCalendar : this.rightCalendar;
let month = calendar.month.month();
let year = calendar.month.year();
let hour = calendar.month.hour();
let minute = calendar.month.minute();
let second = calendar.month.second();
let daysInMonth = moment([year, month]).daysInMonth();
let firstDay = moment([year, month, 1]);
let lastDay = moment([year, month, daysInMonth]);
let lastMonth = moment(firstDay).subtract(1, 'month').month();
let lastYear = moment(firstDay).subtract(1, 'month').year();
let daysInLastMonth = moment([lastYear, lastMonth]).daysInMonth();
let dayOfWeek = firstDay.day();
// initialize a 6 rows x 7 columns array for the calendar
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 = moment([lastYear, lastMonth, startDay, 12, minute, second]);
for (let i = 0, col = 0, row = 0; i < 42; i++, col++, curDate = moment(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.hour(12);
if (this.minDate && calendar[row][col].format('YYYY-MM-DD') === this.minDate.format('YYYY-MM-DD') && calendar[row][col].isBefore(this.minDate) && side === 'left') {
calendar[row][col] = this.minDate.clone();
}
if (this.maxDate && calendar[row][col].format('YYYY-MM-DD') === this.maxDate.format('YYYY-MM-DD') && calendar[row][col].isAfter(this.maxDate) && side === 'right') {
calendar[row][col] = this.maxDate.clone();
}
}
// make the calendar object available to hoverDate/clickDate
if (side === 'left') {
this.leftCalendar.calendar = calendar;
} else {
this.rightCalendar.calendar = calendar;
}
//
// Display the calendar
//
var minDate = side === 'left' ? this.minDate : this.startDate;
var maxDate = this.maxDate;
let html = '<div class="ui-datepicker"><div class="ui-datepicker-header">';
if ((!minDate || minDate.isBefore(calendar.firstDay)) && (!this.linkedCalendars || side === 'left')) {
html += '<a class="ui-datepicker-prev" title="Prev"><span class="ui-icon ui-icon-circle-triangle-w">Prev</span></a>';
}
let monthHtml = this.locale.monthNames[calendar[1][1].month()];
let yearHtml = calendar[1][1].format('YYYY');
if (this.showDropdowns) {
let currentMonth = calendar[1][1].month();
let currentYear = calendar[1][1].year();
let maxYear = (maxDate && maxDate.year()) || (this.maxYear);
let minYear = (minDate && minDate.year()) || (this.minYear);
let inMinYear = currentYear === minYear;
let inMaxYear = currentYear === maxYear;
monthHtml = '<select class="month-select">';
for (let m = 0; m < 12; m++) {
if ((!inMinYear || (minDate && m >= minDate.month())) && (!inMaxYear || (maxDate && m <= maxDate.month()))) {
monthHtml += `<option value="${m}" ${m === currentMonth ? 'selected="selected"' : ''}>${this.locale.monthNames[m]}</option>`;
} else {
monthHtml += `<option value="${m}" ${m === currentMonth ? 'selected="selected"' : ''} disabled="disabled">${this.locale.monthNames[m]}</option>`;
}
}
monthHtml += '</select>';
yearHtml = '<select class="year-select">';
for (let y = minYear; y <= maxYear; y++) {
yearHtml += `<option value="${y}" ${y === currentYear ? 'selected="selected"' : ''}>${y}</option>`;
}
yearHtml += '</select>';
}
html += `<div class="ui-datepicker-title">
<span class="ui-datepicker-month">${monthHtml}</span> <span class="ui-datepicker-year">${yearHtml}</span>
</div>`;
if ((!maxDate || maxDate.isAfter(calendar.lastDay)) && (!this.linkedCalendars || side === 'right' || this.singleDatePicker)) {
html += '<a class="ui-datepicker-next" title="Next"><span class="ui-icon ui-icon-circle-triangle-e">Next</span></a>';
}
html += '</div>'; // end `ui-datepicker-header`
html += '<table class="ui-datepicker-calendar"><thead><tr>';
// add week number label
if (this.showWeekNumbers || this.showISOWeekNumbers)
html += `<th class="week">${this.locale.weekLabel}</th>`;
for (let [index, dayOfWeek] of Object.entries(this.locale.daysOfWeek)) {
html += `<th><span title="${this.locale.monthNames[index]}">${dayOfWeek}</span></th>`;
}
html += '</tr></thead><tbody>';
// adjust maxDate to reflect the maxSpan setting in order to
// grey out end dates beyond the maxSpan
if (this.endDate === null && this.maxSpan) {
var maxLimit = this.startDate.clone().add(this.maxSpan).endOf('day');
if (!maxDate || maxLimit.isBefore(maxDate)) {
maxDate = maxLimit;
}
}
for (let row = 0; row < 6; row++) {
html += '<tr>';
// add week number
if (this.showWeekNumbers)
html += `<td class="ui-datepicker-week">${calendar[row][0].week()}</td>`;
else if (this.showISOWeekNumbers)
html += `<td class="ui-datepicker-week">${calendar[row][0].isoWeek()}</td>`;
for (let col = 0; col < 7; col++) {
let classes = [];
// mark first day of the month
if (calendar[row][col].date() === calendar[row][col].clone().startOf('month').date())
classes.push('is-first');
// mark last day of the month
if (calendar[row][col].date() === calendar[row][col].clone().endOf('month').date())
classes.push('is-last');
// highlight today's date
if (calendar[row][col].isSame(new Date(), 'day'))
classes.push('ui-datepicker-today');
// highlight weekends
if (calendar[row][col].isoWeekday() > 5)
classes.push('ui-datepicker-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('ui-datepicker-other-month', 'ui-datepicker-ends');
// don't allow selection of dates before the minimum date
if (this.minDate && calendar[row][col].isBefore(this.minDate, 'day'))
classes.push('ui-state-disabled');
// don't allow selection of dates after the maximum date
if (maxDate && calendar[row][col].isAfter(maxDate, 'day'))
classes.push('ui-state-disabled');
// don't allow selection of date if a custom function decides it's invalid
if (this.isInvalidDate(calendar[row][col]))
classes.push('ui-state-disabled');
// highlight the currently selected start date
if (calendar[row][col].format('YYYY-MM-DD') === this.startDate.format('YYYY-MM-DD'))
classes.push('is-selected', 'is-from');
// 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('is-selected', 'is-to');
// highlight dates in-between the selected dates
if (this.endDate !== null && calendar[row][col] > this.startDate && calendar[row][col] < this.endDate)
classes.push('is-in-range');
// apply custom classes for this date
let isCustom = this.isCustomDate(calendar[row][col]);
if (isCustom !== false) {
if (typeof isCustom === 'string')
classes.push(isCustom);
else
Array.prototype.push.apply(classes, isCustom);
}
if (classes.indexOf('ui-state-disabled') === -1)
classes.push('ui-datepicker-current-day');
html += `<td class="${classes.join(' ').replace(/^\s+|\s+$/g, '')}" data-title="r${row}c${col}"><span>${calendar[row][col].date()}</span></td>`;
}
html += '</tr>';
}
html += '</tbody></table></div>';
this.$container.find(`.calendar.${side}`).html(html);
}
renderTimePicker(side) {
// don't bother updating the time picker if it's currently disabled
// because an end date hasn't been clicked yet
if (side === 'right' && !this.endDate) return;
let selected;
let minDate;
let maxDate = this.maxDate;
if (this.maxSpan && (!this.maxDate || this.startDate.clone().add(this.maxSpan).isBefore(this.maxDate)))
maxDate = this.startDate.clone().add(this.maxSpan);
if (side === 'left') {
selected = this.startDate.clone();
minDate = this.minDate;
} else if (side === 'right') {
selected = this.endDate.clone();
minDate = this.startDate;
// preserve the time already selected
let $timeSelector = this.$container.find('.to-date-selector .time-select');
if ($timeSelector.html() !== '') {
selected.hour(!isNaN(selected.hour()) ? selected.hour() : ($timeSelector.val() || '00:00').split(':')[0]);
selected.minute(!isNaN(selected.minute()) ? selected.minute() : ($timeSelector.val() || '00:00').split(':')[1]);
selected.second(0);
}
if (selected.isBefore(this.startDate))
selected = this.startDate.clone();
if (maxDate && selected.isAfter(maxDate))
selected = maxDate.clone();
}
// force zero seconds
selected.second(0);
let hours = [];
if (this.timePickerHours.length === 0) {
for (let hour = 0; hour < 24; hour++) {
hours.push(hour);
}
}
// check and update selected option
let found = false;
for (let hour of hours) {
for (let m = 0; m < Math.round(60 / this.timePickerIncrement); m++) {
let minute = m * this.timePickerIncrement;
let time = selected.clone().hour(hour).minute(minute);
if (time.isSame(selected)) {
found = true;
break;
}
}
}
if (!found) {
selected.hour(0).minute(0);
if (side === 'left') {
this.startDate.hour(0).minute(0);
} else if (side === 'right') {
this.endDate.hour(0).minute(0);
}
}
let choices = [];
for (let hour of hours) {
for (let m = 0; m < Math.round(60 / this.timePickerIncrement); m++) {
let minute = m * this.timePickerIncrement;
let timeLabel = hour < 10 ? String(hour).padStart(2, '0') : hour;
if (this.timePicker24Hour)
timeLabel += `:${minute < 10 ? String(minute).padStart(2, '0') : minute}`;
else if (hour === 0)
timeLabel = `12:${minute < 10 ? String(minute).padStart(2, '0') : minute} AM`;
else if (hour === 12)
timeLabel += `:${minute < 10 ? String(minute).padStart(2, '0') : minute} PM`;
else if (hour < 12)
timeLabel += `:${minute < 10 ? String(minute).padStart(2, '0') : minute} AM`;
else if (hour > 12) {
timeLabel = (hour - 12) < 10 ? String(hour - 12).padStart(2, '0') : (hour - 12);
timeLabel += `:${minute < 10 ? String(minute).padStart(2, '0') : minute} PM`;
}
if (hour === 0 && minute === 0)
timeLabel = 'Midnight';
else if (hour === 12 && minute === 0)
timeLabel = 'Noon';
let time = selected.clone().hour(hour).minute(minute);
let isDisabled = (minDate && time.isBefore(minDate))
|| (maxDate && time.isAfter(maxDate));
let isSelected = time.isSame(selected);
choices.push(`<option value="${hour < 10 ? String(hour).padStart(2, '0') : hour}:${minute < 10 ? String(minute).padStart(2, '0') : minute}" ${isSelected ? 'selected="selected"' : ''} ${isDisabled ? 'disabled="disabled"' : ''}>${timeLabel}</option>`);
}
}
this.$container.find(`.${side === 'left' ? 'from' : 'to'}-date-selector .time-select`).html(choices.join(''));
}
updateFormInputs() {
// ignore mouse movements while an above-calendar text input has focus
if (this.$container.find('.from-date-selector .date-input').is(':focus') || this.$container.find('.to-date-selector .date-input').is(':focus'))
return;
this.$container.find('.from-date-selector .date-input').val(this.startDate.format(this.locale.dateFormat));
if (this.endDate)
this.$container.find('.to-date-selector .date-input').val(this.endDate.format(this.locale.dateFormat));
if (this.singleDatePicker || (this.endDate && (this.startDate.isBefore(this.endDate) || this.startDate.isSame(this.endDate)))) {
this.$container.find('button.apply-button').enable();
} else {
this.$container.find('button.apply-button').disable();
}
}
move() {
if (this.ignoreMove || this.useModal) return;
let parentOffset = { top: 0, left: 0 };
let containerTop;
let drops = this.drops;
let parentRightEdge = $(window).width();
if (!this.parentEl.is('body')) {
parentOffset = {
top: this.parentEl.offset().top - this.parentEl.scrollTop(),
left: this.parentEl.offset().left - this.parentEl.scrollLeft()
};
parentRightEdge = this.parentEl[0].clientWidth + this.parentEl.offset().left;
}
switch (drops) {
case 'auto':
containerTop = this.$picker.offset().top + this.$picker.outerHeight() - parentOffset.top;
if (containerTop + this.$container.outerHeight() >= this.parentEl[0].scrollHeight) {
containerTop = this.$picker.offset().top - this.$container.outerHeight() - parentOffset.top;
drops = 'up';
}
break;
case 'up':
containerTop = this.$picker.offset().top - this.$container.outerHeight() - parentOffset.top;
break;
default:
containerTop = this.$picker.offset().top + this.$picker.outerHeight() - parentOffset.top;
break;
}
// force the container to it's actual width
this.$container.css({
top: 0,
left: 0,
right: 'auto'
});
let containerWidth = this.$container.outerWidth();
this.$container.toggleClass('drop-up', drops === 'up');
if (this.opens === 'left') {
let containerRight = parentRightEdge - this.$picker.offset().left - this.$picker.outerWidth();
if (containerWidth + containerRight > $(window).width()) {
this.$container.css({
top: containerTop,
right: 'auto',
left: 9
});
} else {
this.$container.css({
top: containerTop,
right: containerRight,
left: 'auto'
});
}
} else if (this.opens === 'center') {
let containerLeft = this.$picker.offset().left - parentOffset.left + this.$picker.outerWidth() / 2 - containerWidth / 2;
if (containerLeft < 0) {
this.$container.css({
top: containerTop,
right: 'auto',
left: 9
});
} else if (containerLeft + containerWidth > $(window).width()) {
this.$container.css({
top: containerTop,
left: 'auto',
right: 0
});
} else {
this.$container.css({
top: containerTop,
left: containerLeft,
right: 'auto'
});
}
} else {
let containerLeft = this.$picker.offset().left - parentOffset.left;
if (containerLeft + containerWidth > $(window).width()) {
this.$container.css({
top: containerTop,
left: 'auto',
right: 0
});
} else {
this.$container.css({
top: containerTop,
left: containerLeft,
right: 'auto'
});
}
}
}
show() {
if (this.isShowing) return;
// create a click proxy that is private to this instance of datepicker, for unbinding
this._outsideClickProxy = (e) => this.outsideClick(e);
// bind global datepicker mousedown for hiding and
$(document)
.on('mousedown.daterangepicker', this._outsideClickProxy)
// also support mobile devices
.on('touchend.daterangepicker', this._outsideClickProxy)
// also explicitly play nice with Bootstrap dropdowns, which stopPropagation when clicking them
.on('click.daterangepicker', '[data-toggle=dropdown]', this._outsideClickProxy)
// and also close when focus changes to outside the picker (eg. tabbing between controls)
.on('focusin.daterangepicker', this._outsideClickProxy);
// reposition the picker if the window is resized while it's open
$(window).on('resize.daterangepicker', this.move());
this.oldStartDate = this.startDate.clone();
this.oldEndDate = this.endDate.clone();
this.previousRightTime = this.endDate.clone();
this.updateView();
if (this.useModal)
this.$container.modal('show');
else
this.$container.show();
this.move();
this.$picker.trigger('show.daterangepicker', this);
this.isShowing = true;
this.isApply = false;
}
hide() {
if (!this.isShowing) return;
// incomplete date selection, revert to last values
if (!this.endDate) {
this.startDate = this.oldStartDate.clone();
this.endDate = this.oldEndDate.clone();
}
if (this.isApply) {
// if a new date range was selected, invoke the user callback function
this.callback(this.startDate.clone(), this.endDate.clone(), this.chosenLabel);
// if picker is attached to a text input, update it
this.updateElement();
}
$(document).off('.daterangepicker');
$(window).off('.daterangepicker');
if (this.useModal)
this.$container.modal('hide');
else
this.$container.hide();
this.$picker.trigger('hide.daterangepicker', this);
this.isShowing = false;
this.isApply = false;
}
toggle() {
if (this.isShowing) {
this.hide();
} else {
this.show();
}
}
outsideClick(e) {
let target = $(e.target);
// if the page is clicked anywhere except within the daterangerpicker/button
// itself then call this.hide()
if (
// ie modal dialog fix
e.type === 'focusin'
|| target.closest(this.$picker).length
|| target.closest(this.$container).length
) return;
this.hide();
this.$picker.trigger('outsideClick.daterangepicker', this);
}
showCalendars() {
this.$container.addClass('show-calendar');
this.move();
this.$picker.trigger('showCalendar.daterangepicker', this);
}
hideCalendars() {
this.$container.removeClass('show-calendar');
this.$picker.trigger('hideCalendar.daterangepicker', this);
}
hoverRange(e) {
// ignore mouse movements while an above-calendar text input has focus
if (this.$container.find('.from-date-selector .date-input').is(':focus') || this.$container.find('.to-date-selector .date-input').is(':focus'))
return;
let label = e.target.getAttribute('data-range-key');
if (label === this.locale.customRangeLabel) {
this.updateView();
} else {
let dates = this.ranges[label];
this.$container.find('.from-date-selector .date-input').val(dates[0].format(this.locale.dateFormat));
this.$container.find('.to-date-selector .date-input').val(dates[1].format(this.locale.dateFormat));
}
}
clickRange(e) {
e.preventDefault();
let label = e.target.getAttribute('data-range-key');
this.chosenLabel = label;
if (label === this.locale.customRangeLabel) {
this.showCalendars();
} else {
let dates = this.ranges[label];
this.startDate = dates[0];
this.endDate = dates[1];
if (!this.timePicker) {
this.startDate.startOf('day');
this.endDate.endOf('day');
}
if (!this.alwaysShowCalendars)
this.hideCalendars();
if (this.autoApply)
this.clickApply();
this.updateView();
}
}
clickPrev(e) {
let $calendar = $(e.currentTarget).parents('.calendar');
if ($calendar.hasClass('left')) {
this.leftCalendar.month.subtract(1, 'month');
if (this.linkedCalendars)
this.rightCalendar.month.subtract(1, 'month');
} else {
this.rightCalendar.month.subtract(1, 'month');
}
this.updateCalendars();
}
clickNext(e) {
let $calendar = $(e.currentTarget).parents('.calendar');
if ($calendar.hasClass('left')) {
this.leftCalendar.month.add(1, 'month');
} else {
this.rightCalendar.month.add(1, 'month');
if (this.linkedCalendars)
this.leftCalendar.month.add(1, 'month');
}
this.updateCalendars();
}
hoverDate(e) {
// ignore dates that can't be selected
if (!$(e.currentTarget).hasClass('ui-datepicker-current-day')) return;
// have the text inputs above calendars reflect the date being hovered over
let title = $(e.currentTarget).attr('data-title');
let row = title.substr(1, 1);
let col = title.substr(3, 1);
let $calendar = $(e.currentTarget).parents('.calendar');
let date = $calendar.hasClass('left') ? this.leftCalendar.calendar[row][col] : this.rightCalendar.calendar[row][col];
if (this.endDate && !this.$container.find('.from-date-selector .date-input').is(':focus')) {
this.$container.find('.from-date-selector .date-input').val(date.format(this.locale.dateFormat));
} else if (!this.endDate && !this.$container.find('.to-date-selector .date-input').is(':focus')) {
this.$container.find('.to-date-selector .date-input').val(date.format(this.locale.dateFormat));
}
// highlight the dates between the start date and the date being hovered as a potential end date
let leftCalendar = this.leftCalendar;
let rightCalendar = this.rightCalendar;
let startDate = this.startDate;
if (!this.endDate) {
this.$container.find('.calendar tbody td').each(function(index, el) {
// skip week numbers, only look at dates
if ($(el).hasClass('ui-datepicker-week')) return;
let title = $(el).attr('data-title');
let row = title.substr(1, 1);
let col = title.substr(3, 1);
let $calendar = $(el).parents('.calendar');
let checkDate = $calendar.hasClass('left') ? leftCalendar.calendar[row][col] : rightCalendar.calendar[row][col];
if ((checkDate.isAfter(startDate) && checkDate.isBefore(date)) || checkDate.isSame(date, 'day')) {
$(el).addClass('is-in-range');
} else {
$(el).removeClass('is-in-range');
}
});
}
}
/**
* List of items performed:
* - alternate between selecting a start and end date for the range,
* - if the time picker is enabled, apply the hour/minute/second from the select boxes to the clicked date
* - if autoapply is enabled, and an end date was chosen, apply the selection
* - if single date picker mode, and time picker isn't enabled, apply the selection immediately
* - if one of the inputs above the calendars was focused, cancel that manual input
*/
clickDate(e) {
if (!$(e.currentTarget).hasClass('ui-datepicker-current-day')) return;
let title = $(e.currentTarget).attr('data-title');
let row = title.substr(1, 1);
let col = title.substr(3, 1);
let $calendar = $(e.currentTarget).parents('.calendar');
let date = $calendar.hasClass('left') ? this.leftCalendar.calendar[row][col] : this.rightCalendar.calendar[row][col];
// picking start
if (this.endDate || date.isBefore(this.startDate, 'day')) {
if (this.timePicker) {
let time = this.$container.find('.from-date-selector .time-select').val();
let hour = time ? parseInt(time.split(':')[0]) : 0;
let minute = time ? parseInt(time.split(':')[1]) : 0;
date = date.clone().hour(hour).minute(minute).second(0);
}
this.endDate = null;
this.setStartDate(date.clone());
// special case: clicking the same date for start/end,
// but the time of the end date is before the start date
} else if (!this.endDate && date.isBefore(this.startDate)) {
this.setEndDate(this.startDate.clone());
// picking end
} else {
if (this.timePicker) {
let time = this.$container.find('.to-date-selector .time-select').val();
let hour = time ? parseInt(time.split(':')[0]) : 0;
let minute = time ? parseInt(time.split(':')[1]) : 0;
date = date.clone().hour(hour).minute(minute).second(0);
}
this.setEndDate(date.clone());
if (this.autoApply) {
this.calculateChosenLabel();
this.clickApply();