3 * @author: Dan Grossman http://www.dangrossman.info/
5 * @copyright: Copyright (c) 2012-2014 Dan Grossman. All rights reserved.
6 * @license: Licensed under the MIT license. See http://www.opensource.org/licenses/mit-license.php
7 * @website: http://www.improvely.com/
10 (function(root, factory) {
12 if (typeof define === 'function' && define.amd) {
13 define(['moment', 'jquery', 'exports'], function(momentjs, $, exports) {
14 root.daterangepicker = factory(root, exports, momentjs, $);
17 } else if (typeof exports !== 'undefined') {
18 var momentjs = require('moment');
21 jQuery = require('jquery');
23 jQuery = window.jQuery;
24 if (!jQuery) throw new Error('jQuery dependency not found');
27 factory(root, exports, momentjs, jQuery);
29 // Finally, as a browser global.
31 root.daterangepicker = factory(root, {}, root.moment, (root.jQuery || root.Zepto || root.ender || root.$));
34 }(this, function(root, daterangepicker, moment, $) {
36 var DateRangePicker = function (element, options, cb) {
38 // by default, the daterangepicker element is placed at the bottom of HTML body
39 this.parentEl = 'body';
41 //element that triggered the date range picker
42 this.element = $(element);
44 //tracks visible state
45 this.isShowing = false;
47 //create the picker HTML object
48 var DRPTemplate = '<div class="daterangepicker dropdown-menu">' +
49 '<div class="calendar first left"></div>' +
50 '<div class="calendar second right"></div>' +
51 '<div class="ranges">' +
52 '<div class="range_inputs">' +
53 '<div class="daterangepicker_start_input">' +
54 '<label for="daterangepicker_start"></label>' +
55 '<input class="input-mini" type="text" name="daterangepicker_start" value="" />' +
57 '<div class="daterangepicker_end_input">' +
58 '<label for="daterangepicker_end"></label>' +
59 '<input class="input-mini" type="text" name="daterangepicker_end" value="" />' +
61 '<button class="applyBtn" disabled="disabled"></button> ' +
62 '<button class="cancelBtn"></button>' +
68 if (typeof options !== 'object' || options === null)
71 this.parentEl = (typeof options === 'object' && options.parentEl && $(options.parentEl).length) ? $(options.parentEl) : $(this.parentEl);
72 this.container = $(DRPTemplate).appendTo(this.parentEl);
74 this.setOptions(options, cb);
76 //apply CSS classes and labels to buttons
77 var c = this.container;
78 $.each(this.buttonClasses, function (idx, val) {
79 c.find('button').addClass(val);
81 this.container.find('.daterangepicker_start_input label').html(this.locale.fromLabel);
82 this.container.find('.daterangepicker_end_input label').html(this.locale.toLabel);
83 if (this.applyClass.length)
84 this.container.find('.applyBtn').addClass(this.applyClass);
85 if (this.cancelClass.length)
86 this.container.find('.cancelBtn').addClass(this.cancelClass);
87 this.container.find('.applyBtn').html(this.locale.applyLabel);
88 this.container.find('.cancelBtn').html(this.locale.cancelLabel);
92 this.container.find('.calendar')
93 .on('click.daterangepicker', '.prev', $.proxy(this.clickPrev, this))
94 .on('click.daterangepicker', '.next', $.proxy(this.clickNext, this))
95 .on('click.daterangepicker', 'td.available', $.proxy(this.clickDate, this))
96 .on('mouseenter.daterangepicker', 'td.available', $.proxy(this.hoverDate, this))
97 .on('mouseleave.daterangepicker', 'td.available', $.proxy(this.updateFormInputs, this))
98 .on('change.daterangepicker', 'select.yearselect', $.proxy(this.updateMonthYear, this))
99 .on('change.daterangepicker', 'select.monthselect', $.proxy(this.updateMonthYear, this))
100 .on('change.daterangepicker', 'select.hourselect,select.minuteselect,select.secondselect,select.ampmselect', $.proxy(this.updateTime, this));
102 this.container.find('.ranges')
103 .on('click.daterangepicker', 'button.applyBtn', $.proxy(this.clickApply, this))
104 .on('click.daterangepicker', 'button.cancelBtn', $.proxy(this.clickCancel, this))
105 .on('click.daterangepicker', '.daterangepicker_start_input,.daterangepicker_end_input', $.proxy(this.showCalendars, this))
106 .on('change.daterangepicker', '.daterangepicker_start_input,.daterangepicker_end_input', $.proxy(this.inputsChanged, this))
107 .on('keydown.daterangepicker', '.daterangepicker_start_input,.daterangepicker_end_input', $.proxy(this.inputsKeydown, this))
108 .on('click.daterangepicker', 'li', $.proxy(this.clickRange, this))
109 .on('mouseenter.daterangepicker', 'li', $.proxy(this.enterRange, this))
110 .on('mouseleave.daterangepicker', 'li', $.proxy(this.updateFormInputs, this));
112 if (this.element.is('input')) {
114 'click.daterangepicker': $.proxy(this.show, this),
115 'focus.daterangepicker': $.proxy(this.show, this),
116 'keyup.daterangepicker': $.proxy(this.updateFromControl, this)
119 this.element.on('click.daterangepicker', $.proxy(this.toggle, this));
124 DateRangePicker.prototype = {
126 constructor: DateRangePicker,
128 setOptions: function(options, callback) {
130 this.startDate = moment().startOf('day');
131 this.endDate = moment().endOf('day');
132 this.timeZone = moment().zone();
133 this.minDate = false;
134 this.maxDate = false;
135 this.dateLimit = false;
137 this.showDropdowns = false;
138 this.showWeekNumbers = false;
139 this.timePicker = false;
140 this.timePickerSeconds = false;
141 this.timePickerIncrement = 30;
142 this.timePicker12Hour = true;
143 this.singleDatePicker = false;
146 this.opens = 'right';
147 if (this.element.hasClass('pull-right'))
150 this.buttonClasses = ['btn', 'btn-small btn-sm'];
151 this.applyClass = 'btn-success';
152 this.cancelClass = 'btn-default';
154 this.format = 'MM/DD/YYYY';
155 this.separator = ' - ';
159 cancelLabel: 'Cancel',
163 customRangeLabel: 'Custom Range',
164 daysOfWeek: moment.weekdaysMin(),
165 monthNames: moment.monthsShort(),
166 firstDay: moment.localeData()._week.dow
169 this.cb = function () { };
171 if (typeof options.format === 'string')
172 this.format = options.format;
174 if (typeof options.separator === 'string')
175 this.separator = options.separator;
177 if (typeof options.startDate === 'string')
178 this.startDate = moment(options.startDate, this.format);
180 if (typeof options.endDate === 'string')
181 this.endDate = moment(options.endDate, this.format);
183 if (typeof options.minDate === 'string')
184 this.minDate = moment(options.minDate, this.format);
186 if (typeof options.maxDate === 'string')
187 this.maxDate = moment(options.maxDate, this.format);
189 if (typeof options.startDate === 'object')
190 this.startDate = moment(options.startDate);
192 if (typeof options.endDate === 'object')
193 this.endDate = moment(options.endDate);
195 if (typeof options.minDate === 'object')
196 this.minDate = moment(options.minDate);
198 if (typeof options.maxDate === 'object')
199 this.maxDate = moment(options.maxDate);
201 if (typeof options.applyClass === 'string')
202 this.applyClass = options.applyClass;
204 if (typeof options.cancelClass === 'string')
205 this.cancelClass = options.cancelClass;
207 if (typeof options.dateLimit === 'object')
208 this.dateLimit = options.dateLimit;
210 if (typeof options.locale === 'object') {
212 if (typeof options.locale.daysOfWeek === 'object') {
213 // Create a copy of daysOfWeek to avoid modification of original
214 // options object for reusability in multiple daterangepicker instances
215 this.locale.daysOfWeek = options.locale.daysOfWeek.slice();
218 if (typeof options.locale.monthNames === 'object') {
219 this.locale.monthNames = options.locale.monthNames.slice();
222 if (typeof options.locale.firstDay === 'number') {
223 this.locale.firstDay = options.locale.firstDay;
226 if (typeof options.locale.applyLabel === 'string') {
227 this.locale.applyLabel = options.locale.applyLabel;
230 if (typeof options.locale.cancelLabel === 'string') {
231 this.locale.cancelLabel = options.locale.cancelLabel;
234 if (typeof options.locale.fromLabel === 'string') {
235 this.locale.fromLabel = options.locale.fromLabel;
238 if (typeof options.locale.toLabel === 'string') {
239 this.locale.toLabel = options.locale.toLabel;
242 if (typeof options.locale.weekLabel === 'string') {
243 this.locale.weekLabel = options.locale.weekLabel;
246 if (typeof options.locale.customRangeLabel === 'string') {
247 this.locale.customRangeLabel = options.locale.customRangeLabel;
251 if (typeof options.opens === 'string')
252 this.opens = options.opens;
254 if (typeof options.showWeekNumbers === 'boolean') {
255 this.showWeekNumbers = options.showWeekNumbers;
258 if (typeof options.buttonClasses === 'string') {
259 this.buttonClasses = [options.buttonClasses];
262 if (typeof options.buttonClasses === 'object') {
263 this.buttonClasses = options.buttonClasses;
266 if (typeof options.showDropdowns === 'boolean') {
267 this.showDropdowns = options.showDropdowns;
270 if (typeof options.singleDatePicker === 'boolean') {
271 this.singleDatePicker = options.singleDatePicker;
272 if (this.singleDatePicker) {
273 this.endDate = this.startDate.clone();
277 if (typeof options.timePicker === 'boolean') {
278 this.timePicker = options.timePicker;
281 if (typeof options.timePickerSeconds === 'boolean') {
282 this.timePickerSeconds = options.timePickerSeconds;
285 if (typeof options.timePickerIncrement === 'number') {
286 this.timePickerIncrement = options.timePickerIncrement;
289 if (typeof options.timePicker12Hour === 'boolean') {
290 this.timePicker12Hour = options.timePicker12Hour;
293 // update day names order to firstDay
294 if (this.locale.firstDay != 0) {
295 var iterator = this.locale.firstDay;
296 while (iterator > 0) {
297 this.locale.daysOfWeek.push(this.locale.daysOfWeek.shift());
302 var start, end, range;
304 //if no start/end dates set, check if an input element contains initial values
305 if (typeof options.startDate === 'undefined' && typeof options.endDate === 'undefined') {
306 if ($(this.element).is('input[type=text]')) {
307 var val = $(this.element).val(),
308 split = val.split(this.separator);
312 if (split.length == 2) {
313 start = moment(split[0], this.format);
314 end = moment(split[1], this.format);
315 } else if (this.singleDatePicker && val !== "") {
316 start = moment(val, this.format);
317 end = moment(val, this.format);
319 if (start !== null && end !== null) {
320 this.startDate = start;
326 // bind the time zone used to build the calendar to either the timeZone passed in through the options or the zone of the startDate (which will be the local time zone by default)
327 if (typeof options.timeZone === 'string' || typeof options.timeZone === 'number') {
328 this.timeZone = options.timeZone;
329 this.startDate.zone(this.timeZone);
330 this.endDate.zone(this.timeZone);
332 this.timeZone = moment(this.startDate).zone();
335 if (typeof options.ranges === 'object') {
336 for (range in options.ranges) {
338 if (typeof options.ranges[range][0] === 'string')
339 start = moment(options.ranges[range][0], this.format);
341 start = moment(options.ranges[range][0]);
343 if (typeof options.ranges[range][1] === 'string')
344 end = moment(options.ranges[range][1], this.format);
346 end = moment(options.ranges[range][1]);
348 // If we have a min/max date set, bound this range
349 // to it, but only if it would otherwise fall
350 // outside of the min/max.
351 if (this.minDate && start.isBefore(this.minDate))
352 start = moment(this.minDate);
354 if (this.maxDate && end.isAfter(this.maxDate))
355 end = moment(this.maxDate);
357 // If the end of the range is before the minimum (if min is set) OR
358 // the start of the range is after the max (also if set) don't display this
360 if ((this.minDate && end.isBefore(this.minDate)) || (this.maxDate && start.isAfter(this.maxDate))) {
364 this.ranges[range] = [start, end];
368 for (range in this.ranges) {
369 list += '<li>' + range + '</li>';
371 list += '<li>' + this.locale.customRangeLabel + '</li>';
373 this.container.find('.ranges ul').remove();
374 this.container.find('.ranges').prepend(list);
377 if (typeof callback === 'function') {
381 if (!this.timePicker) {
382 this.startDate = this.startDate.startOf('day');
383 this.endDate = this.endDate.endOf('day');
386 if (this.singleDatePicker) {
387 this.opens = 'right';
388 this.container.addClass('single');
389 this.container.find('.calendar.right').show();
390 this.container.find('.calendar.left').hide();
391 if (!this.timePicker) {
392 this.container.find('.ranges').hide();
394 this.container.find('.ranges .daterangepicker_start_input, .ranges .daterangepicker_end_input').hide();
396 if (!this.container.find('.calendar.right').hasClass('single'))
397 this.container.find('.calendar.right').addClass('single');
399 this.container.removeClass('single');
400 this.container.find('.calendar.right').removeClass('single');
401 this.container.find('.ranges').show();
404 this.oldStartDate = this.startDate.clone();
405 this.oldEndDate = this.endDate.clone();
406 this.oldChosenLabel = this.chosenLabel;
408 this.leftCalendar = {
409 month: moment([this.startDate.year(), this.startDate.month(), 1, this.startDate.hour(), this.startDate.minute(), this.startDate.second()]),
413 this.rightCalendar = {
414 month: moment([this.endDate.year(), this.endDate.month(), 1, this.endDate.hour(), this.endDate.minute(), this.endDate.second()]),
418 if (this.opens == 'right' || this.opens == 'center') {
419 //swap calendar positions
420 var first = this.container.find('.calendar.first');
421 var second = this.container.find('.calendar.second');
423 if (second.hasClass('single')) {
424 second.removeClass('single');
425 first.addClass('single');
428 first.removeClass('left').addClass('right');
429 second.removeClass('right').addClass('left');
431 if (this.singleDatePicker) {
437 if (typeof options.ranges === 'undefined' && !this.singleDatePicker) {
438 this.container.addClass('show-calendar');
441 this.container.addClass('opens' + this.opens);
444 this.updateCalendars();
448 setStartDate: function(startDate) {
449 if (typeof startDate === 'string')
450 this.startDate = moment(startDate, this.format).zone(this.timeZone);
452 if (typeof startDate === 'object')
453 this.startDate = moment(startDate);
455 if (!this.timePicker)
456 this.startDate = this.startDate.startOf('day');
458 this.oldStartDate = this.startDate.clone();
461 this.updateCalendars();
462 this.updateInputText();
465 setEndDate: function(endDate) {
466 if (typeof endDate === 'string')
467 this.endDate = moment(endDate, this.format).zone(this.timeZone);
469 if (typeof endDate === 'object')
470 this.endDate = moment(endDate);
472 if (!this.timePicker)
473 this.endDate = this.endDate.endOf('day');
475 this.oldEndDate = this.endDate.clone();
478 this.updateCalendars();
479 this.updateInputText();
482 updateView: function () {
483 this.leftCalendar.month.month(this.startDate.month()).year(this.startDate.year()).hour(this.startDate.hour()).minute(this.startDate.minute());
484 this.rightCalendar.month.month(this.endDate.month()).year(this.endDate.year()).hour(this.endDate.hour()).minute(this.endDate.minute());
485 this.updateFormInputs();
488 updateFormInputs: function () {
489 this.container.find('input[name=daterangepicker_start]').val(this.startDate.format(this.format));
490 this.container.find('input[name=daterangepicker_end]').val(this.endDate.format(this.format));
492 if (this.startDate.isSame(this.endDate) || this.startDate.isBefore(this.endDate)) {
493 this.container.find('button.applyBtn').removeAttr('disabled');
495 this.container.find('button.applyBtn').attr('disabled', 'disabled');
499 updateFromControl: function () {
500 if (!this.element.is('input')) return;
501 if (!this.element.val().length) return;
503 var dateString = this.element.val().split(this.separator),
507 if(dateString.length === 2) {
508 start = moment(dateString[0], this.format).zone(this.timeZone);
509 end = moment(dateString[1], this.format).zone(this.timeZone);
512 if (this.singleDatePicker || start === null || end === null) {
513 start = moment(this.element.val(), this.format).zone(this.timeZone);
517 if (end.isBefore(start)) return;
519 this.oldStartDate = this.startDate.clone();
520 this.oldEndDate = this.endDate.clone();
522 this.startDate = start;
525 if (!this.startDate.isSame(this.oldStartDate) || !this.endDate.isSame(this.oldEndDate))
528 this.updateCalendars();
531 notify: function () {
533 this.cb(this.startDate, this.endDate, this.chosenLabel);
537 var parentOffset = { top: 0, left: 0 };
538 var parentRightEdge = $(window).width();
539 if (!this.parentEl.is('body')) {
541 top: this.parentEl.offset().top - this.parentEl.scrollTop(),
542 left: this.parentEl.offset().left - this.parentEl.scrollLeft()
544 parentRightEdge = this.parentEl[0].clientWidth + this.parentEl.offset().left;
547 if (this.opens == 'left') {
549 top: this.element.offset().top + this.element.outerHeight() - parentOffset.top,
550 right: parentRightEdge - this.element.offset().left - this.element.outerWidth(),
553 if (this.container.offset().left < 0) {
559 } else if (this.opens == 'center') {
561 top: this.element.offset().top + this.element.outerHeight() - parentOffset.top,
562 left: this.element.offset().left - parentOffset.left + this.element.outerWidth() / 2
563 - this.container.outerWidth() / 2,
566 if (this.container.offset().left < 0) {
574 top: this.element.offset().top + this.element.outerHeight() - parentOffset.top,
575 left: this.element.offset().left - parentOffset.left,
578 if (this.container.offset().left + this.container.outerWidth() > $(window).width()) {
587 toggle: function (e) {
588 if (this.element.hasClass('active')) {
596 if (this.isShowing) return;
598 this.element.addClass('active');
599 this.container.show();
602 // Create a click proxy that is private to this instance of datepicker, for unbinding
603 this._outsideClickProxy = $.proxy(function (e) { this.outsideClick(e); }, this);
604 // Bind global datepicker mousedown for hiding and
606 .on('mousedown.daterangepicker', this._outsideClickProxy)
607 // also support mobile devices
608 .on('touchend.daterangepicker', this._outsideClickProxy)
609 // also explicitly play nice with Bootstrap dropdowns, which stopPropagation when clicking them
610 .on('click.daterangepicker', '[data-toggle=dropdown]', this._outsideClickProxy)
611 // and also close when focus changes to outside the picker (eg. tabbing between controls)
612 .on('focusin.daterangepicker', this._outsideClickProxy);
614 this.isShowing = true;
615 this.element.trigger('show.daterangepicker', this);
618 outsideClick: function (e) {
619 var target = $(e.target);
620 // if the page is clicked anywhere except within the daterangerpicker/button
621 // itself then call this.hide()
623 // ie modal dialog fix
624 e.type == "focusin" ||
625 target.closest(this.element).length ||
626 target.closest(this.container).length ||
627 target.closest('.calendar-date').length
633 if (!this.isShowing) return;
636 .off('.daterangepicker');
638 this.element.removeClass('active');
639 this.container.hide();
641 if (!this.startDate.isSame(this.oldStartDate) || !this.endDate.isSame(this.oldEndDate))
644 this.oldStartDate = this.startDate.clone();
645 this.oldEndDate = this.endDate.clone();
647 this.isShowing = false;
648 this.element.trigger('hide.daterangepicker', this);
651 enterRange: function (e) {
652 // mouse pointer has entered a range label
653 var label = e.target.innerHTML;
654 if (label == this.locale.customRangeLabel) {
657 var dates = this.ranges[label];
658 this.container.find('input[name=daterangepicker_start]').val(dates[0].format(this.format));
659 this.container.find('input[name=daterangepicker_end]').val(dates[1].format(this.format));
663 showCalendars: function() {
664 this.container.addClass('show-calendar');
666 this.element.trigger('showCalendar.daterangepicker', this);
669 hideCalendars: function() {
670 this.container.removeClass('show-calendar');
671 this.element.trigger('hideCalendar.daterangepicker', this);
674 // when a date is typed into the start to end date textboxes
675 inputsChanged: function (e) {
676 var el = $(e.target);
677 var date = moment(el.val(), this.format);
678 if (!date.isValid()) return;
680 var startDate, endDate;
681 if (el.attr('name') === 'daterangepicker_start') {
683 endDate = this.endDate;
685 startDate = this.startDate;
688 this.setCustomDates(startDate, endDate);
691 inputsKeydown: function(e) {
692 if (e.keyCode === 13) {
693 this.inputsChanged(e);
698 updateInputText: function() {
699 if (this.element.is('input') && !this.singleDatePicker) {
700 this.element.val(this.startDate.format(this.format) + this.separator + this.endDate.format(this.format));
701 } else if (this.element.is('input')) {
702 this.element.val(this.endDate.format(this.format));
706 clickRange: function (e) {
707 var label = e.target.innerHTML;
708 this.chosenLabel = label;
709 if (label == this.locale.customRangeLabel) {
710 this.showCalendars();
712 var dates = this.ranges[label];
714 this.startDate = dates[0];
715 this.endDate = dates[1];
717 if (!this.timePicker) {
718 this.startDate.startOf('day');
719 this.endDate.endOf('day');
722 this.leftCalendar.month.month(this.startDate.month()).year(this.startDate.year()).hour(this.startDate.hour()).minute(this.startDate.minute());
723 this.rightCalendar.month.month(this.endDate.month()).year(this.endDate.year()).hour(this.endDate.hour()).minute(this.endDate.minute());
724 this.updateCalendars();
726 this.updateInputText();
728 this.hideCalendars();
730 this.element.trigger('apply.daterangepicker', this);
734 clickPrev: function (e) {
735 var cal = $(e.target).parents('.calendar');
736 if (cal.hasClass('left')) {
737 this.leftCalendar.month.subtract(1, 'month');
739 this.rightCalendar.month.subtract(1, 'month');
741 this.updateCalendars();
744 clickNext: function (e) {
745 var cal = $(e.target).parents('.calendar');
746 if (cal.hasClass('left')) {
747 this.leftCalendar.month.add(1, 'month');
749 this.rightCalendar.month.add(1, 'month');
751 this.updateCalendars();
754 hoverDate: function (e) {
755 var title = $(e.target).attr('data-title');
756 var row = title.substr(1, 1);
757 var col = title.substr(3, 1);
758 var cal = $(e.target).parents('.calendar');
760 if (cal.hasClass('left')) {
761 this.container.find('input[name=daterangepicker_start]').val(this.leftCalendar.calendar[row][col].format(this.format));
763 this.container.find('input[name=daterangepicker_end]').val(this.rightCalendar.calendar[row][col].format(this.format));
767 setCustomDates: function(startDate, endDate) {
768 this.chosenLabel = this.locale.customRangeLabel;
769 if (startDate.isAfter(endDate)) {
770 var difference = this.endDate.diff(this.startDate);
771 endDate = moment(startDate).add(difference, 'ms');
773 this.startDate = startDate;
774 this.endDate = endDate;
777 this.updateCalendars();
780 clickDate: function (e) {
781 var title = $(e.target).attr('data-title');
782 var row = title.substr(1, 1);
783 var col = title.substr(3, 1);
784 var cal = $(e.target).parents('.calendar');
786 var startDate, endDate;
787 if (cal.hasClass('left')) {
788 startDate = this.leftCalendar.calendar[row][col];
789 endDate = this.endDate;
790 if (typeof this.dateLimit === 'object') {
791 var maxDate = moment(startDate).add(this.dateLimit).startOf('day');
792 if (endDate.isAfter(maxDate)) {
797 startDate = this.startDate;
798 endDate = this.rightCalendar.calendar[row][col];
799 if (typeof this.dateLimit === 'object') {
800 var minDate = moment(endDate).subtract(this.dateLimit).startOf('day');
801 if (startDate.isBefore(minDate)) {
807 if (this.singleDatePicker && cal.hasClass('left')) {
808 endDate = startDate.clone();
809 } else if (this.singleDatePicker && cal.hasClass('right')) {
810 startDate = endDate.clone();
813 cal.find('td').removeClass('active');
815 $(e.target).addClass('active');
817 this.setCustomDates(startDate, endDate);
819 if (!this.timePicker)
820 endDate.endOf('day');
822 if (this.singleDatePicker && !this.timePicker)
826 clickApply: function (e) {
827 this.updateInputText();
829 this.element.trigger('apply.daterangepicker', this);
832 clickCancel: function (e) {
833 this.startDate = this.oldStartDate;
834 this.endDate = this.oldEndDate;
835 this.chosenLabel = this.oldChosenLabel;
837 this.updateCalendars();
839 this.element.trigger('cancel.daterangepicker', this);
842 updateMonthYear: function (e) {
843 var isLeft = $(e.target).closest('.calendar').hasClass('left'),
844 leftOrRight = isLeft ? 'left' : 'right',
845 cal = this.container.find('.calendar.'+leftOrRight);
847 // Month must be Number for new moment versions
848 var month = parseInt(cal.find('.monthselect').val(), 10);
849 var year = cal.find('.yearselect').val();
851 this[leftOrRight+'Calendar'].month.month(month).year(year);
852 this.updateCalendars();
855 updateTime: function(e) {
857 var cal = $(e.target).closest('.calendar'),
858 isLeft = cal.hasClass('left');
860 var hour = parseInt(cal.find('.hourselect').val(), 10);
861 var minute = parseInt(cal.find('.minuteselect').val(), 10);
864 if (this.timePickerSeconds) {
865 second = parseInt(cal.find('.secondselect').val(), 10);
868 if (this.timePicker12Hour) {
869 var ampm = cal.find('.ampmselect').val();
870 if (ampm === 'PM' && hour < 12)
872 if (ampm === 'AM' && hour === 12)
877 var start = this.startDate.clone();
879 start.minute(minute);
880 start.second(second);
881 this.startDate = start;
882 this.leftCalendar.month.hour(hour).minute(minute).second(second);
883 if (this.singleDatePicker)
884 this.endDate = start.clone();
886 var end = this.endDate.clone();
891 if (this.singleDatePicker)
892 this.startDate = end.clone();
893 this.rightCalendar.month.hour(hour).minute(minute).second(second);
897 this.updateCalendars();
900 updateCalendars: function () {
901 this.leftCalendar.calendar = this.buildCalendar(this.leftCalendar.month.month(), this.leftCalendar.month.year(), this.leftCalendar.month.hour(), this.leftCalendar.month.minute(), this.leftCalendar.month.second(), 'left');
902 this.rightCalendar.calendar = this.buildCalendar(this.rightCalendar.month.month(), this.rightCalendar.month.year(), this.rightCalendar.month.hour(), this.rightCalendar.month.minute(), this.rightCalendar.month.second(), 'right');
903 this.container.find('.calendar.left').empty().html(this.renderCalendar(this.leftCalendar.calendar, this.startDate, this.minDate, this.maxDate, 'left'));
904 this.container.find('.calendar.right').empty().html(this.renderCalendar(this.rightCalendar.calendar, this.endDate, this.singleDatePicker ? this.minDate : this.startDate, this.maxDate, 'right'));
906 this.container.find('.ranges li').removeClass('active');
907 var customRange = true;
909 for (var range in this.ranges) {
910 if (this.timePicker) {
911 if (this.startDate.isSame(this.ranges[range][0]) && this.endDate.isSame(this.ranges[range][1])) {
913 this.chosenLabel = this.container.find('.ranges li:eq(' + i + ')')
914 .addClass('active').html();
917 //ignore times when comparing dates if time picker is not enabled
918 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')) {
920 this.chosenLabel = this.container.find('.ranges li:eq(' + i + ')')
921 .addClass('active').html();
927 this.chosenLabel = this.container.find('.ranges li:last').addClass('active').html();
928 this.showCalendars();
932 buildCalendar: function (month, year, hour, minute, second, side) {
933 var daysInMonth = moment([year, month]).daysInMonth();
934 var firstDay = moment([year, month, 1]);
935 var lastDay = moment([year, month, daysInMonth]);
936 var lastMonth = moment(firstDay).subtract(1, 'month').month();
937 var lastYear = moment(firstDay).subtract(1, 'month').year();
939 var daysInLastMonth = moment([lastYear, lastMonth]).daysInMonth();
941 var dayOfWeek = firstDay.day();
945 //initialize a 6 rows x 7 columns array for the calendar
947 calendar.firstDay = firstDay;
948 calendar.lastDay = lastDay;
950 for (i = 0; i < 6; i++) {
954 //populate the calendar with date objects
955 var startDay = daysInLastMonth - dayOfWeek + this.locale.firstDay + 1;
956 if (startDay > daysInLastMonth)
959 if (dayOfWeek == this.locale.firstDay)
960 startDay = daysInLastMonth - 6;
962 var curDate = moment([lastYear, lastMonth, startDay, 12, minute, second]).zone(this.timeZone);
965 for (i = 0, col = 0, row = 0; i < 42; i++, col++, curDate = moment(curDate).add(24, 'hour')) {
966 if (i > 0 && col % 7 === 0) {
970 calendar[row][col] = curDate.clone().hour(hour);
973 if (this.minDate && calendar[row][col].format('YYYY-MM-DD') == this.minDate.format('YYYY-MM-DD') && calendar[row][col].isBefore(this.minDate) && side == 'left') {
974 calendar[row][col] = this.minDate.clone();
977 if (this.maxDate && calendar[row][col].format('YYYY-MM-DD') == this.maxDate.format('YYYY-MM-DD') && calendar[row][col].isAfter(this.maxDate) && side == 'right') {
978 calendar[row][col] = this.maxDate.clone();
986 renderDropdowns: function (selected, minDate, maxDate) {
987 var currentMonth = selected.month();
988 var currentYear = selected.year();
989 var maxYear = (maxDate && maxDate.year()) || (currentYear + 5);
990 var minYear = (minDate && minDate.year()) || (currentYear - 50);
992 var monthHtml = '<select class="monthselect">';
993 var inMinYear = currentYear == minYear;
994 var inMaxYear = currentYear == maxYear;
996 for (var m = 0; m < 12; m++) {
997 if ((!inMinYear || m >= minDate.month()) && (!inMaxYear || m <= maxDate.month())) {
998 monthHtml += "<option value='" + m + "'" +
999 (m === currentMonth ? " selected='selected'" : "") +
1000 ">" + this.locale.monthNames[m] + "</option>";
1003 monthHtml += "</select>";
1005 var yearHtml = '<select class="yearselect">';
1007 for (var y = minYear; y <= maxYear; y++) {
1008 yearHtml += '<option value="' + y + '"' +
1009 (y === currentYear ? ' selected="selected"' : '') +
1010 '>' + y + '</option>';
1013 yearHtml += '</select>';
1015 return monthHtml + yearHtml;
1018 renderCalendar: function (calendar, selected, minDate, maxDate, side) {
1020 var html = '<div class="calendar-date">';
1021 html += '<table class="table-condensed">';
1025 // add empty cell for week number
1026 if (this.showWeekNumbers)
1027 html += '<th></th>';
1029 if (!minDate || minDate.isBefore(calendar.firstDay)) {
1030 html += '<th class="prev available"><i class="fa fa-arrow-left icon-arrow-left glyphicon glyphicon-arrow-left"></i></th>';
1032 html += '<th></th>';
1035 var dateHtml = this.locale.monthNames[calendar[1][1].month()] + calendar[1][1].format(" YYYY");
1037 if (this.showDropdowns) {
1038 dateHtml = this.renderDropdowns(calendar[1][1], minDate, maxDate);
1041 html += '<th colspan="5" class="month">' + dateHtml + '</th>';
1042 if (!maxDate || maxDate.isAfter(calendar.lastDay)) {
1043 html += '<th class="next available"><i class="fa fa-arrow-right icon-arrow-right glyphicon glyphicon-arrow-right"></i></th>';
1045 html += '<th></th>';
1051 // add week number label
1052 if (this.showWeekNumbers)
1053 html += '<th class="week">' + this.locale.weekLabel + '</th>';
1055 $.each(this.locale.daysOfWeek, function (index, dayOfWeek) {
1056 html += '<th>' + dayOfWeek + '</th>';
1063 for (var row = 0; row < 6; row++) {
1067 if (this.showWeekNumbers)
1068 html += '<td class="week">' + calendar[row][0].week() + '</td>';
1070 for (var col = 0; col < 7; col++) {
1071 var cname = 'available ';
1072 cname += (calendar[row][col].month() == calendar[1][1].month()) ? '' : 'off';
1074 if ((minDate && calendar[row][col].isBefore(minDate, 'day')) || (maxDate && calendar[row][col].isAfter(maxDate, 'day'))) {
1075 cname = ' off disabled ';
1076 } else if (calendar[row][col].format('YYYY-MM-DD') == selected.format('YYYY-MM-DD')) {
1077 cname += ' active ';
1078 if (calendar[row][col].format('YYYY-MM-DD') == this.startDate.format('YYYY-MM-DD')) {
1079 cname += ' start-date ';
1081 if (calendar[row][col].format('YYYY-MM-DD') == this.endDate.format('YYYY-MM-DD')) {
1082 cname += ' end-date ';
1084 } else if (calendar[row][col] >= this.startDate && calendar[row][col] <= this.endDate) {
1085 cname += ' in-range ';
1086 if (calendar[row][col].isSame(this.startDate)) { cname += ' start-date '; }
1087 if (calendar[row][col].isSame(this.endDate)) { cname += ' end-date '; }
1090 var title = 'r' + row + 'c' + col;
1091 html += '<td class="' + cname.replace(/\s+/g, ' ').replace(/^\s?(.*?)\s?$/, '$1') + '" data-title="' + title + '">' + calendar[row][col].date() + '</td>';
1101 if (this.timePicker) {
1103 html += '<div class="calendar-time">';
1104 html += '<select class="hourselect">';
1106 // Disallow selections before the minDate or after the maxDate
1110 if (minDate && (side == 'left' || this.singleDatePicker) && selected.format('YYYY-MM-DD') == minDate.format('YYYY-MM-DD')) {
1111 min_hour = minDate.hour();
1112 if (selected.hour() < min_hour)
1113 selected.hour(min_hour);
1114 if (this.timePicker12Hour && min_hour >= 12 && selected.hour() >= 12)
1116 if (this.timePicker12Hour && min_hour == 12)
1120 if (maxDate && (side == 'right' || this.singleDatePicker) && selected.format('YYYY-MM-DD') == maxDate.format('YYYY-MM-DD')) {
1121 max_hour = maxDate.hour();
1122 if (selected.hour() > max_hour)
1123 selected.hour(max_hour);
1124 if (this.timePicker12Hour && max_hour >= 12 && selected.hour() >= 12)
1130 var selected_hour = selected.hour();
1131 if (this.timePicker12Hour) {
1134 if (selected_hour >= 12)
1135 selected_hour -= 12;
1136 if (selected_hour === 0)
1140 for (i = start; i <= end; i++) {
1142 if (i == selected_hour) {
1143 html += '<option value="' + i + '" selected="selected">' + i + '</option>';
1144 } else if (i < min_hour || i > max_hour) {
1145 html += '<option value="' + i + '" disabled="disabled" class="disabled">' + i + '</option>';
1147 html += '<option value="' + i + '">' + i + '</option>';
1151 html += '</select> : ';
1153 html += '<select class="minuteselect">';
1155 // Disallow selections before the minDate or after the maxDate
1157 var max_minute = 59;
1159 if (minDate && (side == 'left' || this.singleDatePicker) && selected.format('YYYY-MM-DD h A') == minDate.format('YYYY-MM-DD h A')) {
1160 min_minute = minDate.minute();
1161 if (selected.minute() < min_minute)
1162 selected.minute(min_minute);
1165 if (maxDate && (side == 'right' || this.singleDatePicker) && selected.format('YYYY-MM-DD h A') == maxDate.format('YYYY-MM-DD h A')) {
1166 max_minute = maxDate.minute();
1167 if (selected.minute() > max_minute)
1168 selected.minute(max_minute);
1171 for (i = 0; i < 60; i += this.timePickerIncrement) {
1175 if (i == selected.minute()) {
1176 html += '<option value="' + i + '" selected="selected">' + num + '</option>';
1177 } else if (i < min_minute || i > max_minute) {
1178 html += '<option value="' + i + '" disabled="disabled" class="disabled">' + num + '</option>';
1180 html += '<option value="' + i + '">' + num + '</option>';
1184 html += '</select> ';
1186 if (this.timePickerSeconds) {
1187 html += ': <select class="secondselect">';
1189 for (i = 0; i < 60; i += this.timePickerIncrement) {
1193 if (i == selected.second()) {
1194 html += '<option value="' + i + '" selected="selected">' + num + '</option>';
1196 html += '<option value="' + i + '">' + num + '</option>';
1200 html += '</select>';
1203 if (this.timePicker12Hour) {
1204 html += '<select class="ampmselect">';
1206 // Disallow selection before the minDate or after the maxDate
1210 if (minDate && (side == 'left' || this.singleDatePicker) && selected.format('YYYY-MM-DD') == minDate.format('YYYY-MM-DD') && minDate.hour() >= 12) {
1211 am_html = ' disabled="disabled" class="disabled"';
1214 if (maxDate && (side == 'right' || this.singleDatePicker) && selected.format('YYYY-MM-DD') == maxDate.format('YYYY-MM-DD') && maxDate.hour() < 12) {
1215 pm_html = ' disabled="disabled" class="disabled"';
1218 if (selected.hour() >= 12) {
1219 html += '<option value="AM"' + am_html + '>AM</option><option value="PM" selected="selected"' + pm_html + '>PM</option>';
1221 html += '<option value="AM" selected="selected"' + am_html + '>AM</option><option value="PM"' + pm_html + '>PM</option>';
1223 html += '</select>';
1234 remove: function() {
1236 this.container.remove();
1237 this.element.off('.daterangepicker');
1238 this.element.removeData('daterangepicker');
1244 $.fn.daterangepicker = function (options, cb) {
1245 this.each(function () {
1247 if (el.data('daterangepicker'))
1248 el.data('daterangepicker').remove();
1249 el.data('daterangepicker', new DateRangePicker(el, options, cb));