(function($) { 'use strict'; let _defaults = { // Close when date is selected autoClose: false, // the default output format for the input field value format: 'mmm dd, yyyy', // Used to create date object from current input string parse: null, // The initial date to view when first opened defaultDate: null, // Make the `defaultDate` the initial selected value setDefaultDate: false, disableWeekends: false, disableDayFn: null, // First day of week (0: Sunday, 1: Monday etc) firstDay: 0, // The earliest date that can be selected minDate: null, // Thelatest date that can be selected maxDate: null, // Number of years either side, or array of upper/lower range yearRange: 10, // used internally (don't config outside) minYear: 0, maxYear: 9999, minMonth: undefined, maxMonth: undefined, startRange: null, endRange: null, isRTL: false, // Render the month after year in the calendar title showMonthAfterYear: false, // Render days of the calendar grid that fall in the next or previous month showDaysInNextAndPreviousMonths: false, // Specify a DOM element to render the calendar in container: null, // Show clear button showClearBtn: false, // internationalization i18n: { cancel: 'Cancel', clear: 'Clear', done: 'Ok', previousMonth: '‹', nextMonth: '›', months: [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ], monthsShort: [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ], weekdays: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], weekdaysShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], weekdaysAbbrev: ['S', 'M', 'T', 'W', 'T', 'F', 'S'] }, // events array events: [], // callback function onSelect: null, onOpen: null, onClose: null, onDraw: null }; /** * @class * */ class Datepicker extends Component { /** * Construct Datepicker instance and set up overlay * @constructor * @param {Element} el * @param {Object} options */ constructor(el, options) { super(Datepicker, el, options); this.el.M_Datepicker = this; this.options = $.extend({}, Datepicker.defaults, options); // make sure i18n defaults are not lost when only few i18n option properties are passed if (!!options && options.hasOwnProperty('i18n') && typeof options.i18n === 'object') { this.options.i18n = $.extend({}, Datepicker.defaults.i18n, options.i18n); } // Remove time component from minDate and maxDate options if (this.options.minDate) this.options.minDate.setHours(0, 0, 0, 0); if (this.options.maxDate) this.options.maxDate.setHours(0, 0, 0, 0); this.id = M.guid(); this._setupVariables(); this._insertHTMLIntoDOM(); this._setupModal(); this._setupEventHandlers(); if (!this.options.defaultDate) { this.options.defaultDate = new Date(Date.parse(this.el.value)); } let defDate = this.options.defaultDate; if (Datepicker._isDate(defDate)) { if (this.options.setDefaultDate) { this.setDate(defDate, true); this.setInputValue(); } else { this.gotoDate(defDate); } } else { this.gotoDate(new Date()); } /** * Describes open/close state of datepicker * @type {Boolean} */ this.isOpen = false; } static get defaults() { return _defaults; } static init(els, options) { return super.init(this, els, options); } static _isDate(obj) { return /Date/.test(Object.prototype.toString.call(obj)) && !isNaN(obj.getTime()); } static _isWeekend(date) { let day = date.getDay(); return day === 0 || day === 6; } static _setToStartOfDay(date) { if (Datepicker._isDate(date)) date.setHours(0, 0, 0, 0); } static _getDaysInMonth(year, month) { return [31, Datepicker._isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][ month ]; } static _isLeapYear(year) { // solution by Matti Virkkunen: http://stackoverflow.com/a/4881951 return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; } static _compareDates(a, b) { // weak date comparison (use setToStartOfDay(date) to ensure correct result) return a.getTime() === b.getTime(); } static _setToStartOfDay(date) { if (Datepicker._isDate(date)) date.setHours(0, 0, 0, 0); } /** * Get Instance */ static getInstance(el) { let domElem = !!el.jquery ? el[0] : el; return domElem.M_Datepicker; } /** * Teardown component */ destroy() { this._removeEventHandlers(); this.modal.destroy(); $(this.modalEl).remove(); this.destroySelects(); this.el.M_Datepicker = undefined; } destroySelects() { let oldYearSelect = this.calendarEl.querySelector('.orig-select-year'); if (oldYearSelect) { M.FormSelect.getInstance(oldYearSelect).destroy(); } let oldMonthSelect = this.calendarEl.querySelector('.orig-select-month'); if (oldMonthSelect) { M.FormSelect.getInstance(oldMonthSelect).destroy(); } } _insertHTMLIntoDOM() { if (this.options.showClearBtn) { $(this.clearBtn).css({ visibility: '' }); this.clearBtn.innerHTML = this.options.i18n.clear; } this.doneBtn.innerHTML = this.options.i18n.done; this.cancelBtn.innerHTML = this.options.i18n.cancel; if (this.options.container) { this.$modalEl.appendTo(this.options.container); } else { this.$modalEl.insertBefore(this.el); } } _setupModal() { this.modalEl.id = 'modal-' + this.id; this.modal = M.Modal.init(this.modalEl, { onCloseEnd: () => { this.isOpen = false; } }); } toString(format) { format = format || this.options.format; if (!Datepicker._isDate(this.date)) { return ''; } let formatArray = format.split(/(d{1,4}|m{1,4}|y{4}|yy|!.)/g); let formattedDate = formatArray .map((label) => { if (this.formats[label]) { return this.formats[label](); } return label; }) .join(''); return formattedDate; } setDate(date, preventOnSelect) { if (!date) { this.date = null; this._renderDateDisplay(); return this.draw(); } if (typeof date === 'string') { date = new Date(Date.parse(date)); } if (!Datepicker._isDate(date)) { return; } let min = this.options.minDate, max = this.options.maxDate; if (Datepicker._isDate(min) && date < min) { date = min; } else if (Datepicker._isDate(max) && date > max) { date = max; } this.date = new Date(date.getTime()); this._renderDateDisplay(); Datepicker._setToStartOfDay(this.date); this.gotoDate(this.date); if (!preventOnSelect && typeof this.options.onSelect === 'function') { this.options.onSelect.call(this, this.date); } } setInputValue() { this.el.value = this.toString(); this.$el.trigger('change', { firedBy: this }); } _renderDateDisplay() { let displayDate = Datepicker._isDate(this.date) ? this.date : new Date(); let i18n = this.options.i18n; let day = i18n.weekdaysShort[displayDate.getDay()]; let month = i18n.monthsShort[displayDate.getMonth()]; let date = displayDate.getDate(); this.yearTextEl.innerHTML = displayDate.getFullYear(); this.dateTextEl.innerHTML = `${day}, ${month} ${date}`; } /** * change view to a specific date */ gotoDate(date) { let newCalendar = true; if (!Datepicker._isDate(date)) { return; } if (this.calendars) { let firstVisibleDate = new Date(this.calendars[0].year, this.calendars[0].month, 1), lastVisibleDate = new Date( this.calendars[this.calendars.length - 1].year, this.calendars[this.calendars.length - 1].month, 1 ), visibleDate = date.getTime(); // get the end of the month lastVisibleDate.setMonth(lastVisibleDate.getMonth() + 1); lastVisibleDate.setDate(lastVisibleDate.getDate() - 1); newCalendar = visibleDate < firstVisibleDate.getTime() || lastVisibleDate.getTime() < visibleDate; } if (newCalendar) { this.calendars = [ { month: date.getMonth(), year: date.getFullYear() } ]; } this.adjustCalendars(); } adjustCalendars() { this.calendars[0] = this.adjustCalendar(this.calendars[0]); this.draw(); } adjustCalendar(calendar) { if (calendar.month < 0) { calendar.year -= Math.ceil(Math.abs(calendar.month) / 12); calendar.month += 12; } if (calendar.month > 11) { calendar.year += Math.floor(Math.abs(calendar.month) / 12); calendar.month -= 12; } return calendar; } nextMonth() { this.calendars[0].month++; this.adjustCalendars(); } prevMonth() { this.calendars[0].month--; this.adjustCalendars(); } render(year, month, randId) { let opts = this.options, now = new Date(), days = Datepicker._getDaysInMonth(year, month), before = new Date(year, month, 1).getDay(), data = [], row = []; Datepicker._setToStartOfDay(now); if (opts.firstDay > 0) { before -= opts.firstDay; if (before < 0) { before += 7; } } let previousMonth = month === 0 ? 11 : month - 1, nextMonth = month === 11 ? 0 : month + 1, yearOfPreviousMonth = month === 0 ? year - 1 : year, yearOfNextMonth = month === 11 ? year + 1 : year, daysInPreviousMonth = Datepicker._getDaysInMonth(yearOfPreviousMonth, previousMonth); let cells = days + before, after = cells; while (after > 7) { after -= 7; } cells += 7 - after; let isWeekSelected = false; for (let i = 0, r = 0; i < cells; i++) { let day = new Date(year, month, 1 + (i - before)), isSelected = Datepicker._isDate(this.date) ? Datepicker._compareDates(day, this.date) : false, isToday = Datepicker._compareDates(day, now), hasEvent = opts.events.indexOf(day.toDateString()) !== -1 ? true : false, isEmpty = i < before || i >= days + before, dayNumber = 1 + (i - before), monthNumber = month, yearNumber = year, isStartRange = opts.startRange && Datepicker._compareDates(opts.startRange, day), isEndRange = opts.endRange && Datepicker._compareDates(opts.endRange, day), isInRange = opts.startRange && opts.endRange && opts.startRange < day && day < opts.endRange, isDisabled = (opts.minDate && day < opts.minDate) || (opts.maxDate && day > opts.maxDate) || (opts.disableWeekends && Datepicker._isWeekend(day)) || (opts.disableDayFn && opts.disableDayFn(day)); if (isEmpty) { if (i < before) { dayNumber = daysInPreviousMonth + dayNumber; monthNumber = previousMonth; yearNumber = yearOfPreviousMonth; } else { dayNumber = dayNumber - days; monthNumber = nextMonth; yearNumber = yearOfNextMonth; } } let dayConfig = { day: dayNumber, month: monthNumber, year: yearNumber, hasEvent: hasEvent, isSelected: isSelected, isToday: isToday, isDisabled: isDisabled, isEmpty: isEmpty, isStartRange: isStartRange, isEndRange: isEndRange, isInRange: isInRange, showDaysInNextAndPreviousMonths: opts.showDaysInNextAndPreviousMonths }; row.push(this.renderDay(dayConfig)); if (++r === 7) { data.push(this.renderRow(row, opts.isRTL, isWeekSelected)); row = []; r = 0; isWeekSelected = false; } } return this.renderTable(opts, data, randId); } renderDay(opts) { let arr = []; let ariaSelected = 'false'; if (opts.isEmpty) { if (opts.showDaysInNextAndPreviousMonths) { arr.push('is-outside-current-month'); arr.push('is-selection-disabled'); } else { return '