add datetimepicker
[qcg-portal.git] / qcg / static / qcg / datetimepicker / bootstrap-datetimepicker.js
1 /*
2  //! version : 4.0.0
3  =========================================================
4  bootstrap-datetimejs
5  https://github.com/Eonasdan/bootstrap-datetimepicker
6  =========================================================
7  The MIT License (MIT)
8
9  Copyright (c) 2015 Jonathan Peterson
10
11  Permission is hereby granted, free of charge, to any person obtaining a copy
12  of this software and associated documentation files (the "Software"), to deal
13  in the Software without restriction, including without limitation the rights
14  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15  copies of the Software, and to permit persons to whom the Software is
16  furnished to do so, subject to the following conditions:
17
18  The above copyright notice and this permission notice shall be included in
19  all copies or substantial portions of the Software.
20
21  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
27  THE SOFTWARE.
28  */
29 (function (factory) {
30     'use strict';
31     if (typeof define === 'function' && define.amd) {
32         // AMD is used - Register as an anonymous module.
33         define(['jquery', 'moment'], factory);
34     } else if (typeof exports === 'object') {
35         factory(require('jquery'), require('moment'));
36     } else {
37         // Neither AMD nor CommonJS used. Use global variables.
38         if (!jQuery) {
39             throw 'bootstrap-datetimepicker requires jQuery to be loaded first';
40         }
41         if (!moment) {
42             throw 'bootstrap-datetimepicker requires Moment.js to be loaded first';
43         }
44         factory(jQuery, moment);
45     }
46 }(function ($, moment) {
47     'use strict';
48     if (!moment) {
49         throw new Error('bootstrap-datetimepicker requires Moment.js to be loaded first');
50     }
51
52     var dateTimePicker = function (element, options) {
53         var picker = {},
54             date = moment(),
55             viewDate = date.clone(),
56             unset = true,
57             input,
58             component = false,
59             widget = false,
60             use24Hours,
61             minViewModeNumber = 0,
62             actualFormat,
63             parseFormats,
64             currentViewMode,
65             datePickerModes = [
66                 {
67                     clsName: 'days',
68                     navFnc: 'M',
69                     navStep: 1
70                 },
71                 {
72                     clsName: 'months',
73                     navFnc: 'y',
74                     navStep: 1
75                 },
76                 {
77                     clsName: 'years',
78                     navFnc: 'y',
79                     navStep: 10
80                 }
81             ],
82             viewModes = ['days', 'months', 'years'],
83             verticalModes = ['top', 'bottom', 'auto'],
84             horizontalModes = ['left', 'right', 'auto'],
85             toolbarPlacements = ['default', 'top', 'bottom'],
86
87             /********************************************************************************
88              *
89              * Private functions
90              *
91              ********************************************************************************/
92             isEnabled = function (granularity) {
93                 if (typeof granularity !== 'string' || granularity.length > 1) {
94                     throw new TypeError('isEnabled expects a single character string parameter');
95                 }
96                 switch (granularity) {
97                     case 'y':
98                         return actualFormat.indexOf('Y') !== -1;
99                     case 'M':
100                         return actualFormat.indexOf('M') !== -1;
101                     case 'd':
102                         return actualFormat.toLowerCase().indexOf('d') !== -1;
103                     case 'h':
104                     case 'H':
105                         return actualFormat.toLowerCase().indexOf('h') !== -1;
106                     case 'm':
107                         return actualFormat.indexOf('m') !== -1;
108                     case 's':
109                         return actualFormat.indexOf('s') !== -1;
110                     default:
111                         return false;
112                 }
113             },
114
115             hasTime = function () {
116                 return (isEnabled('h') || isEnabled('m') || isEnabled('s'));
117             },
118
119             hasDate = function () {
120                 return (isEnabled('y') || isEnabled('M') || isEnabled('d'));
121             },
122
123             getDatePickerTemplate = function () {
124                 var headTemplate = $('<thead>')
125                         .append($('<tr>')
126                             .append($('<th>').addClass('prev').attr('data-action', 'previous')
127                                 .append($('<span>').addClass(options.icons.previous))
128                                 )
129                             .append($('<th>').addClass('picker-switch').attr('data-action', 'pickerSwitch').attr('colspan', (options.calendarWeeks ? '6' : '5')))
130                             .append($('<th>').addClass('next').attr('data-action', 'next')
131                                 .append($('<span>').addClass(options.icons.next))
132                                 )
133                             ),
134                     contTemplate = $('<tbody>')
135                         .append($('<tr>')
136                             .append($('<td>').attr('colspan', (options.calendarWeeks ? '8' : '7')))
137                             );
138
139                 return [
140                     $('<div>').addClass('datepicker-days')
141                         .append($('<table>').addClass('table-condensed')
142                             .append(headTemplate)
143                             .append($('<tbody>'))
144                             ),
145                     $('<div>').addClass('datepicker-months')
146                         .append($('<table>').addClass('table-condensed')
147                             .append(headTemplate.clone())
148                             .append(contTemplate.clone())
149                             ),
150                     $('<div>').addClass('datepicker-years')
151                         .append($('<table>').addClass('table-condensed')
152                             .append(headTemplate.clone())
153                             .append(contTemplate.clone())
154                             )
155                 ];
156             },
157
158             getTimePickerMainTemplate = function () {
159                 var topRow = $('<tr>'),
160                     middleRow = $('<tr>'),
161                     bottomRow = $('<tr>');
162
163                 if (isEnabled('h')) {
164                     topRow.append($('<td>')
165                         .append($('<a>').attr('href', '#').addClass('btn').attr('data-action', 'incrementHours')
166                             .append($('<span>').addClass(options.icons.up))));
167                     middleRow.append($('<td>')
168                         .append($('<span>').addClass('timepicker-hour').attr('data-time-component', 'hours').attr('data-action', 'showHours')));
169                     bottomRow.append($('<td>')
170                         .append($('<a>').attr('href', '#').addClass('btn').attr('data-action', 'decrementHours')
171                             .append($('<span>').addClass(options.icons.down))));
172                 }
173                 if (isEnabled('m')) {
174                     if (isEnabled('h')) {
175                         topRow.append($('<td>').addClass('separator'));
176                         middleRow.append($('<td>').addClass('separator').html(':'));
177                         bottomRow.append($('<td>').addClass('separator'));
178                     }
179                     topRow.append($('<td>')
180                         .append($('<a>').attr('href', '#').addClass('btn').attr('data-action', 'incrementMinutes')
181                             .append($('<span>').addClass(options.icons.up))));
182                     middleRow.append($('<td>')
183                         .append($('<span>').addClass('timepicker-minute').attr('data-time-component', 'minutes').attr('data-action', 'showMinutes')));
184                     bottomRow.append($('<td>')
185                         .append($('<a>').attr('href', '#').addClass('btn').attr('data-action', 'decrementMinutes')
186                             .append($('<span>').addClass(options.icons.down))));
187                 }
188                 if (isEnabled('s')) {
189                     if (isEnabled('m')) {
190                         topRow.append($('<td>').addClass('separator'));
191                         middleRow.append($('<td>').addClass('separator').html(':'));
192                         bottomRow.append($('<td>').addClass('separator'));
193                     }
194                     topRow.append($('<td>')
195                         .append($('<a>').attr('href', '#').addClass('btn').attr('data-action', 'incrementSeconds')
196                             .append($('<span>').addClass(options.icons.up))));
197                     middleRow.append($('<td>')
198                         .append($('<span>').addClass('timepicker-second').attr('data-time-component', 'seconds').attr('data-action', 'showSeconds')));
199                     bottomRow.append($('<td>')
200                         .append($('<a>').attr('href', '#').addClass('btn').attr('data-action', 'decrementSeconds')
201                             .append($('<span>').addClass(options.icons.down))));
202                 }
203
204                 if (!use24Hours) {
205                     topRow.append($('<td>').addClass('separator'));
206                     middleRow.append($('<td>')
207                         .append($('<button>').addClass('btn btn-primary').attr('data-action', 'togglePeriod')));
208                     bottomRow.append($('<td>').addClass('separator'));
209                 }
210
211                 return $('<div>').addClass('timepicker-picker')
212                     .append($('<table>').addClass('table-condensed')
213                         .append([topRow, middleRow, bottomRow]));
214             },
215
216             getTimePickerTemplate = function () {
217                 var hoursView = $('<div>').addClass('timepicker-hours')
218                         .append($('<table>').addClass('table-condensed')),
219                     minutesView = $('<div>').addClass('timepicker-minutes')
220                         .append($('<table>').addClass('table-condensed')),
221                     secondsView = $('<div>').addClass('timepicker-seconds')
222                         .append($('<table>').addClass('table-condensed')),
223                     ret = [getTimePickerMainTemplate()];
224
225                 if (isEnabled('h')) {
226                     ret.push(hoursView);
227                 }
228                 if (isEnabled('m')) {
229                     ret.push(minutesView);
230                 }
231                 if (isEnabled('s')) {
232                     ret.push(secondsView);
233                 }
234
235                 return ret;
236             },
237
238             getToolbar = function () {
239                 var row = [];
240                 if (options.showTodayButton) {
241                     row.push($('<td>').append($('<a>').attr('data-action', 'today').append($('<span>').addClass(options.icons.today))));
242                 }
243                 if (!options.sideBySide && hasDate() && hasTime()) {
244                     row.push($('<td>').append($('<a>').attr('data-action', 'togglePicker').append($('<span>').addClass(options.icons.time))));
245                 }
246                 if (options.showClear) {
247                     row.push($('<td>').append($('<a>').attr('data-action', 'clear').append($('<span>').addClass(options.icons.clear))));
248                 }
249                 return $('<table>').addClass('table-condensed').append($('<tbody>').append($('<tr>').append(row)));
250             },
251
252             getTemplate = function () {
253                 var template = $('<div>').addClass('bootstrap-datetimepicker-widget dropdown-menu'),
254                     dateView = $('<div>').addClass('datepicker').append(getDatePickerTemplate()),
255                     timeView = $('<div>').addClass('timepicker').append(getTimePickerTemplate()),
256                     content = $('<ul>').addClass('list-unstyled'),
257                     toolbar = $('<li>').addClass('picker-switch' + (options.collapse ? ' accordion-toggle' : '')).append(getToolbar());
258
259                 if (use24Hours) {
260                     template.addClass('usetwentyfour');
261                 }
262                 if (options.sideBySide && hasDate() && hasTime()) {
263                     template.addClass('timepicker-sbs');
264                     template.append(
265                         $('<div>').addClass('row')
266                             .append(dateView.addClass('col-sm-6'))
267                             .append(timeView.addClass('col-sm-6'))
268                     );
269                     template.append(toolbar);
270                     return template;
271                 }
272
273                 if (options.toolbarPlacement === 'top') {
274                     content.append(toolbar);
275                 }
276                 if (hasDate()) {
277                     content.append($('<li>').addClass((options.collapse && hasTime() ? 'collapse in' : '')).append(dateView));
278                 }
279                 if (options.toolbarPlacement === 'default') {
280                     content.append(toolbar);
281                 }
282                 if (hasTime()) {
283                     content.append($('<li>').addClass((options.collapse && hasDate() ? 'collapse' : '')).append(timeView));
284                 }
285                 if (options.toolbarPlacement === 'bottom') {
286                     content.append(toolbar);
287                 }
288                 return template.append(content);
289             },
290
291             dataToOptions = function () {
292                 var eData = element.data(),
293                     dataOptions = {};
294
295                 if (eData.dateOptions && eData.dateOptions instanceof Object) {
296                     dataOptions = $.extend(true, dataOptions, eData.dateOptions);
297                 }
298
299                 $.each(options, function (key) {
300                     var attributeName = 'date' + key.charAt(0).toUpperCase() + key.slice(1);
301                     if (eData[attributeName] !== undefined) {
302                         dataOptions[key] = eData[attributeName];
303                     }
304                 });
305                 return dataOptions;
306             },
307
308             place = function () {
309                 var offset = (component || element).position(),
310                     vertical = options.widgetPositioning.vertical,
311                     horizontal = options.widgetPositioning.horizontal,
312                     parent;
313
314                 if (options.widgetParent) {
315                     parent = options.widgetParent.append(widget);
316                 } else if (element.is('input')) {
317                     parent = element.parent().append(widget);
318                 } else {
319                     parent = element;
320                     element.children().first().after(widget);
321                 }
322
323                 // Top and bottom logic
324                 if (vertical === 'auto') {
325                     if ((component || element).offset().top + widget.height() > $(window).height() + $(window).scrollTop() &&
326                             widget.height() + element.outerHeight() < (component || element).offset().top) {
327                         vertical = 'top';
328                     } else {
329                         vertical = 'bottom';
330                     }
331                 }
332
333                 // Left and right logic
334                 if (horizontal === 'auto') {
335                     if (parent.width() < offset.left + widget.outerWidth()) {
336                         horizontal = 'right';
337                     } else {
338                         horizontal = 'left';
339                     }
340                 }
341
342                 if (vertical === 'top') {
343                     widget.addClass('top').removeClass('bottom');
344                 } else {
345                     widget.addClass('bottom').removeClass('top');
346                 }
347
348                 if (horizontal === 'right') {
349                     widget.addClass('pull-right');
350                 } else {
351                     widget.removeClass('pull-right');
352                 }
353
354                 // find the first parent element that has a relative css positioning
355                 if (parent.css('position') !== 'relative') {
356                     parent = parent.parents().filter(function () {
357                         return $(this).css('position') === 'relative';
358                     }).first();
359                 }
360
361                 if (parent.length === 0) {
362                     throw new Error('datetimepicker component should be placed within a relative positioned container');
363                 }
364
365                 widget.css({
366                     top: vertical === 'top' ? 'auto' : offset.top + element.outerHeight(),
367                     bottom: vertical === 'top' ? offset.top + element.outerHeight() : 'auto',
368                     left: horizontal === 'left' ? parent.css('padding-left') : 'auto',
369                     right: horizontal === 'left' ? 'auto' : parent.css('padding-right')
370                 });
371             },
372
373             notifyEvent = function (e) {
374                 if (e.type === 'dp.change' && ((e.date && e.date.isSame(e.oldDate)) || (!e.date && !e.oldDate))) {
375                     return;
376                 }
377                 element.trigger(e);
378             },
379
380             showMode = function (dir) {
381                 if (!widget) {
382                     return;
383                 }
384                 if (dir) {
385                     currentViewMode = Math.max(minViewModeNumber, Math.min(2, currentViewMode + dir));
386                 }
387                 widget.find('.datepicker > div').hide().filter('.datepicker-' + datePickerModes[currentViewMode].clsName).show();
388             },
389
390             fillDow = function () {
391                 var row = $('<tr>'),
392                     currentDate = viewDate.clone().startOf('w');
393
394                 if (options.calendarWeeks === true) {
395                     row.append($('<th>').addClass('cw').text('#'));
396                 }
397
398                 while (currentDate.isBefore(viewDate.clone().endOf('w'))) {
399                     row.append($('<th>').addClass('dow').text(currentDate.format('dd')));
400                     currentDate.add(1, 'd');
401                 }
402                 widget.find('.datepicker-days thead').append(row);
403             },
404
405             isInDisabledDates = function (date) {
406                 if (!options.disabledDates) {
407                     return false;
408                 }
409                 return options.disabledDates[date.format('YYYY-MM-DD')] === true;
410             },
411
412             isInEnabledDates = function (date) {
413                 if (!options.enabledDates) {
414                     return false;
415                 }
416                 return options.enabledDates[date.format('YYYY-MM-DD')] === true;
417             },
418
419             isValid = function (targetMoment, granularity) {
420                 if (!targetMoment.isValid()) {
421                     return false;
422                 }
423                 if (options.disabledDates && isInDisabledDates(targetMoment)) {
424                     return false;
425                 }
426                 if (options.enabledDates && isInEnabledDates(targetMoment)) {
427                     return true;
428                 }
429                 if (options.minDate && targetMoment.isBefore(options.minDate, granularity)) {
430                     return false;
431                 }
432                 if (options.maxDate && targetMoment.isAfter(options.maxDate, granularity)) {
433                     return false;
434                 }
435                 if (granularity === 'd' && options.daysOfWeekDisabled.indexOf(targetMoment.day()) !== -1) {
436                     return false;
437                 }
438                 return true;
439             },
440
441             fillMonths = function () {
442                 var spans = [],
443                     monthsShort = viewDate.clone().startOf('y').hour(12); // hour is changed to avoid DST issues in some browsers
444                 while (monthsShort.isSame(viewDate, 'y')) {
445                     spans.push($('<span>').attr('data-action', 'selectMonth').addClass('month').text(monthsShort.format('MMM')));
446                     monthsShort.add(1, 'M');
447                 }
448                 widget.find('.datepicker-months td').empty().append(spans);
449             },
450
451             updateMonths = function () {
452                 var monthsView = widget.find('.datepicker-months'),
453                     monthsViewHeader = monthsView.find('th'),
454                     months = monthsView.find('tbody').find('span');
455
456                 monthsView.find('.disabled').removeClass('disabled');
457
458                 if (!isValid(viewDate.clone().subtract(1, 'y'), 'y')) {
459                     monthsViewHeader.eq(0).addClass('disabled');
460                 }
461
462                 monthsViewHeader.eq(1).text(viewDate.year());
463
464                 if (!isValid(viewDate.clone().add(1, 'y'), 'y')) {
465                     monthsViewHeader.eq(2).addClass('disabled');
466                 }
467
468                 months.removeClass('active');
469                 if (date.isSame(viewDate, 'y')) {
470                     months.eq(date.month()).addClass('active');
471                 }
472
473                 months.each(function (index) {
474                     if (!isValid(viewDate.clone().month(index), 'M')) {
475                         $(this).addClass('disabled');
476                     }
477                 });
478             },
479
480             updateYears = function () {
481                 var yearsView = widget.find('.datepicker-years'),
482                     yearsViewHeader = yearsView.find('th'),
483                     startYear = viewDate.clone().subtract(5, 'y'),
484                     endYear = viewDate.clone().add(6, 'y'),
485                     html = '';
486
487                 yearsView.find('.disabled').removeClass('disabled');
488
489                 if (options.minDate && options.minDate.isAfter(startYear, 'y')) {
490                     yearsViewHeader.eq(0).addClass('disabled');
491                 }
492
493                 yearsViewHeader.eq(1).text(startYear.year() + '-' + endYear.year());
494
495                 if (options.maxDate && options.maxDate.isBefore(endYear, 'y')) {
496                     yearsViewHeader.eq(2).addClass('disabled');
497                 }
498
499                 while (!startYear.isAfter(endYear, 'y')) {
500                     html += '<span data-action="selectYear" class="year' + (startYear.isSame(date, 'y') ? ' active' : '') + (!isValid(startYear, 'y') ? ' disabled' : '') + '">' + startYear.year() + '</span>';
501                     startYear.add(1, 'y');
502                 }
503
504                 yearsView.find('td').html(html);
505             },
506
507             fillDate = function () {
508                 var daysView = widget.find('.datepicker-days'),
509                     daysViewHeader = daysView.find('th'),
510                     currentDate,
511                     html = [],
512                     row,
513                     clsName;
514
515                 if (!hasDate()) {
516                     return;
517                 }
518
519                 daysView.find('.disabled').removeClass('disabled');
520                 daysViewHeader.eq(1).text(viewDate.format(options.dayViewHeaderFormat));
521
522                 if (!isValid(viewDate.clone().subtract(1, 'M'), 'M')) {
523                     daysViewHeader.eq(0).addClass('disabled');
524                 }
525                 if (!isValid(viewDate.clone().add(1, 'M'), 'M')) {
526                     daysViewHeader.eq(2).addClass('disabled');
527                 }
528
529                 currentDate = viewDate.clone().startOf('M').startOf('week');
530
531                 while (!viewDate.clone().endOf('M').endOf('w').isBefore(currentDate, 'd')) {
532                     if (currentDate.weekday() === 0) {
533                         row = $('<tr>');
534                         if (options.calendarWeeks) {
535                             row.append('<td class="cw">' + currentDate.week() + '</td>');
536                         }
537                         html.push(row);
538                     }
539                     clsName = '';
540                     if (currentDate.isBefore(viewDate, 'M')) {
541                         clsName += ' old';
542                     }
543                     if (currentDate.isAfter(viewDate, 'M')) {
544                         clsName += ' new';
545                     }
546                     if (currentDate.isSame(date, 'd') && !unset) {
547                         clsName += ' active';
548                     }
549                     if (!isValid(currentDate, 'd')) {
550                         clsName += ' disabled';
551                     }
552                     if (currentDate.isSame(moment(), 'd')) {
553                         clsName += ' today';
554                     }
555                     if (currentDate.day() === 0 || currentDate.day() === 6) {
556                         clsName += ' weekend';
557                     }
558                     row.append('<td data-action="selectDay" class="day' + clsName + '">' + currentDate.date() + '</td>');
559                     currentDate.add(1, 'd');
560                 }
561
562                 daysView.find('tbody').empty().append(html);
563
564                 updateMonths();
565
566                 updateYears();
567             },
568
569             fillHours = function () {
570                 var table = widget.find('.timepicker-hours table'),
571                     currentHour = viewDate.clone().startOf('d'),
572                     html = [],
573                     row = $('<tr>');
574
575                 if (viewDate.hour() > 11 && !use24Hours) {
576                     currentHour.hour(12);
577                 }
578                 while (currentHour.isSame(viewDate, 'd') && (use24Hours || (viewDate.hour() < 12 && currentHour.hour() < 12) || viewDate.hour() > 11)) {
579                     if (currentHour.hour() % 4 === 0) {
580                         row = $('<tr>');
581                         html.push(row);
582                     }
583                     row.append('<td data-action="selectHour" class="hour' + (!isValid(currentHour, 'h') ? ' disabled' : '') + '">' + currentHour.format(use24Hours ? 'HH' : 'hh') + '</td>');
584                     currentHour.add(1, 'h');
585                 }
586                 table.empty().append(html);
587             },
588
589             fillMinutes = function () {
590                 var table = widget.find('.timepicker-minutes table'),
591                     currentMinute = viewDate.clone().startOf('h'),
592                     html = [],
593                     row = $('<tr>'),
594                     step = options.stepping === 1 ? 5 : options.stepping;
595
596                 while (viewDate.isSame(currentMinute, 'h')) {
597                     if (currentMinute.minute() % (step * 4) === 0) {
598                         row = $('<tr>');
599                         html.push(row);
600                     }
601                     row.append('<td data-action="selectMinute" class="minute' + (!isValid(currentMinute, 'm') ? ' disabled' : '') + '">' + currentMinute.format('mm') + '</td>');
602                     currentMinute.add(step, 'm');
603                 }
604                 table.empty().append(html);
605             },
606
607             fillSeconds = function () {
608                 var table = widget.find('.timepicker-seconds table'),
609                     currentSecond = viewDate.clone().startOf('m'),
610                     html = [],
611                     row = $('<tr>');
612
613                 while (viewDate.isSame(currentSecond, 'm')) {
614                     if (currentSecond.second() % 20 === 0) {
615                         row = $('<tr>');
616                         html.push(row);
617                     }
618                     row.append('<td data-action="selectSecond" class="second' + (!isValid(currentSecond, 's') ? ' disabled' : '') + '">' + currentSecond.format('ss') + '</td>');
619                     currentSecond.add(5, 's');
620                 }
621
622                 table.empty().append(html);
623             },
624
625             fillTime = function () {
626                 var timeComponents = widget.find('.timepicker span[data-time-component]');
627                 if (!use24Hours) {
628                     widget.find('.timepicker [data-action=togglePeriod]').text(date.format('A'));
629                 }
630                 timeComponents.filter('[data-time-component=hours]').text(date.format(use24Hours ? 'HH' : 'hh'));
631                 timeComponents.filter('[data-time-component=minutes]').text(date.format('mm'));
632                 timeComponents.filter('[data-time-component=seconds]').text(date.format('ss'));
633
634                 fillHours();
635                 fillMinutes();
636                 fillSeconds();
637             },
638
639             update = function () {
640                 if (!widget) {
641                     return;
642                 }
643                 fillDate();
644                 fillTime();
645             },
646
647             setValue = function (targetMoment) {
648                 var oldDate = unset ? null : date;
649
650                 // case of calling setValue(null or false)
651                 if (!targetMoment) {
652                     unset = true;
653                     input.val('');
654                     element.data('date', '');
655                     notifyEvent({
656                         type: 'dp.change',
657                         date: null,
658                         oldDate: oldDate
659                     });
660                     update();
661                     return;
662                 }
663
664                 targetMoment = targetMoment.clone().locale(options.locale);
665
666                 if (options.stepping !== 1) {
667                     targetMoment.minutes((Math.round(targetMoment.minutes() / options.stepping) * options.stepping) % 60).seconds(0);
668                 }
669
670                 if (isValid(targetMoment)) {
671                     date = targetMoment;
672                     viewDate = date.clone();
673                     input.val(date.format(actualFormat));
674                     element.data('date', date.format(actualFormat));
675                     update();
676                     unset = false;
677                     notifyEvent({
678                         type: 'dp.change',
679                         date: date.clone(),
680                         oldDate: oldDate
681                     });
682                 } else {
683                     input.val(unset ? '' : date.format(actualFormat));
684                     notifyEvent({
685                         type: 'dp.error',
686                         date: targetMoment
687                     });
688                 }
689             },
690
691             hide = function () {
692                 var transitioning = false;
693                 if (!widget) {
694                     return picker;
695                 }
696                 // Ignore event if in the middle of a picker transition
697                 widget.find('.collapse').each(function () {
698                     var collapseData = $(this).data('collapse');
699                     if (collapseData && collapseData.transitioning) {
700                         transitioning = true;
701                         return false;
702                     }
703                 });
704                 if (transitioning) {
705                     return picker;
706                 }
707                 if (component && component.hasClass('btn')) {
708                     component.toggleClass('active');
709                 }
710                 widget.hide();
711
712                 $(window).off('resize', place);
713                 widget.off('click', '[data-action]');
714                 widget.off('mousedown', false);
715
716                 widget.remove();
717                 widget = false;
718
719                 notifyEvent({
720                     type: 'dp.hide',
721                     date: date.clone()
722                 });
723                 return picker;
724             },
725
726             /********************************************************************************
727              *
728              * Widget UI interaction functions
729              *
730              ********************************************************************************/
731             actions = {
732                 next: function () {
733                     viewDate.add(datePickerModes[currentViewMode].navStep, datePickerModes[currentViewMode].navFnc);
734                     fillDate();
735                 },
736
737                 previous: function () {
738                     viewDate.subtract(datePickerModes[currentViewMode].navStep, datePickerModes[currentViewMode].navFnc);
739                     fillDate();
740                 },
741
742                 pickerSwitch: function () {
743                     showMode(1);
744                 },
745
746                 selectMonth: function (e) {
747                     var month = $(e.target).closest('tbody').find('span').index($(e.target));
748                     viewDate.month(month);
749                     if (currentViewMode === minViewModeNumber) {
750                         setValue(date.clone().year(viewDate.year()).month(viewDate.month()));
751                         hide();
752                     }
753                     showMode(-1);
754                     fillDate();
755                 },
756
757                 selectYear: function (e) {
758                     var year = parseInt($(e.target).text(), 10) || 0;
759                     viewDate.year(year);
760                     if (currentViewMode === minViewModeNumber) {
761                         setValue(date.clone().year(viewDate.year()));
762                         hide();
763                     }
764                     showMode(-1);
765                     fillDate();
766                 },
767
768                 selectDay: function (e) {
769                     var day = viewDate.clone();
770                     if ($(e.target).is('.old')) {
771                         day.subtract(1, 'M');
772                     }
773                     if ($(e.target).is('.new')) {
774                         day.add(1, 'M');
775                     }
776                     setValue(day.date(parseInt($(e.target).text(), 10)));
777                     if (!hasTime() && !options.keepOpen) {
778                         hide();
779                     }
780                 },
781
782                 incrementHours: function () {
783                     setValue(date.clone().add(1, 'h'));
784                 },
785
786                 incrementMinutes: function () {
787                     setValue(date.clone().add(options.stepping, 'm'));
788                 },
789
790                 incrementSeconds: function () {
791                     setValue(date.clone().add(1, 's'));
792                 },
793
794                 decrementHours: function () {
795                     setValue(date.clone().subtract(1, 'h'));
796                 },
797
798                 decrementMinutes: function () {
799                     setValue(date.clone().subtract(options.stepping, 'm'));
800                 },
801
802                 decrementSeconds: function () {
803                     setValue(date.clone().subtract(1, 's'));
804                 },
805
806                 togglePeriod: function () {
807                     setValue(date.clone().add((date.hours() >= 12) ? -12 : 12, 'h'));
808                 },
809
810                 togglePicker: function (e) {
811                     var $this = $(e.target),
812                         $parent = $this.closest('ul'),
813                         expanded = $parent.find('.in'),
814                         closed = $parent.find('.collapse:not(.in)'),
815                         collapseData;
816
817                     if (expanded && expanded.length) {
818                         collapseData = expanded.data('collapse');
819                         if (collapseData && collapseData.transitioning) {
820                             return;
821                         }
822                         expanded.collapse('hide');
823                         closed.collapse('show');
824                         if ($this.is('span')) {
825                             $this.toggleClass(options.icons.time + ' ' + options.icons.date);
826                         } else {
827                             $this.find('span').toggleClass(options.icons.time + ' ' + options.icons.date);
828                         }
829
830                         // NOTE: uncomment if toggled state will be restored in show()
831                         //if (component) {
832                         //    component.find('span').toggleClass(options.icons.time + ' ' + options.icons.date);
833                         //}
834                     }
835                 },
836
837                 showPicker: function () {
838                     widget.find('.timepicker > div:not(.timepicker-picker)').hide();
839                     widget.find('.timepicker .timepicker-picker').show();
840                 },
841
842                 showHours: function () {
843                     widget.find('.timepicker .timepicker-picker').hide();
844                     widget.find('.timepicker .timepicker-hours').show();
845                 },
846
847                 showMinutes: function () {
848                     widget.find('.timepicker .timepicker-picker').hide();
849                     widget.find('.timepicker .timepicker-minutes').show();
850                 },
851
852                 showSeconds: function () {
853                     widget.find('.timepicker .timepicker-picker').hide();
854                     widget.find('.timepicker .timepicker-seconds').show();
855                 },
856
857                 selectHour: function (e) {
858                     var hour = parseInt($(e.target).text(), 10);
859
860                     if (!use24Hours) {
861                         if (date.hours() >= 12) {
862                             if (hour !== 12) {
863                                 hour += 12;
864                             }
865                         } else {
866                             if (hour === 12) {
867                                 hour = 0;
868                             }
869                         }
870                     }
871                     setValue(date.clone().hours(hour));
872                     actions.showPicker.call(picker);
873                 },
874
875                 selectMinute: function (e) {
876                     setValue(date.clone().minutes(parseInt($(e.target).text(), 10)));
877                     actions.showPicker.call(picker);
878                 },
879
880                 selectSecond: function (e) {
881                     setValue(date.clone().seconds(parseInt($(e.target).text(), 10)));
882                     actions.showPicker.call(picker);
883                 },
884
885                 clear: function () {
886                     setValue(null);
887                 },
888
889                 today: function () {
890                     setValue(moment());
891                 }
892             },
893
894             doAction = function (e) {
895                 if ($(e.currentTarget).is('.disabled')) {
896                     return false;
897                 }
898                 actions[$(e.currentTarget).data('action')].apply(picker, arguments);
899                 return false;
900             },
901
902             show = function () {
903                 var currentMoment,
904                     useCurrentGranularity = {
905                         'year': function (m) {
906                             return m.month(0).date(1).hours(0).seconds(0).minutes(0);
907                         },
908                         'month': function (m) {
909                             return m.date(1).hours(0).seconds(0).minutes(0);
910                         },
911                         'day': function (m) {
912                             return m.hours(0).seconds(0).minutes(0);
913                         },
914                         'hour': function (m) {
915                             return m.seconds(0).minutes(0);
916                         },
917                         'minute': function (m) {
918                             return m.seconds(0);
919                         }
920                     };
921
922                 if (input.prop('disabled') || input.prop('readonly') || widget) {
923                     return picker;
924                 }
925                 if (options.useCurrent && unset) { // && input.val().trim().length !== 0) { this broke the jasmine test
926                     currentMoment = moment();
927                     if (typeof options.useCurrent === 'string') {
928                         currentMoment = useCurrentGranularity[options.useCurrent](currentMoment);
929                     }
930                     setValue(currentMoment);
931                 }
932
933                 widget = getTemplate();
934
935                 fillDow();
936                 fillMonths();
937
938                 widget.find('.timepicker-hours').hide();
939                 widget.find('.timepicker-minutes').hide();
940                 widget.find('.timepicker-seconds').hide();
941
942                 update();
943                 showMode();
944
945                 $(window).on('resize', place);
946                 widget.on('click', '[data-action]', doAction); // this handles clicks on the widget
947                 widget.on('mousedown', false);
948
949                 if (component && component.hasClass('btn')) {
950                     component.toggleClass('active');
951                 }
952                 widget.show();
953                 place();
954
955                 if (!input.is(':focus')) {
956                     input.focus();
957                 }
958
959                 notifyEvent({
960                     type: 'dp.show'
961                 });
962                 return picker;
963             },
964
965             toggle = function () {
966                 return (widget ? hide() : show());
967             },
968
969             parseInputDate = function (date) {
970                 if (moment.isMoment(date) || date instanceof Date) {
971                     date = moment(date);
972                 } else {
973                     date = moment(date, parseFormats, options.useStrict);
974                 }
975                 date.locale(options.locale);
976                 return date;
977             },
978
979             keydown = function (e) {
980                 if (e.keyCode === 27) { // allow escape to hide picker
981                     hide();
982                 }
983             },
984
985             change = function (e) {
986                 var val = $(e.target).val().trim(),
987                     parsedDate = val ? parseInputDate(val) : null;
988                 setValue(parsedDate);
989                 e.stopImmediatePropagation();
990                 return false;
991             },
992
993             attachDatePickerElementEvents = function () {
994                 input.on({
995                     'change': change,
996                     'blur': hide,
997                     'keydown': keydown
998                 });
999
1000                 if (element.is('input')) {
1001                     input.on({
1002                         'focus': show
1003                     });
1004                 } else if (component) {
1005                     component.on('click', toggle);
1006                     component.on('mousedown', false);
1007                 }
1008             },
1009
1010             detachDatePickerElementEvents = function () {
1011                 input.off({
1012                     'change': change,
1013                     'blur': hide,
1014                     'keydown': keydown
1015                 });
1016
1017                 if (element.is('input')) {
1018                     input.off({
1019                         'focus': show
1020                     });
1021                 } else if (component) {
1022                     component.off('click', toggle);
1023                     component.off('mousedown', false);
1024                 }
1025             },
1026
1027             indexGivenDates = function (givenDatesArray) {
1028                 // Store given enabledDates and disabledDates as keys.
1029                 // This way we can check their existence in O(1) time instead of looping through whole array.
1030                 // (for example: options.enabledDates['2014-02-27'] === true)
1031                 var givenDatesIndexed = {};
1032                 $.each(givenDatesArray, function () {
1033                     var dDate = parseInputDate(this);
1034                     if (dDate.isValid()) {
1035                         givenDatesIndexed[dDate.format('YYYY-MM-DD')] = true;
1036                     }
1037                 });
1038                 return (Object.keys(givenDatesIndexed).length) ? givenDatesIndexed : false;
1039             },
1040
1041             initFormatting = function () {
1042                 var format = options.format || 'L LT';
1043
1044                 actualFormat = format.replace(/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g, function (input) {
1045                     return date.localeData().longDateFormat(input) || input;
1046                 });
1047
1048                 parseFormats = options.extraFormats ? options.extraFormats.slice() : [];
1049                 if (parseFormats.indexOf(format) < 0 && parseFormats.indexOf(actualFormat) < 0) {
1050                     parseFormats.push(actualFormat);
1051                 }
1052
1053                 use24Hours = (actualFormat.toLowerCase().indexOf('a') < 1 && actualFormat.indexOf('h') < 1);
1054
1055                 if (isEnabled('y')) {
1056                     minViewModeNumber = 2;
1057                 }
1058                 if (isEnabled('M')) {
1059                     minViewModeNumber = 1;
1060                 }
1061                 if (isEnabled('d')) {
1062                     minViewModeNumber = 0;
1063                 }
1064
1065                 currentViewMode = Math.max(minViewModeNumber, currentViewMode);
1066
1067                 if (!unset) {
1068                     setValue(date);
1069                 }
1070             };
1071
1072         /********************************************************************************
1073          *
1074          * Public API functions
1075          * =====================
1076          *
1077          * Important: Do not expose direct references to private objects or the options
1078          * object to the outer world. Always return a clone when returning values or make
1079          * a clone when setting a private variable.
1080          *
1081          ********************************************************************************/
1082         picker.destroy = function () {
1083             hide();
1084             detachDatePickerElementEvents();
1085             element.removeData('DateTimePicker');
1086             element.removeData('date');
1087         };
1088
1089         picker.toggle = toggle;
1090
1091         picker.show = show;
1092
1093         picker.hide = hide;
1094
1095         picker.disable = function () {
1096             hide();
1097             if (component && component.hasClass('btn')) {
1098                 component.addClass('disabled');
1099             }
1100             input.prop('disabled', true);
1101             return picker;
1102         };
1103
1104         picker.enable = function () {
1105             if (component && component.hasClass('btn')) {
1106                 component.removeClass('disabled');
1107             }
1108             input.prop('disabled', false);
1109             return picker;
1110         };
1111
1112         picker.options = function (newOptions) {
1113             if (arguments.length === 0) {
1114                 return $.extend(true, {}, options);
1115             }
1116
1117             if (!(newOptions instanceof Object)) {
1118                 throw new TypeError('options() options parameter should be an object');
1119             }
1120             $.extend(true, options, newOptions);
1121             $.each(options, function (key, value) {
1122                 if (picker[key] !== undefined) {
1123                     picker[key](value);
1124                 } else {
1125                     throw new TypeError('option ' + key + ' is not recognized!');
1126                 }
1127             });
1128             return picker;
1129         };
1130
1131         picker.date = function (newDate) {
1132             if (arguments.length === 0) {
1133                 if (unset) {
1134                     return null;
1135                 }
1136                 return date.clone();
1137             }
1138
1139             if (newDate !== null && typeof newDate !== 'string' && !moment.isMoment(newDate) && !(newDate instanceof Date)) {
1140                 throw new TypeError('date() parameter must be one of [null, string, moment or Date]');
1141             }
1142
1143             setValue(newDate === null ? null : parseInputDate(newDate));
1144             return picker;
1145         };
1146
1147         picker.format = function (newFormat) {
1148             if (arguments.length === 0) {
1149                 return options.format;
1150             }
1151
1152             if ((typeof newFormat !== 'string') && ((typeof newFormat !== 'boolean') || (newFormat !== false))) {
1153                 throw new TypeError('format() expects a sting or boolean:false parameter ' + newFormat);
1154             }
1155
1156             options.format = newFormat;
1157             if (actualFormat) {
1158                 initFormatting(); // reinit formatting
1159             }
1160             return picker;
1161         };
1162
1163         picker.dayViewHeaderFormat = function (newFormat) {
1164             if (arguments.length === 0) {
1165                 return options.dayViewHeaderFormat;
1166             }
1167
1168             if (typeof newFormat !== 'string') {
1169                 throw new TypeError('dayViewHeaderFormat() expects a string parameter');
1170             }
1171
1172             options.dayViewHeaderFormat = newFormat;
1173             return picker;
1174         };
1175
1176         picker.extraFormats = function (formats) {
1177             if (arguments.length === 0) {
1178                 return options.extraFormats;
1179             }
1180
1181             if (formats !== false && !(formats instanceof Array)) {
1182                 throw new TypeError('extraFormats() expects an array or false parameter');
1183             }
1184
1185             options.extraFormats = formats;
1186             if (parseFormats) {
1187                 initFormatting(); // reinit formatting
1188             }
1189             return picker;
1190         };
1191
1192         picker.disabledDates = function (dates) {
1193             if (arguments.length === 0) {
1194                 return (options.disabledDates ? $.extend({}, options.disabledDates) : options.disabledDates);
1195             }
1196
1197             if (!dates) {
1198                 options.disabledDates = false;
1199                 update();
1200                 return picker;
1201             }
1202             if (!(dates instanceof Array)) {
1203                 throw new TypeError('disabledDates() expects an array parameter');
1204             }
1205             options.disabledDates = indexGivenDates(dates);
1206             options.enabledDates = false;
1207             update();
1208             return picker;
1209         };
1210
1211         picker.enabledDates = function (dates) {
1212             if (arguments.length === 0) {
1213                 return (options.enabledDates ? $.extend({}, options.enabledDates) : options.enabledDates);
1214             }
1215
1216             if (!dates) {
1217                 options.enabledDates = false;
1218                 update();
1219                 return picker;
1220             }
1221             if (!(dates instanceof Array)) {
1222                 throw new TypeError('enabledDates() expects an array parameter');
1223             }
1224             options.enabledDates = indexGivenDates(dates);
1225             options.disabledDates = false;
1226             update();
1227             return picker;
1228         };
1229
1230         picker.daysOfWeekDisabled = function (daysOfWeekDisabled) {
1231             if (arguments.length === 0) {
1232                 return options.daysOfWeekDisabled.splice(0);
1233             }
1234
1235             if (!(daysOfWeekDisabled instanceof Array)) {
1236                 throw new TypeError('daysOfWeekDisabled() expects an array parameter');
1237             }
1238             options.daysOfWeekDisabled = daysOfWeekDisabled.reduce(function (previousValue, currentValue) {
1239                 currentValue = parseInt(currentValue, 10);
1240                 if (currentValue > 6 || currentValue < 0 || isNaN(currentValue)) {
1241                     return previousValue;
1242                 }
1243                 if (previousValue.indexOf(currentValue) === -1) {
1244                     previousValue.push(currentValue);
1245                 }
1246                 return previousValue;
1247             }, []).sort();
1248             update();
1249             return picker;
1250         };
1251
1252         picker.maxDate = function (date) {
1253             if (arguments.length === 0) {
1254                 return options.maxDate ? options.maxDate.clone() : options.maxDate;
1255             }
1256
1257             if ((typeof date === 'boolean') && date === false) {
1258                 options.maxDate = false;
1259                 update();
1260                 return picker;
1261             }
1262
1263             var parsedDate = parseInputDate(date);
1264
1265             if (!parsedDate.isValid()) {
1266                 throw new TypeError('maxDate() Could not parse date parameter: ' + date);
1267             }
1268             if (options.minDate && parsedDate.isBefore(options.minDate)) {
1269                 throw new TypeError('maxDate() date parameter is before options.minDate: ' + parsedDate.format(actualFormat));
1270             }
1271             options.maxDate = parsedDate;
1272             if (options.maxDate.isBefore(date)) {
1273                 setValue(options.maxDate);
1274             }
1275             update();
1276             return picker;
1277         };
1278
1279         picker.minDate = function (date) {
1280             if (arguments.length === 0) {
1281                 return options.minDate ? options.minDate.clone() : options.minDate;
1282             }
1283
1284             if ((typeof date === 'boolean') && date === false) {
1285                 options.minDate = false;
1286                 update();
1287                 return picker;
1288             }
1289
1290             var parsedDate = parseInputDate(date);
1291
1292             if (!parsedDate.isValid()) {
1293                 throw new TypeError('minDate() Could not parse date parameter: ' + date);
1294             }
1295             if (options.maxDate && parsedDate.isAfter(options.maxDate)) {
1296                 throw new TypeError('minDate() date parameter is after options.maxDate: ' + parsedDate.format(actualFormat));
1297             }
1298             options.minDate = parsedDate;
1299             if (options.minDate.isAfter(date)) {
1300                 setValue(options.minDate);
1301             }
1302             update();
1303             return picker;
1304         };
1305
1306         picker.defaultDate = function (defaultDate) {
1307             if (arguments.length === 0) {
1308                 return options.defaultDate ? options.defaultDate.clone() : options.defaultDate;
1309             }
1310             if (!defaultDate) {
1311                 options.defaultDate = false;
1312                 return picker;
1313             }
1314             var parsedDate = parseInputDate(defaultDate);
1315             if (!parsedDate.isValid()) {
1316                 throw new TypeError('defaultDate() Could not parse date parameter: ' + defaultDate);
1317             }
1318             if (!isValid(parsedDate)) {
1319                 throw new TypeError('defaultDate() date passed is invalid according to component setup validations');
1320             }
1321
1322             options.defaultDate = parsedDate;
1323
1324             if (options.defaultDate && input.val().trim() === '') {
1325                 setValue(options.defaultDate);
1326             }
1327             return picker;
1328         };
1329
1330         picker.locale = function (locale) {
1331             if (arguments.length === 0) {
1332                 return options.locale;
1333             }
1334
1335             if (!moment.localeData(locale)) {
1336                 throw new TypeError('locale() locale ' + locale + ' is not loaded from moment locales!');
1337             }
1338
1339             options.locale = locale;
1340             date.locale(options.locale);
1341             viewDate.locale(options.locale);
1342
1343             if (actualFormat) {
1344                 initFormatting(); // reinit formatting
1345             }
1346             if (widget) {
1347                 hide();
1348                 show();
1349             }
1350             return picker;
1351         };
1352
1353         picker.stepping = function (stepping) {
1354             if (arguments.length === 0) {
1355                 return options.stepping;
1356             }
1357
1358             stepping = parseInt(stepping, 10);
1359             if (isNaN(stepping) || stepping < 1) {
1360                 stepping = 1;
1361             }
1362             options.stepping = stepping;
1363             return picker;
1364         };
1365
1366         picker.useCurrent = function (useCurrent) {
1367             var useCurrentOptions = ['year', 'month', 'day', 'hour', 'minute'];
1368             if (arguments.length === 0) {
1369                 return options.useCurrent;
1370             }
1371
1372             if ((typeof useCurrent !== 'boolean') && (typeof useCurrent !== 'string')) {
1373                 throw new TypeError('useCurrent() expects a boolean or string parameter');
1374             }
1375             if (typeof useCurrent === 'string' && useCurrentOptions.indexOf(useCurrent.toLowerCase()) === -1) {
1376                 throw new TypeError('useCurrent() expects a string parameter of ' + useCurrentOptions.join(', '));
1377             }
1378             options.useCurrent = useCurrent;
1379             return picker;
1380         };
1381
1382         picker.collapse = function (collapse) {
1383             if (arguments.length === 0) {
1384                 return options.collapse;
1385             }
1386
1387             if (typeof collapse !== 'boolean') {
1388                 throw new TypeError('collapse() expects a boolean parameter');
1389             }
1390             if (options.collapse === collapse) {
1391                 return picker;
1392             }
1393             options.collapse = collapse;
1394             if (widget) {
1395                 hide();
1396                 show();
1397             }
1398             return picker;
1399         };
1400
1401         picker.icons = function (icons) {
1402             if (arguments.length === 0) {
1403                 return $.extend({}, options.icons);
1404             }
1405
1406             if (!(icons instanceof Object)) {
1407                 throw new TypeError('icons() expects parameter to be an Object');
1408             }
1409             $.extend(options.icons, icons);
1410             if (widget) {
1411                 hide();
1412                 show();
1413             }
1414             return picker;
1415         };
1416
1417         picker.useStrict = function (useStrict) {
1418             if (arguments.length === 0) {
1419                 return options.useStrict;
1420             }
1421
1422             if (typeof useStrict !== 'boolean') {
1423                 throw new TypeError('useStrict() expects a boolean parameter');
1424             }
1425             options.useStrict = useStrict;
1426             return picker;
1427         };
1428
1429         picker.sideBySide = function (sideBySide) {
1430             if (arguments.length === 0) {
1431                 return options.sideBySide;
1432             }
1433
1434             if (typeof sideBySide !== 'boolean') {
1435                 throw new TypeError('sideBySide() expects a boolean parameter');
1436             }
1437             options.sideBySide = sideBySide;
1438             if (widget) {
1439                 hide();
1440                 show();
1441             }
1442             return picker;
1443         };
1444
1445         picker.viewMode = function (newViewMode) {
1446             if (arguments.length === 0) {
1447                 return options.viewMode;
1448             }
1449
1450             if (typeof newViewMode !== 'string') {
1451                 throw new TypeError('viewMode() expects a string parameter');
1452             }
1453
1454             if (viewModes.indexOf(newViewMode) === -1) {
1455                 throw new TypeError('viewMode() parameter must be one of (' + viewModes.join(', ') + ') value');
1456             }
1457
1458             options.viewMode = newViewMode;
1459             currentViewMode = Math.max(viewModes.indexOf(newViewMode), minViewModeNumber);
1460
1461             showMode();
1462             return picker;
1463         };
1464
1465         picker.toolbarPlacement = function (toolbarPlacement) {
1466             if (arguments.length === 0) {
1467                 return options.toolbarPlacement;
1468             }
1469
1470             if (typeof toolbarPlacement !== 'string') {
1471                 throw new TypeError('toolbarPlacement() expects a string parameter');
1472             }
1473             if (toolbarPlacements.indexOf(toolbarPlacement) === -1) {
1474                 throw new TypeError('toolbarPlacement() parameter must be one of (' + toolbarPlacements.join(', ') + ') value');
1475             }
1476             options.toolbarPlacement = toolbarPlacement;
1477
1478             if (widget) {
1479                 hide();
1480                 show();
1481             }
1482             return picker;
1483         };
1484
1485         picker.widgetPositioning = function (widgetPositioning) {
1486             if (arguments.length === 0) {
1487                 return $.extend({}, options.widgetPositioning);
1488             }
1489
1490             if (({}).toString.call(widgetPositioning) !== '[object Object]') {
1491                 throw new TypeError('widgetPositioning() expects an object variable');
1492             }
1493             if (widgetPositioning.horizontal) {
1494                 if (typeof widgetPositioning.horizontal !== 'string') {
1495                     throw new TypeError('widgetPositioning() horizontal variable must be a string');
1496                 }
1497                 widgetPositioning.horizontal = widgetPositioning.horizontal.toLowerCase();
1498                 if (horizontalModes.indexOf(widgetPositioning.horizontal) === -1) {
1499                     throw new TypeError('widgetPositioning() expects horizontal parameter to be one of (' + horizontalModes.join(', ') + ')');
1500                 }
1501                 options.widgetPositioning.horizontal = widgetPositioning.horizontal;
1502             }
1503             if (widgetPositioning.vertical) {
1504                 if (typeof widgetPositioning.vertical !== 'string') {
1505                     throw new TypeError('widgetPositioning() vertical variable must be a string');
1506                 }
1507                 widgetPositioning.vertical = widgetPositioning.vertical.toLowerCase();
1508                 if (verticalModes.indexOf(widgetPositioning.vertical) === -1) {
1509                     throw new TypeError('widgetPositioning() expects vertical parameter to be one of (' + verticalModes.join(', ') + ')');
1510                 }
1511                 options.widgetPositioning.vertical = widgetPositioning.vertical;
1512             }
1513             update();
1514             return picker;
1515         };
1516
1517         picker.calendarWeeks = function (showCalendarWeeks) {
1518             if (arguments.length === 0) {
1519                 return options.calendarWeeks;
1520             }
1521
1522             if (typeof showCalendarWeeks !== 'boolean') {
1523                 throw new TypeError('calendarWeeks() expects parameter to be a boolean value');
1524             }
1525
1526             options.calendarWeeks = showCalendarWeeks;
1527             update();
1528             return picker;
1529         };
1530
1531         picker.showTodayButton = function (showTodayButton) {
1532             if (arguments.length === 0) {
1533                 return options.showTodayButton;
1534             }
1535
1536             if (typeof showTodayButton !== 'boolean') {
1537                 throw new TypeError('showTodayButton() expects a boolean parameter');
1538             }
1539
1540             options.showTodayButton = showTodayButton;
1541             if (widget) {
1542                 hide();
1543                 show();
1544             }
1545             return picker;
1546         };
1547
1548         picker.showClear = function (showClear) {
1549             if (arguments.length === 0) {
1550                 return options.showClear;
1551             }
1552
1553             if (typeof showClear !== 'boolean') {
1554                 throw new TypeError('showClear() expects a boolean parameter');
1555             }
1556
1557             options.showClear = showClear;
1558             if (widget) {
1559                 hide();
1560                 show();
1561             }
1562             return picker;
1563         };
1564
1565         picker.widgetParent = function (widgetParent) {
1566             if (arguments.length === 0) {
1567                 return options.widgetParent;
1568             }
1569
1570             if (typeof widgetParent === 'string') {
1571                 widgetParent = $(widgetParent);
1572             }
1573
1574             if (widgetParent !== null && (typeof widgetParent !== 'string' && !(widgetParent instanceof jQuery))) {
1575                 throw new TypeError('widgetParent() expects a string or a jQuery object parameter');
1576             }
1577
1578             options.widgetParent = widgetParent;
1579             if (widget) {
1580                 hide();
1581                 show();
1582             }
1583             return picker;
1584         };
1585
1586         picker.keepOpen = function (keepOpen) {
1587             if (arguments.length === 0) {
1588                 return options.format;
1589             }
1590
1591             if (typeof keepOpen !== 'boolean') {
1592                 throw new TypeError('keepOpen() expects a boolean parameter');
1593             }
1594
1595             options.keepOpen = keepOpen;
1596             return picker;
1597         };
1598
1599         // initializing element and component attributes
1600         if (element.is('input')) {
1601             input = element;
1602         } else {
1603             input = element.find('.datepickerinput');
1604             if (input.size() === 0) {
1605                 input = element.find('input');
1606             } else if (!input.is('input')) {
1607                 throw new Error('CSS class "datepickerinput" cannot be applied to non input element');
1608             }
1609         }
1610
1611         if (element.hasClass('input-group')) {
1612             // in case there is more then one 'input-group-addon' Issue #48
1613             if (element.find('.datepickerbutton').size() === 0) {
1614                 component = element.find('[class^="input-group-"]');
1615             } else {
1616                 component = element.find('.datepickerbutton');
1617             }
1618         }
1619
1620         if (!input.is('input')) {
1621             throw new Error('Could not initialize DateTimePicker without an input element');
1622         }
1623
1624         $.extend(true, options, dataToOptions());
1625
1626         picker.options(options);
1627
1628         initFormatting();
1629
1630         attachDatePickerElementEvents();
1631
1632         if (input.prop('disabled')) {
1633             picker.disable();
1634         }
1635
1636         if (input.val().trim().length !== 0) {
1637             setValue(parseInputDate(input.val().trim()));
1638         } else if (options.defaultDate) {
1639             setValue(options.defaultDate);
1640         }
1641
1642         return picker;
1643     };
1644
1645     /********************************************************************************
1646      *
1647      * jQuery plugin constructor and defaults object
1648      *
1649      ********************************************************************************/
1650
1651     $.fn.datetimepicker = function (options) {
1652         return this.each(function () {
1653             var $this = $(this);
1654             if (!$this.data('DateTimePicker')) {
1655                 // create a private copy of the defaults object
1656                 options = $.extend(true, {}, $.fn.datetimepicker.defaults, options);
1657                 $this.data('DateTimePicker', dateTimePicker($this, options));
1658             }
1659         });
1660     };
1661
1662     $.fn.datetimepicker.defaults = {
1663         format: false,
1664         dayViewHeaderFormat: 'MMMM YYYY',
1665         extraFormats: false,
1666         stepping: 1,
1667         minDate: false,
1668         maxDate: false,
1669         useCurrent: true,
1670         collapse: true,
1671         locale: moment.locale(),
1672         defaultDate: false,
1673         disabledDates: false,
1674         enabledDates: false,
1675         icons: {
1676             time: 'glyphicon glyphicon-time',
1677             date: 'glyphicon glyphicon-calendar',
1678             up: 'glyphicon glyphicon-chevron-up',
1679             down: 'glyphicon glyphicon-chevron-down',
1680             previous: 'glyphicon glyphicon-chevron-left',
1681             next: 'glyphicon glyphicon-chevron-right',
1682             today: 'glyphicon glyphicon-screenshot',
1683             clear: 'glyphicon glyphicon-trash'
1684         },
1685         useStrict: false,
1686         sideBySide: false,
1687         daysOfWeekDisabled: [],
1688         calendarWeeks: false,
1689         viewMode: 'days',
1690         toolbarPlacement: 'default',
1691         showTodayButton: false,
1692         showClear: false,
1693         widgetPositioning: {
1694             horizontal: 'auto',
1695             vertical: 'auto'
1696         },
1697         widgetParent: null,
1698         keepOpen: false
1699     };
1700 }));