--- a/js/daterangepicker.js +++ b/js/daterangepicker.js @@ -1,1 +1,609 @@ - +/** +* @version: 1.0.1 +* @author: Dan Grossman http://www.dangrossman.info/ +* @date: 2012-08-20 +* @copyright: Copyright (c) 2012 Dan Grossman. All rights reserved. +* @license: Licensed under Apache License v2.0. See http://www.apache.org/licenses/LICENSE-2.0 +* @website: http://www.improvely.com/ +*/ +!function ($) { + + var DateRangePicker = function (element, options, cb) { + var hasOptions = typeof options == 'object' + var localeObject; + + //state + this.startDate = Date.create('today'); + this.endDate = Date.create('today'); + this.minDate = false; + this.maxDate = false; + this.changed = false; + this.cleared = false; + this.ranges = {}; + this.opens = 'right'; + this.cb = function () { }; + this.format = '{MM}/{dd}/{yyyy}'; + this.separator = ' - '; + this.showWeekNumbers = false; + this.buttonClasses = ['btn-success']; + this.locale = { + applyLabel: 'Apply', + clearLabel:"Clear", + fromLabel: 'From', + toLabel: 'To', + weekLabel: 'W', + customRangeLabel: 'Custom Range', + daysOfWeek: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr','Sa'], + monthNames: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], + firstDay: 0 + }; + + localeObject = this.locale; + + this.leftCalendar = { + month: Date.create('today').set({ day: 1, month: this.startDate.getMonth(), year: this.startDate.getFullYear() }), + calendar: Array() + }; + + this.rightCalendar = { + month: Date.create('today').set({ day: 1, month: this.endDate.getMonth(), year: this.endDate.getFullYear() }), + calendar: Array() + }; + + //element that triggered the date range picker + this.element = $(element); + + if (this.element.hasClass('pull-right')) + this.opens = 'left'; + + if (this.element.is('input')) { + this.element.on({ + click: $.proxy(this.show, this), + focus: $.proxy(this.show, this) + }); + } else { + this.element.on('click', $.proxy(this.show, this)); + } + + if (hasOptions) { + if(typeof options.locale == 'object') { + $.each(localeObject, function (property, value) { + localeObject[property] = options.locale[property] || value; + }); + } + } + + var DRPTemplate = ''; + + this.container = $(DRPTemplate).appendTo('body'); + + if (hasOptions) { + + if (typeof options.format == 'string') + this.format = options.format; + + if (typeof options.separator == 'string') + this.separator = options.separator; + + if (typeof options.startDate == 'string') + this.startDate = Date.create(options.startDate); + + if (typeof options.endDate == 'string') + this.endDate = Date.create(options.endDate); + + if (typeof options.minDate == 'string') + this.minDate = Date.create(options.minDate); + + if (typeof options.maxDate == 'string') + this.maxDate = Date.create(options.maxDate); + + + if (typeof options.startDate == 'object') + this.startDate = options.startDate; + + if (typeof options.endDate == 'object') + this.endDate = options.endDate; + + if (typeof options.minDate == 'object') + this.minDate = options.minDate; + + if (typeof options.maxDate == 'object') + this.maxDate = options.maxDate; + + if (typeof options.ranges == 'object') { + for (var range in options.ranges) { + + var start = options.ranges[range][0]; + var end = options.ranges[range][1]; + + if (typeof start == 'string') + start = Date.create(start); + + if (typeof end == 'string') + end = Date.create(end); + + // If we have a min/max date set, bound this range + // to it, but only if it would otherwise fall + // outside of the min/max. + if (this.minDate && start < this.minDate) + start = this.minDate; + + if (this.maxDate && end > this.maxDate) + end = this.maxDate; + + // If the end of the range is before the minimum (if min is set) OR + // the start of the range is after the max (also if set) don't display this + // range option. + if ((this.minDate && end < this.minDate) || (this.maxDate && start > this.maxDate)) + { + continue; + } + + this.ranges[range] = [start, end]; + } + + var list = ''; + this.container.find('.ranges').prepend(list); + } + + // update day names order to firstDay + if (typeof options.locale == 'object') { + if (typeof options.locale.firstDay == 'number') { + this.locale.firstDay = options.locale.firstDay; + var iterator = options.locale.firstDay; + while (iterator > 0) { + this.locale.daysOfWeek.push(this.locale.daysOfWeek.shift()); + iterator--; + } + } + } + + if (typeof options.opens == 'string') + this.opens = options.opens; + + if (typeof options.showWeekNumbers == 'boolean') { + this.showWeekNumbers = options.showWeekNumbers; + } + + if (typeof options.buttonClasses == 'string') { + this.buttonClasses = [options.buttonClasses]; + } + + if (typeof options.buttonClasses == 'object') { + this.buttonClasses = options.buttonClasses; + } + + } + + //apply CSS classes to buttons + var c = this.container; + $.each(this.buttonClasses, function (idx, val) { + c.find('button').addClass(val); + }); + + if (this.opens == 'right') { + //swap calendar positions + var left = this.container.find('.calendar.left'); + var right = this.container.find('.calendar.right'); + left.removeClass('left').addClass('right'); + right.removeClass('right').addClass('left'); + } + + if (typeof options == 'undefined' || typeof options.ranges == 'undefined') + this.container.find('.calendar').show(); + + if (typeof cb == 'function') + this.cb = cb; + + this.container.addClass('opens' + this.opens); + + //event listeners + this.container.on('mousedown', $.proxy(this.mousedown, this)); + this.container.find('.calendar').on('click', '.prev', $.proxy(this.clickPrev, this)); + this.container.find('.calendar').on('click', '.next', $.proxy(this.clickNext, this)); + this.container.find('.ranges').on('click', 'button.applyBtn', $.proxy(this.clickApply, this)); + this.container.find('.ranges').on('click', 'button.clearBtn', $.proxy(this.clickClear, this)); + + this.container.find('.calendar').on('click', 'td.available', $.proxy(this.clickDate, this)); + this.container.find('.calendar').on('mouseenter', 'td.available', $.proxy(this.enterDate, this)); + this.container.find('.calendar').on('mouseleave', 'td.available', $.proxy(this.updateView, this)); + + this.container.find('.ranges').on('click', 'li', $.proxy(this.clickRange, this)); + this.container.find('.ranges').on('mouseenter', 'li', $.proxy(this.enterRange, this)); + this.container.find('.ranges').on('mouseleave', 'li', $.proxy(this.updateView, this)); + + this.element.on('keyup', $.proxy(this.updateFromControl, this)); + + this.updateView(); + this.updateCalendars(); + + }; + + DateRangePicker.prototype = { + + constructor: DateRangePicker, + + mousedown: function (e) { + e.stopPropagation(); + e.preventDefault(); + }, + + updateView: function () { + this.leftCalendar.month.set({ month: this.startDate.getMonth(), year: this.startDate.getFullYear() }); + this.rightCalendar.month.set({ month: this.endDate.getMonth(), year: this.endDate.getFullYear() }); + + this.container.find('input[name=daterangepicker_start]').val(this.startDate.format(this.format)); + this.container.find('input[name=daterangepicker_end]').val(this.endDate.format(this.format)); + + if (this.startDate.is(this.endDate) || this.startDate.isBefore(this.endDate)) { + this.container.find('button.applyBtn').removeAttr('disabled'); + } else { + this.container.find('button.applyBtn').attr('disabled', 'disabled'); + } + }, + + updateFromControl: function () { + if (!this.element.is('input')) return; + + var dateString = this.element.val().split(this.separator); + var start = Date.create(dateString[0]); + var end = Date.create(dateString[1]); + + if (start == null || end == null) return; + if (end.isBefore(start)) return; + + this.startDate = start; + this.endDate = end; + + this.updateView(); + this.cb(this.startDate, this.endDate); + this.updateCalendars(); + }, + + notify: function () { + if (!this.cleared) { + this.updateView(); + } + + if (this.element.is('input')) { + this.element.val(this.cleared ? '' : this.startDate.format(this.format) + this.separator + this.endDate.format(this.format)); + } + var arg1 = (this.cleared ? null : this.startDate), + arg2 = (this.cleared ? null : this.endDate); + this.cleared = false; + this.cb(arg1,arg2); + }, + + move: function () { + if (this.opens == 'left') { + this.container.css({ + top: this.element.offset().top + this.element.outerHeight(), + right: $(window).width() - this.element.offset().left - this.element.outerWidth(), + left: 'auto' + }); + } else { + this.container.css({ + top: this.element.offset().top + this.element.outerHeight(), + left: this.element.offset().left, + right: 'auto' + }); + } + }, + + show: function (e) { + this.container.show(); + this.move(); + + if (e) { + e.stopPropagation(); + e.preventDefault(); + } + + this.changed = false; + + this.element.trigger('shown',{target:e.target,picker:this}); + + $(document).on('mousedown', $.proxy(this.hide, this)); + }, + + hide: function (e) { + this.container.hide(); + $(document).off('mousedown', this.hide); + + if (this.changed) { + this.changed = false; + this.notify(); + } + }, + + enterRange: function (e) { + var label = e.target.innerHTML; + if (label == this.locale.customRangeLabel) { + this.updateView(); + } else { + var dates = this.ranges[label]; + this.container.find('input[name=daterangepicker_start]').val(dates[0].format(this.format)); + this.container.find('input[name=daterangepicker_end]').val(dates[1].format(this.format)); + } + }, + + clickRange: function (e) { + var label = e.target.innerHTML; + if (label == this.locale.customRangeLabel) { + this.container.find('.calendar').show(); + } else { + var dates = this.ranges[label]; + + this.startDate = dates[0]; + this.endDate = dates[1]; + + this.leftCalendar.month.set({ month: this.startDate.getMonth(), year: this.startDate.getFullYear() }); + this.rightCalendar.month.set({ month: this.endDate.getMonth(), year: this.endDate.getFullYear() }); + this.updateCalendars(); + + this.changed = true; + + this.container.find('.calendar').hide(); + this.hide(); + } + }, + + clickPrev: function (e) { + var cal = $(e.target).parents('.calendar'); + if (cal.hasClass('left')) { + this.leftCalendar.month.addMonths(-1); + } else { + this.rightCalendar.month.addMonths(-1); + } + this.updateCalendars(); + }, + + clickNext: function (e) { + var cal = $(e.target).parents('.calendar'); + if (cal.hasClass('left')) { + this.leftCalendar.month.addMonths(1); + } else { + this.rightCalendar.month.addMonths(1); + } + this.updateCalendars(); + }, + + enterDate: function (e) { + + var title = $(e.target).attr('title'); + var row = title.substr(1, 1); + var col = title.substr(3, 1); + var cal = $(e.target).parents('.calendar'); + + if (cal.hasClass('left')) { + this.container.find('input[name=daterangepicker_start]').val(this.leftCalendar.calendar[row][col].format(this.format)); + } else { + this.container.find('input[name=daterangepicker_end]').val(this.rightCalendar.calendar[row][col].format(this.format)); + } + + }, + + clickDate: function (e) { + var title = $(e.target).attr('title'); + var row = title.substr(1, 1); + var col = title.substr(3, 1); + var cal = $(e.target).parents('.calendar'); + + if (cal.hasClass('left')) { + startDate = this.leftCalendar.calendar[row][col]; + endDate = this.endDate; + this.element.trigger('clicked',{ + dir: 'left', + picker: this + }); + } else { + startDate = this.startDate; + endDate = this.rightCalendar.calendar[row][col]; + this.element.trigger('clicked',{ + dir: 'right', + picker: this + }); + } + + cal.find('td').removeClass('active'); + + if (startDate.is(endDate) || startDate.isBefore(endDate)) { + $(e.target).addClass('active'); + if (!startDate.is(this.startDate) || !endDate.is(this.endDate)) + this.changed = true; + this.startDate = startDate; + this.endDate = endDate; + } + else if (startDate.isAfter(endDate)) { + $(e.target).addClass('active'); + this.changed = true; + this.startDate = startDate; + this.endDate = startDate.clone().addDays(1); + } + + this.leftCalendar.month.set({ month: this.startDate.getMonth(), year: this.startDate.getFullYear() }); + this.rightCalendar.month.set({ month: this.endDate.getMonth(), year: this.endDate.getFullYear() }); + this.updateCalendars(); + }, + + clickApply: function (e) { + this.hide(); + }, + + clickClear: function (e) { + this.changed = true; + this.cleared = true; + this.hide(); + }, + + updateCalendars: function () { + this.leftCalendar.calendar = this.buildCalendar(this.leftCalendar.month.getMonth(), this.leftCalendar.month.getFullYear()); + this.rightCalendar.calendar = this.buildCalendar(this.rightCalendar.month.getMonth(), this.rightCalendar.month.getFullYear()); + this.container.find('.calendar.left').html(this.renderCalendar(this.leftCalendar.calendar, this.startDate, this.minDate, this.maxDate)); + this.container.find('.calendar.right').html(this.renderCalendar(this.rightCalendar.calendar, this.endDate, this.startDate, this.maxDate)); + this.element.trigger('updated',this); + }, + + buildCalendar: function (month, year) { + + var firstDay = Date.create('today').set({ day: 1, month: month, year: year }); + var lastMonth = firstDay.clone().addDays(-1).getMonth(); + var lastYear = firstDay.clone().addDays(-1).getFullYear(); + + var daysInMonth = this.getDaysInMonth(year, month); + var daysInLastMonth = this.getDaysInMonth(lastYear, lastMonth); + + var dayOfWeek = firstDay.getDay(); + + //initialize a 6 rows x 7 columns array for the calendar + var calendar = Array(); + for (var i = 0; i < 6; i++) { + calendar[i] = Array(); + } + + //populate the calendar with date objects + var startDay = daysInLastMonth - dayOfWeek + this.locale.firstDay + 1; + if (startDay > daysInLastMonth) + startDay -= 7; + + if (dayOfWeek == this.locale.firstDay) + startDay = daysInLastMonth - 6; + + var curDate = Date.create('today').set({ day: startDay, month: lastMonth, year: lastYear }); + for (var i = 0, col = 0, row = 0; i < 42; i++, col++, curDate = curDate.clone().addDays(1)) { + if (i > 0 && col % 7 == 0) { + col = 0; + row++; + } + calendar[row][col] = curDate; + } + + return calendar; + + }, + + renderCalendar: function (calendar, selected, minDate, maxDate) { + var html = ''; + html += ''; + html += ''; + + // add empty cell for week number + if (this.showWeekNumbers) + html += ''; + + if (!minDate || minDate < calendar[1][1]) + { + html += ''; + } + else + { + html += ''; + } + html += ''; + if (!maxDate || maxDate > calendar[1][1]) + { + html += ''; + } + else + { + html += ''; + } + + html += ''; + html += ''; + + // add week number label + if (this.showWeekNumbers) + html += ''; + + $.each(this.locale.daysOfWeek, function (index, dayOfWeek) { + html += ''; + }); + + html += ''; + html += ''; + html += ''; + + for (var row = 0; row < 6; row++) { + html += ''; + + // add week number + if (this.showWeekNumbers) + html += ''; + + for (var col = 0; col < 7; col++) { + var cname = 'available '; + cname += (calendar[row][col].getMonth() == calendar[1][1].getMonth()) ? '' : 'off'; + + // Normalise the time so the comparison won't fail + selected.setHours(0,0,0,0); + + if ( (minDate && calendar[row][col] < minDate) || (maxDate && calendar[row][col] > maxDate)) + { + cname = ' off disabled '; + } + else if (calendar[row][col].is(selected)) + { + cname += ' active '; + if (calendar[row][col].is(this.startDate)) { cname += ' start-date '; } + if (calendar[row][col].is(this.endDate)) { cname += ' end-date '; } + } + else if (calendar[row][col] >= this.startDate && calendar[row][col] <= this.endDate) + { + cname += ' in-range '; + if (calendar[row][col].is(this.startDate)) { cname += ' start-date '; } + if (calendar[row][col].is(this.endDate)) { cname += ' end-date '; } + } + + var title = 'r' + row + 'c' + col; + html += ''; + } + html += ''; + } + + html += ''; + html += '
' + this.locale.monthNames[calendar[1][1].getMonth()] + calendar[1][1].format(' {yyyy}') + '
' + this.locale.weekLabel + '' + dayOfWeek + '
' + calendar[row][0].getWeek() + '' + calendar[row][col].getDate() + '
'; + + return html; + + }, + + getDaysInMonth: function (y, m) { + return /8|3|5|10/.test(--m)?30:m==1?(!(y%4)&&y%100)||!(y%400)?29:28:31; + } + + }; + + $.fn.daterangepicker = function (options, cb) { + this.each(function() { + var el = $(this); + if (!el.data('daterangepicker')) + el.data('daterangepicker', new DateRangePicker(el, options, cb)); + }); + return this; + }; + +} (window.jQuery); +