1 /* =========================================================
2 * bootstrap-datepicker.js
3 * Repo: https://github.com/eternicode/bootstrap-datepicker/
4 * Demo: http://eternicode.github.io/bootstrap-datepicker/
5 * Docs: http://bootstrap-datepicker.readthedocs.org/
6 * Forked from http://www.eyecon.ro/bootstrap-datepicker
7 * =========================================================
8 * Started by Stefan Petre; improvements by Andrew Rowls + contributors
10 * Licensed under the Apache License, Version 2.0 (the "License");
11 * you may not use this file except in compliance with the License.
12 * You may obtain a copy of the License at
14 * http://www.apache.org/licenses/LICENSE-2.0
16 * Unless required by applicable law or agreed to in writing, software
17 * distributed under the License is distributed on an "AS IS" BASIS,
18 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19 * See the License for the specific language governing permissions and
20 * limitations under the License.
21 * ========================================================= */
23 (function($, undefined){
25 var $window = $(window);
28 return new Date(Date.UTC.apply(Date, arguments));
31 var today = new Date();
32 return UTCDate(today.getFullYear(), today.getMonth(), today.getDate());
34 function alias(method){
36 return this[method].apply(this, arguments);
40 var DateArray = (function(){
43 return this.slice(i)[0];
45 contains: function(d){
46 // Array.indexOf is not cross-browser;
47 // $.inArray doesn't work with Dates
48 var val = d && d.valueOf();
49 for (var i=0, l=this.length; i < l; i++)
50 if (this[i].valueOf() === val)
57 replace: function(new_array){
60 if (!$.isArray(new_array))
61 new_array = [new_array];
63 this.push.apply(this, new_array);
69 var a = new DateArray();
77 a.push.apply(a, arguments);
86 var Datepicker = function(element, options){
87 this.dates = new DateArray();
88 this.viewDate = UTCToday();
89 this.focusDate = null;
91 this._process_options(options);
93 this.element = $(element);
94 this.isInline = false;
95 this.isInput = this.element.is('input');
96 this.component = this.element.is('.date') ? this.element.find('.add-on, .input-group-addon, .btn') : false;
97 this.hasInput = this.component && this.element.find('input').length;
98 if (this.component && this.component.length === 0)
99 this.component = false;
101 this.picker = $(DPGlobal.template);
103 this._attachEvents();
106 this.picker.addClass('datepicker-inline').appendTo(this.element);
109 this.picker.addClass('datepicker-dropdown dropdown-menu');
113 this.picker.addClass('datepicker-rtl');
116 this.viewMode = this.o.startView;
118 if (this.o.calendarWeeks)
119 this.picker.find('tfoot th.today')
120 .attr('colspan', function(i, val){
121 return parseInt(val) + 1;
124 this._allow_update = false;
126 this.setStartDate(this._o.startDate);
127 this.setEndDate(this._o.endDate);
128 this.setDaysOfWeekDisabled(this.o.daysOfWeekDisabled);
133 this._allow_update = true;
143 Datepicker.prototype = {
144 constructor: Datepicker,
146 _process_options: function(opts){
147 // Store raw options for reference
148 this._o = $.extend({}, this._o, opts);
150 var o = this.o = $.extend({}, this._o);
152 // Check if "de-DE" style date is available, if not language should
153 // fallback to 2 letter code eg "de"
154 var lang = o.language;
156 lang = lang.split('-')[0];
158 lang = defaults.language;
162 switch (o.startView){
175 switch (o.minViewMode){
188 o.startView = Math.max(o.startView, o.minViewMode);
190 // true, false, or Number > 0
191 if (o.multidate !== true){
192 o.multidate = Number(o.multidate) || false;
193 if (o.multidate !== false)
194 o.multidate = Math.max(0, o.multidate);
198 o.multidateSeparator = String(o.multidateSeparator);
201 o.weekEnd = ((o.weekStart + 6) % 7);
203 var format = DPGlobal.parseFormat(o.format);
204 if (o.startDate !== -Infinity){
206 if (o.startDate instanceof Date)
207 o.startDate = this._local_to_utc(this._zero_time(o.startDate));
209 o.startDate = DPGlobal.parseDate(o.startDate, format, o.language);
212 o.startDate = -Infinity;
215 if (o.endDate !== Infinity){
217 if (o.endDate instanceof Date)
218 o.endDate = this._local_to_utc(this._zero_time(o.endDate));
220 o.endDate = DPGlobal.parseDate(o.endDate, format, o.language);
223 o.endDate = Infinity;
227 o.daysOfWeekDisabled = o.daysOfWeekDisabled||[];
228 if (!$.isArray(o.daysOfWeekDisabled))
229 o.daysOfWeekDisabled = o.daysOfWeekDisabled.split(/[,\s]*/);
230 o.daysOfWeekDisabled = $.map(o.daysOfWeekDisabled, function(d){
231 return parseInt(d, 10);
234 var plc = String(o.orientation).toLowerCase().split(/\s+/g),
235 _plc = o.orientation.toLowerCase();
236 plc = $.grep(plc, function(word){
237 return (/^auto|left|right|top|bottom$/).test(word);
239 o.orientation = {x: 'auto', y: 'auto'};
240 if (!_plc || _plc === 'auto')
242 else if (plc.length === 1){
246 o.orientation.y = plc[0];
250 o.orientation.x = plc[0];
255 _plc = $.grep(plc, function(word){
256 return (/^left|right$/).test(word);
258 o.orientation.x = _plc[0] || 'auto';
260 _plc = $.grep(plc, function(word){
261 return (/^top|bottom$/).test(word);
263 o.orientation.y = _plc[0] || 'auto';
267 _secondaryEvents: [],
268 _applyEvents: function(evs){
269 for (var i=0, el, ch, ev; i < evs.length; i++){
271 if (evs[i].length === 2){
275 else if (evs[i].length === 3){
282 _unapplyEvents: function(evs){
283 for (var i=0, el, ev, ch; i < evs.length; i++){
285 if (evs[i].length === 2){
289 else if (evs[i].length === 3){
296 _buildEvents: function(){
297 if (this.isInput){ // single input
300 focus: $.proxy(this.show, this),
301 keyup: $.proxy(function(e){
302 if ($.inArray(e.keyCode, [27,37,39,38,40,32,13,9]) === -1)
305 keydown: $.proxy(this.keydown, this)
309 else if (this.component && this.hasInput){ // component: input + button
311 // For components that are not readonly, allow keyboard nav
312 [this.element.find('input'), {
313 focus: $.proxy(this.show, this),
314 keyup: $.proxy(function(e){
315 if ($.inArray(e.keyCode, [27,37,39,38,40,32,13,9]) === -1)
318 keydown: $.proxy(this.keydown, this)
321 click: $.proxy(this.show, this)
325 else if (this.element.is('div')){ // inline datepicker
326 this.isInline = true;
331 click: $.proxy(this.show, this)
336 // Component: listen for blur on element descendants
337 [this.element, '*', {
338 blur: $.proxy(function(e){
339 this._focused_from = e.target;
342 // Input: listen for blur on element
344 blur: $.proxy(function(e){
345 this._focused_from = e.target;
350 this._secondaryEvents = [
352 click: $.proxy(this.click, this)
355 resize: $.proxy(this.place, this)
358 'mousedown touchstart': $.proxy(function(e){
359 // Clicked outside the datepicker, hide it
361 this.element.is(e.target) ||
362 this.element.find(e.target).length ||
363 this.picker.is(e.target) ||
364 this.picker.find(e.target).length
372 _attachEvents: function(){
373 this._detachEvents();
374 this._applyEvents(this._events);
376 _detachEvents: function(){
377 this._unapplyEvents(this._events);
379 _attachSecondaryEvents: function(){
380 this._detachSecondaryEvents();
381 this._applyEvents(this._secondaryEvents);
383 _detachSecondaryEvents: function(){
384 this._unapplyEvents(this._secondaryEvents);
386 _trigger: function(event, altdate){
387 var date = altdate || this.dates.get(-1),
388 local_date = this._utc_to_local(date);
390 this.element.trigger({
393 dates: $.map(this.dates, this._utc_to_local),
394 format: $.proxy(function(ix, format){
395 if (arguments.length === 0){
396 ix = this.dates.length - 1;
397 format = this.o.format;
399 else if (typeof ix === 'string'){
401 ix = this.dates.length - 1;
403 format = format || this.o.format;
404 var date = this.dates.get(ix);
405 return DPGlobal.formatDate(date, format, this.o.language);
412 this.picker.appendTo('body');
415 this._attachSecondaryEvents();
416 this._trigger('show');
422 if (!this.picker.is(':visible'))
424 this.focusDate = null;
425 this.picker.hide().detach();
426 this._detachSecondaryEvents();
427 this.viewMode = this.o.startView;
433 this.isInput && this.element.val() ||
434 this.hasInput && this.element.find('input').val()
438 this._trigger('hide');
443 this._detachEvents();
444 this._detachSecondaryEvents();
445 this.picker.remove();
446 delete this.element.data().datepicker;
448 delete this.element.data().date;
452 _utc_to_local: function(utc){
453 return utc && new Date(utc.getTime() + (utc.getTimezoneOffset()*60000));
455 _local_to_utc: function(local){
456 return local && new Date(local.getTime() - (local.getTimezoneOffset()*60000));
458 _zero_time: function(local){
459 return local && new Date(local.getFullYear(), local.getMonth(), local.getDate());
461 _zero_utc_time: function(utc){
462 return utc && new Date(Date.UTC(utc.getUTCFullYear(), utc.getUTCMonth(), utc.getUTCDate()));
465 getDates: function(){
466 return $.map(this.dates, this._utc_to_local);
469 getUTCDates: function(){
470 return $.map(this.dates, function(d){
476 return this._utc_to_local(this.getUTCDate());
479 getUTCDate: function(){
480 return new Date(this.dates.get(-1));
483 setDates: function(){
484 var args = $.isArray(arguments[0]) ? arguments[0] : arguments;
485 this.update.apply(this, args);
486 this._trigger('changeDate');
490 setUTCDates: function(){
491 var args = $.isArray(arguments[0]) ? arguments[0] : arguments;
492 this.update.apply(this, $.map(args, this._utc_to_local));
493 this._trigger('changeDate');
497 setDate: alias('setDates'),
498 setUTCDate: alias('setUTCDates'),
500 setValue: function(){
501 var formatted = this.getFormattedDate();
504 this.element.find('input').val(formatted).change();
508 this.element.val(formatted).change();
512 getFormattedDate: function(format){
513 if (format === undefined)
514 format = this.o.format;
516 var lang = this.o.language;
517 return $.map(this.dates, function(d){
518 return DPGlobal.formatDate(d, format, lang);
519 }).join(this.o.multidateSeparator);
522 setStartDate: function(startDate){
523 this._process_options({startDate: startDate});
525 this.updateNavArrows();
528 setEndDate: function(endDate){
529 this._process_options({endDate: endDate});
531 this.updateNavArrows();
534 setDaysOfWeekDisabled: function(daysOfWeekDisabled){
535 this._process_options({daysOfWeekDisabled: daysOfWeekDisabled});
537 this.updateNavArrows();
543 var calendarWidth = this.picker.outerWidth(),
544 calendarHeight = this.picker.outerHeight(),
546 windowWidth = $window.width(),
547 windowHeight = $window.height(),
548 scrollTop = $window.scrollTop();
550 var zIndex = parseInt(this.element.parents().filter(function(){
551 return $(this).css('z-index') !== 'auto';
552 }).first().css('z-index'))+10;
553 var offset = this.component ? this.component.parent().offset() : this.element.offset();
554 var height = this.component ? this.component.outerHeight(true) : this.element.outerHeight(false);
555 var width = this.component ? this.component.outerWidth(true) : this.element.outerWidth(false);
556 var left = offset.left,
559 this.picker.removeClass(
560 'datepicker-orient-top datepicker-orient-bottom '+
561 'datepicker-orient-right datepicker-orient-left'
564 if (this.o.orientation.x !== 'auto'){
565 this.picker.addClass('datepicker-orient-' + this.o.orientation.x);
566 if (this.o.orientation.x === 'right')
567 left -= calendarWidth - width;
569 // auto x orientation is best-placement: if it crosses a window
570 // edge, fudge it sideways
573 this.picker.addClass('datepicker-orient-left');
575 left -= offset.left - visualPadding;
576 else if (offset.left + calendarWidth > windowWidth)
577 left = windowWidth - calendarWidth - visualPadding;
580 // auto y orientation is best-situation: top or bottom, no fudging,
581 // decision based on which shows more of the calendar
582 var yorient = this.o.orientation.y,
583 top_overflow, bottom_overflow;
584 if (yorient === 'auto'){
585 top_overflow = -scrollTop + offset.top - calendarHeight;
586 bottom_overflow = scrollTop + windowHeight - (offset.top + height + calendarHeight);
587 if (Math.max(top_overflow, bottom_overflow) === bottom_overflow)
592 this.picker.addClass('datepicker-orient-' + yorient);
593 if (yorient === 'top')
596 top -= calendarHeight + parseInt(this.picker.css('padding-top'));
607 if (!this._allow_update)
610 var oldDates = this.dates.copy(),
613 if (arguments.length){
614 $.each(arguments, $.proxy(function(i, date){
615 if (date instanceof Date)
616 date = this._local_to_utc(date);
624 : this.element.data('date') || this.element.find('input').val();
625 if (dates && this.o.multidate)
626 dates = dates.split(this.o.multidateSeparator);
629 delete this.element.data().date;
632 dates = $.map(dates, $.proxy(function(date){
633 return DPGlobal.parseDate(date, this.o.format, this.o.language);
635 dates = $.grep(dates, $.proxy(function(date){
637 date < this.o.startDate ||
638 date > this.o.endDate ||
642 this.dates.replace(dates);
644 if (this.dates.length)
645 this.viewDate = new Date(this.dates.get(-1));
646 else if (this.viewDate < this.o.startDate)
647 this.viewDate = new Date(this.o.startDate);
648 else if (this.viewDate > this.o.endDate)
649 this.viewDate = new Date(this.o.endDate);
652 // setting date by clicking
655 else if (dates.length){
656 // setting date by typing
657 if (String(oldDates) !== String(this.dates))
658 this._trigger('changeDate');
660 if (!this.dates.length && oldDates.length)
661 this._trigger('clearDate');
667 var dowCnt = this.o.weekStart,
669 if (this.o.calendarWeeks){
670 var cell = '<th class="cw"> </th>';
672 this.picker.find('.datepicker-days thead tr:first-child').prepend(cell);
674 while (dowCnt < this.o.weekStart + 7){
675 html += '<th class="dow">'+dates[this.o.language].daysMin[(dowCnt++)%7]+'</th>';
678 this.picker.find('.datepicker-days thead').append(html);
681 fillMonths: function(){
685 html += '<span class="month">'+dates[this.o.language].monthsShort[i++]+'</span>';
687 this.picker.find('.datepicker-months td').html(html);
690 setRange: function(range){
691 if (!range || !range.length)
694 this.range = $.map(range, function(d){
700 getClassNames: function(date){
702 year = this.viewDate.getUTCFullYear(),
703 month = this.viewDate.getUTCMonth(),
705 if (date.getUTCFullYear() < year || (date.getUTCFullYear() === year && date.getUTCMonth() < month)){
708 else if (date.getUTCFullYear() > year || (date.getUTCFullYear() === year && date.getUTCMonth() > month)){
711 if (this.focusDate && date.valueOf() === this.focusDate.valueOf())
713 // Compare internal UTC date with local today, not UTC today
714 if (this.o.todayHighlight &&
715 date.getUTCFullYear() === today.getFullYear() &&
716 date.getUTCMonth() === today.getMonth() &&
717 date.getUTCDate() === today.getDate()){
720 if (this.dates.contains(date) !== -1)
722 if (date.valueOf() < this.o.startDate || date.valueOf() > this.o.endDate ||
723 $.inArray(date.getUTCDay(), this.o.daysOfWeekDisabled) !== -1){
724 cls.push('disabled');
727 if (date > this.range[0] && date < this.range[this.range.length-1]){
730 if ($.inArray(date.valueOf(), this.range) !== -1){
731 cls.push('selected');
738 var d = new Date(this.viewDate),
739 year = d.getUTCFullYear(),
740 month = d.getUTCMonth(),
741 startYear = this.o.startDate !== -Infinity ? this.o.startDate.getUTCFullYear() : -Infinity,
742 startMonth = this.o.startDate !== -Infinity ? this.o.startDate.getUTCMonth() : -Infinity,
743 endYear = this.o.endDate !== Infinity ? this.o.endDate.getUTCFullYear() : Infinity,
744 endMonth = this.o.endDate !== Infinity ? this.o.endDate.getUTCMonth() : Infinity,
745 todaytxt = dates[this.o.language].today || dates['en'].today || '',
746 cleartxt = dates[this.o.language].clear || dates['en'].clear || '',
748 this.picker.find('.datepicker-days thead th.datepicker-switch')
749 .text(dates[this.o.language].months[month]+' '+year);
750 this.picker.find('tfoot th.today')
752 .toggle(this.o.todayBtn !== false);
753 this.picker.find('tfoot th.clear')
755 .toggle(this.o.clearBtn !== false);
756 this.updateNavArrows();
758 var prevMonth = UTCDate(year, month-1, 28),
759 day = DPGlobal.getDaysInMonth(prevMonth.getUTCFullYear(), prevMonth.getUTCMonth());
760 prevMonth.setUTCDate(day);
761 prevMonth.setUTCDate(day - (prevMonth.getUTCDay() - this.o.weekStart + 7)%7);
762 var nextMonth = new Date(prevMonth);
763 nextMonth.setUTCDate(nextMonth.getUTCDate() + 42);
764 nextMonth = nextMonth.valueOf();
767 while (prevMonth.valueOf() < nextMonth){
768 if (prevMonth.getUTCDay() === this.o.weekStart){
770 if (this.o.calendarWeeks){
771 // ISO 8601: First week contains first thursday.
772 // ISO also states week starts on Monday, but we can be more abstract here.
774 // Start of current week: based on weekstart/current date
775 ws = new Date(+prevMonth + (this.o.weekStart - prevMonth.getUTCDay() - 7) % 7 * 864e5),
776 // Thursday of this week
777 th = new Date(Number(ws) + (7 + 4 - ws.getUTCDay()) % 7 * 864e5),
778 // First Thursday of year, year from thursday
779 yth = new Date(Number(yth = UTCDate(th.getUTCFullYear(), 0, 1)) + (7 + 4 - yth.getUTCDay())%7*864e5),
780 // Calendar week: ms between thursdays, div ms per day, div 7 days
781 calWeek = (th - yth) / 864e5 / 7 + 1;
782 html.push('<td class="cw">'+ calWeek +'</td>');
786 clsName = this.getClassNames(prevMonth);
789 if (this.o.beforeShowDay !== $.noop){
790 var before = this.o.beforeShowDay(this._utc_to_local(prevMonth));
791 if (before === undefined)
793 else if (typeof(before) === 'boolean')
794 before = {enabled: before};
795 else if (typeof(before) === 'string')
796 before = {classes: before};
797 if (before.enabled === false)
798 clsName.push('disabled');
800 clsName = clsName.concat(before.classes.split(/\s+/));
802 tooltip = before.tooltip;
805 clsName = $.unique(clsName);
806 html.push('<td class="'+clsName.join(' ')+'"' + (tooltip ? ' title="'+tooltip+'"' : '') + '>'+prevMonth.getUTCDate() + '</td>');
807 if (prevMonth.getUTCDay() === this.o.weekEnd){
810 prevMonth.setUTCDate(prevMonth.getUTCDate()+1);
812 this.picker.find('.datepicker-days tbody').empty().append(html.join(''));
814 var months = this.picker.find('.datepicker-months')
818 .find('span').removeClass('active');
820 $.each(this.dates, function(i, d){
821 if (d.getUTCFullYear() === year)
822 months.eq(d.getUTCMonth()).addClass('active');
825 if (year < startYear || year > endYear){
826 months.addClass('disabled');
828 if (year === startYear){
829 months.slice(0, startMonth).addClass('disabled');
831 if (year === endYear){
832 months.slice(endMonth+1).addClass('disabled');
836 year = parseInt(year/10, 10) * 10;
837 var yearCont = this.picker.find('.datepicker-years')
839 .text(year + '-' + (year + 9))
843 var years = $.map(this.dates, function(d){
844 return d.getUTCFullYear();
847 for (var i = -1; i < 11; i++){
853 if ($.inArray(year, years) !== -1)
854 classes.push('active');
855 if (year < startYear || year > endYear)
856 classes.push('disabled');
857 html += '<span class="' + classes.join(' ') + '">'+year+'</span>';
863 updateNavArrows: function(){
864 if (!this._allow_update)
867 var d = new Date(this.viewDate),
868 year = d.getUTCFullYear(),
869 month = d.getUTCMonth();
870 switch (this.viewMode){
872 if (this.o.startDate !== -Infinity && year <= this.o.startDate.getUTCFullYear() && month <= this.o.startDate.getUTCMonth()){
873 this.picker.find('.prev').css({visibility: 'hidden'});
876 this.picker.find('.prev').css({visibility: 'visible'});
878 if (this.o.endDate !== Infinity && year >= this.o.endDate.getUTCFullYear() && month >= this.o.endDate.getUTCMonth()){
879 this.picker.find('.next').css({visibility: 'hidden'});
882 this.picker.find('.next').css({visibility: 'visible'});
887 if (this.o.startDate !== -Infinity && year <= this.o.startDate.getUTCFullYear()){
888 this.picker.find('.prev').css({visibility: 'hidden'});
891 this.picker.find('.prev').css({visibility: 'visible'});
893 if (this.o.endDate !== Infinity && year >= this.o.endDate.getUTCFullYear()){
894 this.picker.find('.next').css({visibility: 'hidden'});
897 this.picker.find('.next').css({visibility: 'visible'});
905 var target = $(e.target).closest('span, td, th'),
907 if (target.length === 1){
908 switch (target[0].nodeName.toLowerCase()){
910 switch (target[0].className){
911 case 'datepicker-switch':
916 var dir = DPGlobal.modes[this.viewMode].navStep * (target[0].className === 'prev' ? -1 : 1);
917 switch (this.viewMode){
919 this.viewDate = this.moveMonth(this.viewDate, dir);
920 this._trigger('changeMonth', this.viewDate);
924 this.viewDate = this.moveYear(this.viewDate, dir);
925 if (this.viewMode === 1)
926 this._trigger('changeYear', this.viewDate);
932 var date = new Date();
933 date = UTCDate(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0);
936 var which = this.o.todayBtn === 'linked' ? null : 'view';
937 this._setDate(date, which);
942 element = this.element;
943 else if (this.component)
944 element = this.element.find('input');
946 element.val("").change();
948 this._trigger('changeDate');
949 if (this.o.autoclose)
955 if (!target.is('.disabled')){
956 this.viewDate.setUTCDate(1);
957 if (target.is('.month')){
959 month = target.parent().find('span').index(target);
960 year = this.viewDate.getUTCFullYear();
961 this.viewDate.setUTCMonth(month);
962 this._trigger('changeMonth', this.viewDate);
963 if (this.o.minViewMode === 1){
964 this._setDate(UTCDate(year, month, day));
970 year = parseInt(target.text(), 10)||0;
971 this.viewDate.setUTCFullYear(year);
972 this._trigger('changeYear', this.viewDate);
973 if (this.o.minViewMode === 2){
974 this._setDate(UTCDate(year, month, day));
982 if (target.is('.day') && !target.is('.disabled')){
983 day = parseInt(target.text(), 10)||1;
984 year = this.viewDate.getUTCFullYear();
985 month = this.viewDate.getUTCMonth();
986 if (target.is('.old')){
995 else if (target.is('.new')){
1004 this._setDate(UTCDate(year, month, day));
1009 if (this.picker.is(':visible') && this._focused_from){
1010 $(this._focused_from).focus();
1012 delete this._focused_from;
1015 _toggle_multidate: function(date){
1016 var ix = this.dates.contains(date);
1020 else if (ix !== -1){
1021 this.dates.remove(ix);
1024 this.dates.push(date);
1026 if (typeof this.o.multidate === 'number')
1027 while (this.dates.length > this.o.multidate)
1028 this.dates.remove(0);
1031 _setDate: function(date, which){
1032 if (!which || which === 'date')
1033 this._toggle_multidate(date && new Date(date));
1034 if (!which || which === 'view')
1035 this.viewDate = date && new Date(date);
1039 this._trigger('changeDate');
1042 element = this.element;
1044 else if (this.component){
1045 element = this.element.find('input');
1050 if (this.o.autoclose && (!which || which === 'date')){
1055 moveMonth: function(date, dir){
1060 var new_date = new Date(date.valueOf()),
1061 day = new_date.getUTCDate(),
1062 month = new_date.getUTCMonth(),
1063 mag = Math.abs(dir),
1065 dir = dir > 0 ? 1 : -1;
1068 // If going back one month, make sure month is not current month
1069 // (eg, Mar 31 -> Feb 31 == Feb 28, not Mar 02)
1071 return new_date.getUTCMonth() === month;
1073 // If going forward one month, make sure month is as expected
1074 // (eg, Jan 31 -> Feb 31 == Feb 28, not Mar 02)
1076 return new_date.getUTCMonth() !== new_month;
1078 new_month = month + dir;
1079 new_date.setUTCMonth(new_month);
1080 // Dec -> Jan (12) or Jan -> Dec (-1) -- limit expected date to 0-11
1081 if (new_month < 0 || new_month > 11)
1082 new_month = (new_month + 12) % 12;
1085 // For magnitudes >1, move one month at a time...
1086 for (var i=0; i < mag; i++)
1087 // ...which might decrease the day (eg, Jan 31 to Feb 28, etc)...
1088 new_date = this.moveMonth(new_date, dir);
1089 // ...then reset the day, keeping it in the new month
1090 new_month = new_date.getUTCMonth();
1091 new_date.setUTCDate(day);
1093 return new_month !== new_date.getUTCMonth();
1096 // Common date-resetting loop -- if date is beyond end of month, make it
1099 new_date.setUTCDate(--day);
1100 new_date.setUTCMonth(new_month);
1105 moveYear: function(date, dir){
1106 return this.moveMonth(date, dir*12);
1109 dateWithinRange: function(date){
1110 return date >= this.o.startDate && date <= this.o.endDate;
1113 keydown: function(e){
1114 if (this.picker.is(':not(:visible)')){
1115 if (e.keyCode === 27) // allow escape to hide and re-show picker
1119 var dateChanged = false,
1120 dir, newDate, newViewDate,
1121 focusDate = this.focusDate || this.viewDate;
1124 if (this.focusDate){
1125 this.focusDate = null;
1126 this.viewDate = this.dates.get(-1) || this.viewDate;
1135 if (!this.o.keyboardNavigation)
1137 dir = e.keyCode === 37 ? -1 : 1;
1139 newDate = this.moveYear(this.dates.get(-1) || UTCToday(), dir);
1140 newViewDate = this.moveYear(focusDate, dir);
1141 this._trigger('changeYear', this.viewDate);
1143 else if (e.shiftKey){
1144 newDate = this.moveMonth(this.dates.get(-1) || UTCToday(), dir);
1145 newViewDate = this.moveMonth(focusDate, dir);
1146 this._trigger('changeMonth', this.viewDate);
1149 newDate = new Date(this.dates.get(-1) || UTCToday());
1150 newDate.setUTCDate(newDate.getUTCDate() + dir);
1151 newViewDate = new Date(focusDate);
1152 newViewDate.setUTCDate(focusDate.getUTCDate() + dir);
1154 if (this.dateWithinRange(newDate)){
1155 this.focusDate = this.viewDate = newViewDate;
1163 if (!this.o.keyboardNavigation)
1165 dir = e.keyCode === 38 ? -1 : 1;
1167 newDate = this.moveYear(this.dates.get(-1) || UTCToday(), dir);
1168 newViewDate = this.moveYear(focusDate, dir);
1169 this._trigger('changeYear', this.viewDate);
1171 else if (e.shiftKey){
1172 newDate = this.moveMonth(this.dates.get(-1) || UTCToday(), dir);
1173 newViewDate = this.moveMonth(focusDate, dir);
1174 this._trigger('changeMonth', this.viewDate);
1177 newDate = new Date(this.dates.get(-1) || UTCToday());
1178 newDate.setUTCDate(newDate.getUTCDate() + dir * 7);
1179 newViewDate = new Date(focusDate);
1180 newViewDate.setUTCDate(focusDate.getUTCDate() + dir * 7);
1182 if (this.dateWithinRange(newDate)){
1183 this.focusDate = this.viewDate = newViewDate;
1189 case 32: // spacebar
1190 // Spacebar is used in manually typing dates in some formats.
1191 // As such, its behavior should not be hijacked.
1194 focusDate = this.focusDate || this.dates.get(-1) || this.viewDate;
1195 this._toggle_multidate(focusDate);
1197 this.focusDate = null;
1198 this.viewDate = this.dates.get(-1) || this.viewDate;
1201 if (this.picker.is(':visible')){
1203 if (this.o.autoclose)
1208 this.focusDate = null;
1209 this.viewDate = this.dates.get(-1) || this.viewDate;
1215 if (this.dates.length)
1216 this._trigger('changeDate');
1218 this._trigger('clearDate');
1221 element = this.element;
1223 else if (this.component){
1224 element = this.element.find('input');
1232 showMode: function(dir){
1234 this.viewMode = Math.max(this.o.minViewMode, Math.min(2, this.viewMode + dir));
1239 .filter('.datepicker-'+DPGlobal.modes[this.viewMode].clsName)
1240 .css('display', 'block');
1241 this.updateNavArrows();
1245 var DateRangePicker = function(element, options){
1246 this.element = $(element);
1247 this.inputs = $.map(options.inputs, function(i){
1248 return i.jquery ? i[0] : i;
1250 delete options.inputs;
1253 .datepicker(options)
1254 .bind('changeDate', $.proxy(this.dateUpdated, this));
1256 this.pickers = $.map(this.inputs, function(i){
1257 return $(i).data('datepicker');
1261 DateRangePicker.prototype = {
1262 updateDates: function(){
1263 this.dates = $.map(this.pickers, function(i){
1264 return i.getUTCDate();
1266 this.updateRanges();
1268 updateRanges: function(){
1269 var range = $.map(this.dates, function(d){
1272 $.each(this.pickers, function(i, p){
1276 dateUpdated: function(e){
1277 // `this.updating` is a workaround for preventing infinite recursion
1278 // between `changeDate` triggering and `setUTCDate` calling. Until
1279 // there is a better mechanism.
1282 this.updating = true;
1284 var dp = $(e.target).data('datepicker'),
1285 new_date = dp.getUTCDate(),
1286 i = $.inArray(e.target, this.inputs),
1287 l = this.inputs.length;
1291 $.each(this.pickers, function(i, p){
1292 if (!p.getUTCDate())
1293 p.setUTCDate(new_date);
1296 if (new_date < this.dates[i]){
1297 // Date being moved earlier/left
1298 while (i >= 0 && new_date < this.dates[i]){
1299 this.pickers[i--].setUTCDate(new_date);
1302 else if (new_date > this.dates[i]){
1303 // Date being moved later/right
1304 while (i < l && new_date > this.dates[i]){
1305 this.pickers[i++].setUTCDate(new_date);
1310 delete this.updating;
1313 $.map(this.pickers, function(p){ p.remove(); });
1314 delete this.element.data().datepicker;
1318 function opts_from_el(el, prefix){
1319 // Derive options from element data-attrs
1320 var data = $(el).data(),
1322 replace = new RegExp('^' + prefix.toLowerCase() + '([A-Z])');
1323 prefix = new RegExp('^' + prefix.toLowerCase());
1324 function re_lower(_,a){
1325 return a.toLowerCase();
1327 for (var key in data)
1328 if (prefix.test(key)){
1329 inkey = key.replace(replace, re_lower);
1330 out[inkey] = data[key];
1335 function opts_from_locale(lang){
1336 // Derive options from locale plugins
1338 // Check if "de-DE" style date is available, if not language should
1339 // fallback to 2 letter code eg "de"
1341 lang = lang.split('-')[0];
1345 var d = dates[lang];
1346 $.each(locale_opts, function(i,k){
1353 var old = $.fn.datepicker;
1354 $.fn.datepicker = function(option){
1355 var args = Array.apply(null, arguments);
1357 var internal_return;
1358 this.each(function(){
1359 var $this = $(this),
1360 data = $this.data('datepicker'),
1361 options = typeof option === 'object' && option;
1363 var elopts = opts_from_el(this, 'date'),
1364 // Preliminary otions
1365 xopts = $.extend({}, defaults, elopts, options),
1366 locopts = opts_from_locale(xopts.language),
1367 // Options priority: js args, data-attrs, locales, defaults
1368 opts = $.extend({}, defaults, locopts, elopts, options);
1369 if ($this.is('.input-daterange') || opts.inputs){
1371 inputs: opts.inputs || $this.find('input').toArray()
1373 $this.data('datepicker', (data = new DateRangePicker(this, $.extend(opts, ropts))));
1376 $this.data('datepicker', (data = new Datepicker(this, opts)));
1379 if (typeof option === 'string' && typeof data[option] === 'function'){
1380 internal_return = data[option].apply(data, args);
1381 if (internal_return !== undefined)
1385 if (internal_return !== undefined)
1386 return internal_return;
1391 var defaults = $.fn.datepicker.defaults = {
1393 beforeShowDay: $.noop,
1394 calendarWeeks: false,
1396 daysOfWeekDisabled: [],
1399 format: 'mm/dd/yyyy',
1400 keyboardNavigation: true,
1404 multidateSeparator: ',',
1405 orientation: "auto",
1407 startDate: -Infinity,
1410 todayHighlight: false,
1413 var locale_opts = $.fn.datepicker.locale_opts = [
1418 $.fn.datepicker.Constructor = Datepicker;
1419 var dates = $.fn.datepicker.dates = {
1421 days: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"],
1422 daysShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
1423 daysMin: ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"],
1424 months: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
1425 monthsShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
1448 isLeapYear: function(year){
1449 return (((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0));
1451 getDaysInMonth: function(year, month){
1452 return [31, (DPGlobal.isLeapYear(year) ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month];
1454 validParts: /dd?|DD?|mm?|MM?|yy(?:yy)?/g,
1455 nonpunctuation: /[^ -\/:-@\[\u3400-\u9fff-`{-~\t\n\r]+/g,
1456 parseFormat: function(format){
1457 // IE treats \0 as a string end in inputs (truncating the value),
1458 // so it's a bad format delimiter, anyway
1459 var separators = format.replace(this.validParts, '\0').split('\0'),
1460 parts = format.match(this.validParts);
1461 if (!separators || !separators.length || !parts || parts.length === 0){
1462 throw new Error("Invalid date format.");
1464 return {separators: separators, parts: parts};
1466 parseDate: function(date, format, language){
1469 if (date instanceof Date)
1471 if (typeof format === 'string')
1472 format = DPGlobal.parseFormat(format);
1473 var part_re = /([\-+]\d+)([dmwy])/,
1474 parts = date.match(/([\-+]\d+)([dmwy])/g),
1476 if (/^[\-+]\d+[dmwy]([\s,]+[\-+]\d+[dmwy])*$/.test(date)){
1478 for (i=0; i < parts.length; i++){
1479 part = part_re.exec(parts[i]);
1480 dir = parseInt(part[1]);
1483 date.setUTCDate(date.getUTCDate() + dir);
1486 date = Datepicker.prototype.moveMonth.call(Datepicker.prototype, date, dir);
1489 date.setUTCDate(date.getUTCDate() + dir * 7);
1492 date = Datepicker.prototype.moveYear.call(Datepicker.prototype, date, dir);
1496 return UTCDate(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), 0, 0, 0);
1498 parts = date && date.match(this.nonpunctuation) || [];
1501 setters_order = ['yyyy', 'yy', 'M', 'MM', 'm', 'mm', 'd', 'dd'],
1503 yyyy: function(d,v){
1504 return d.setUTCFullYear(v);
1507 return d.setUTCFullYear(2000+v);
1513 while (v < 0) v += 12;
1516 while (d.getUTCMonth() !== v)
1517 d.setUTCDate(d.getUTCDate()-1);
1521 return d.setUTCDate(v);
1525 setters_map['M'] = setters_map['MM'] = setters_map['mm'] = setters_map['m'];
1526 setters_map['dd'] = setters_map['d'];
1527 date = UTCDate(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0);
1528 var fparts = format.parts.slice();
1529 // Remove noop parts
1530 if (parts.length !== fparts.length){
1531 fparts = $(fparts).filter(function(i,p){
1532 return $.inArray(p, setters_order) !== -1;
1535 // Process remainder
1536 function match_part(){
1537 var m = this.slice(0, parts[i].length),
1538 p = parts[i].slice(0, m.length);
1541 if (parts.length === fparts.length){
1543 for (i=0, cnt = fparts.length; i < cnt; i++){
1544 val = parseInt(parts[i], 10);
1549 filtered = $(dates[language].months).filter(match_part);
1550 val = $.inArray(filtered[0], dates[language].months) + 1;
1553 filtered = $(dates[language].monthsShort).filter(match_part);
1554 val = $.inArray(filtered[0], dates[language].monthsShort) + 1;
1561 for (i=0; i < setters_order.length; i++){
1562 s = setters_order[i];
1563 if (s in parsed && !isNaN(parsed[s])){
1564 _date = new Date(date);
1565 setters_map[s](_date, parsed[s]);
1573 formatDate: function(date, format, language){
1576 if (typeof format === 'string')
1577 format = DPGlobal.parseFormat(format);
1579 d: date.getUTCDate(),
1580 D: dates[language].daysShort[date.getUTCDay()],
1581 DD: dates[language].days[date.getUTCDay()],
1582 m: date.getUTCMonth() + 1,
1583 M: dates[language].monthsShort[date.getUTCMonth()],
1584 MM: dates[language].months[date.getUTCMonth()],
1585 yy: date.getUTCFullYear().toString().substring(2),
1586 yyyy: date.getUTCFullYear()
1588 val.dd = (val.d < 10 ? '0' : '') + val.d;
1589 val.mm = (val.m < 10 ? '0' : '') + val.m;
1591 var seps = $.extend([], format.separators);
1592 for (var i=0, cnt = format.parts.length; i <= cnt; i++){
1594 date.push(seps.shift());
1595 date.push(val[format.parts[i]]);
1597 return date.join('');
1599 headTemplate: '<thead>'+
1601 '<th class="prev">«</th>'+
1602 '<th colspan="5" class="datepicker-switch"></th>'+
1603 '<th class="next">»</th>'+
1606 contTemplate: '<tbody><tr><td colspan="7"></td></tr></tbody>',
1607 footTemplate: '<tfoot>'+
1609 '<th colspan="7" class="today"></th>'+
1612 '<th colspan="7" class="clear"></th>'+
1616 DPGlobal.template = '<div class="datepicker">'+
1617 '<div class="datepicker-days">'+
1618 '<table class="table table-condensed">'+
1619 DPGlobal.headTemplate+
1621 DPGlobal.footTemplate+
1624 '<div class="datepicker-months">'+
1625 '<table class="table table-condensed">'+
1626 DPGlobal.headTemplate+
1627 DPGlobal.contTemplate+
1628 DPGlobal.footTemplate+
1631 '<div class="datepicker-years">'+
1632 '<table class="table table-condensed">'+
1633 DPGlobal.headTemplate+
1634 DPGlobal.contTemplate+
1635 DPGlobal.footTemplate+
1640 $.fn.datepicker.DPGlobal = DPGlobal;
1643 /* DATEPICKER NO CONFLICT
1644 * =================== */
1646 $.fn.datepicker.noConflict = function(){
1647 $.fn.datepicker = old;
1652 /* DATEPICKER DATA-API
1653 * ================== */
1656 'focus.datepicker.data-api click.datepicker.data-api',
1657 '[data-provide="datepicker"]',
1659 var $this = $(this);
1660 if ($this.data('datepicker'))
1663 // component click requires us to explicitly show it
1664 $this.datepicker('show');
1668 $('[data-provide="datepicker-inline"]').datepicker();