3 =========================================================
5 https://github.com/Eonasdan/bootstrap-datetimepicker
6 =========================================================
9 Copyright (c) 2015 Jonathan Peterson
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:
18 The above copyright notice and this permission notice shall be included in
19 all copies or substantial portions of the Software.
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
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'));
37 // Neither AMD nor CommonJS used. Use global variables.
39 throw 'bootstrap-datetimepicker requires jQuery to be loaded first';
42 throw 'bootstrap-datetimepicker requires Moment.js to be loaded first';
44 factory(jQuery, moment);
46 }(function ($, moment) {
49 throw new Error('bootstrap-datetimepicker requires Moment.js to be loaded first');
52 var dateTimePicker = function (element, options) {
55 viewDate = date.clone(),
61 minViewModeNumber = 0,
82 viewModes = ['days', 'months', 'years'],
83 verticalModes = ['top', 'bottom', 'auto'],
84 horizontalModes = ['left', 'right', 'auto'],
85 toolbarPlacements = ['default', 'top', 'bottom'],
87 /********************************************************************************
91 ********************************************************************************/
92 isEnabled = function (granularity) {
93 if (typeof granularity !== 'string' || granularity.length > 1) {
94 throw new TypeError('isEnabled expects a single character string parameter');
96 switch (granularity) {
98 return actualFormat.indexOf('Y') !== -1;
100 return actualFormat.indexOf('M') !== -1;
102 return actualFormat.toLowerCase().indexOf('d') !== -1;
105 return actualFormat.toLowerCase().indexOf('h') !== -1;
107 return actualFormat.indexOf('m') !== -1;
109 return actualFormat.indexOf('s') !== -1;
115 hasTime = function () {
116 return (isEnabled('h') || isEnabled('m') || isEnabled('s'));
119 hasDate = function () {
120 return (isEnabled('y') || isEnabled('M') || isEnabled('d'));
123 getDatePickerTemplate = function () {
124 var headTemplate = $('<thead>')
126 .append($('<th>').addClass('prev').attr('data-action', 'previous')
127 .append($('<span>').addClass(options.icons.previous))
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))
134 contTemplate = $('<tbody>')
136 .append($('<td>').attr('colspan', (options.calendarWeeks ? '8' : '7')))
140 $('<div>').addClass('datepicker-days')
141 .append($('<table>').addClass('table-condensed')
142 .append(headTemplate)
143 .append($('<tbody>'))
145 $('<div>').addClass('datepicker-months')
146 .append($('<table>').addClass('table-condensed')
147 .append(headTemplate.clone())
148 .append(contTemplate.clone())
150 $('<div>').addClass('datepicker-years')
151 .append($('<table>').addClass('table-condensed')
152 .append(headTemplate.clone())
153 .append(contTemplate.clone())
158 getTimePickerMainTemplate = function () {
159 var topRow = $('<tr>'),
160 middleRow = $('<tr>'),
161 bottomRow = $('<tr>');
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))));
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'));
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))));
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'));
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))));
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'));
211 return $('<div>').addClass('timepicker-picker')
212 .append($('<table>').addClass('table-condensed')
213 .append([topRow, middleRow, bottomRow]));
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()];
225 if (isEnabled('h')) {
228 if (isEnabled('m')) {
229 ret.push(minutesView);
231 if (isEnabled('s')) {
232 ret.push(secondsView);
238 getToolbar = function () {
240 if (options.showTodayButton) {
241 row.push($('<td>').append($('<a>').attr('data-action', 'today').append($('<span>').addClass(options.icons.today))));
243 if (!options.sideBySide && hasDate() && hasTime()) {
244 row.push($('<td>').append($('<a>').attr('data-action', 'togglePicker').append($('<span>').addClass(options.icons.time))));
246 if (options.showClear) {
247 row.push($('<td>').append($('<a>').attr('data-action', 'clear').append($('<span>').addClass(options.icons.clear))));
249 return $('<table>').addClass('table-condensed').append($('<tbody>').append($('<tr>').append(row)));
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());
260 template.addClass('usetwentyfour');
262 if (options.sideBySide && hasDate() && hasTime()) {
263 template.addClass('timepicker-sbs');
265 $('<div>').addClass('row')
266 .append(dateView.addClass('col-sm-6'))
267 .append(timeView.addClass('col-sm-6'))
269 template.append(toolbar);
273 if (options.toolbarPlacement === 'top') {
274 content.append(toolbar);
277 content.append($('<li>').addClass((options.collapse && hasTime() ? 'collapse in' : '')).append(dateView));
279 if (options.toolbarPlacement === 'default') {
280 content.append(toolbar);
283 content.append($('<li>').addClass((options.collapse && hasDate() ? 'collapse' : '')).append(timeView));
285 if (options.toolbarPlacement === 'bottom') {
286 content.append(toolbar);
288 return template.append(content);
291 dataToOptions = function () {
292 var eData = element.data(),
295 if (eData.dateOptions && eData.dateOptions instanceof Object) {
296 dataOptions = $.extend(true, dataOptions, eData.dateOptions);
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];
308 place = function () {
309 var offset = (component || element).position(),
310 vertical = options.widgetPositioning.vertical,
311 horizontal = options.widgetPositioning.horizontal,
314 if (options.widgetParent) {
315 parent = options.widgetParent.append(widget);
316 } else if (element.is('input')) {
317 parent = element.parent().append(widget);
320 element.children().first().after(widget);
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) {
333 // Left and right logic
334 if (horizontal === 'auto') {
335 if (parent.width() < offset.left + widget.outerWidth()) {
336 horizontal = 'right';
342 if (vertical === 'top') {
343 widget.addClass('top').removeClass('bottom');
345 widget.addClass('bottom').removeClass('top');
348 if (horizontal === 'right') {
349 widget.addClass('pull-right');
351 widget.removeClass('pull-right');
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';
361 if (parent.length === 0) {
362 throw new Error('datetimepicker component should be placed within a relative positioned container');
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')
373 notifyEvent = function (e) {
374 if (e.type === 'dp.change' && ((e.date && e.date.isSame(e.oldDate)) || (!e.date && !e.oldDate))) {
380 showMode = function (dir) {
385 currentViewMode = Math.max(minViewModeNumber, Math.min(2, currentViewMode + dir));
387 widget.find('.datepicker > div').hide().filter('.datepicker-' + datePickerModes[currentViewMode].clsName).show();
390 fillDow = function () {
392 currentDate = viewDate.clone().startOf('w');
394 if (options.calendarWeeks === true) {
395 row.append($('<th>').addClass('cw').text('#'));
398 while (currentDate.isBefore(viewDate.clone().endOf('w'))) {
399 row.append($('<th>').addClass('dow').text(currentDate.format('dd')));
400 currentDate.add(1, 'd');
402 widget.find('.datepicker-days thead').append(row);
405 isInDisabledDates = function (date) {
406 if (!options.disabledDates) {
409 return options.disabledDates[date.format('YYYY-MM-DD')] === true;
412 isInEnabledDates = function (date) {
413 if (!options.enabledDates) {
416 return options.enabledDates[date.format('YYYY-MM-DD')] === true;
419 isValid = function (targetMoment, granularity) {
420 if (!targetMoment.isValid()) {
423 if (options.disabledDates && isInDisabledDates(targetMoment)) {
426 if (options.enabledDates && isInEnabledDates(targetMoment)) {
429 if (options.minDate && targetMoment.isBefore(options.minDate, granularity)) {
432 if (options.maxDate && targetMoment.isAfter(options.maxDate, granularity)) {
435 if (granularity === 'd' && options.daysOfWeekDisabled.indexOf(targetMoment.day()) !== -1) {
441 fillMonths = function () {
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');
448 widget.find('.datepicker-months td').empty().append(spans);
451 updateMonths = function () {
452 var monthsView = widget.find('.datepicker-months'),
453 monthsViewHeader = monthsView.find('th'),
454 months = monthsView.find('tbody').find('span');
456 monthsView.find('.disabled').removeClass('disabled');
458 if (!isValid(viewDate.clone().subtract(1, 'y'), 'y')) {
459 monthsViewHeader.eq(0).addClass('disabled');
462 monthsViewHeader.eq(1).text(viewDate.year());
464 if (!isValid(viewDate.clone().add(1, 'y'), 'y')) {
465 monthsViewHeader.eq(2).addClass('disabled');
468 months.removeClass('active');
469 if (date.isSame(viewDate, 'y')) {
470 months.eq(date.month()).addClass('active');
473 months.each(function (index) {
474 if (!isValid(viewDate.clone().month(index), 'M')) {
475 $(this).addClass('disabled');
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'),
487 yearsView.find('.disabled').removeClass('disabled');
489 if (options.minDate && options.minDate.isAfter(startYear, 'y')) {
490 yearsViewHeader.eq(0).addClass('disabled');
493 yearsViewHeader.eq(1).text(startYear.year() + '-' + endYear.year());
495 if (options.maxDate && options.maxDate.isBefore(endYear, 'y')) {
496 yearsViewHeader.eq(2).addClass('disabled');
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');
504 yearsView.find('td').html(html);
507 fillDate = function () {
508 var daysView = widget.find('.datepicker-days'),
509 daysViewHeader = daysView.find('th'),
519 daysView.find('.disabled').removeClass('disabled');
520 daysViewHeader.eq(1).text(viewDate.format(options.dayViewHeaderFormat));
522 if (!isValid(viewDate.clone().subtract(1, 'M'), 'M')) {
523 daysViewHeader.eq(0).addClass('disabled');
525 if (!isValid(viewDate.clone().add(1, 'M'), 'M')) {
526 daysViewHeader.eq(2).addClass('disabled');
529 currentDate = viewDate.clone().startOf('M').startOf('week');
531 while (!viewDate.clone().endOf('M').endOf('w').isBefore(currentDate, 'd')) {
532 if (currentDate.weekday() === 0) {
534 if (options.calendarWeeks) {
535 row.append('<td class="cw">' + currentDate.week() + '</td>');
540 if (currentDate.isBefore(viewDate, 'M')) {
543 if (currentDate.isAfter(viewDate, 'M')) {
546 if (currentDate.isSame(date, 'd') && !unset) {
547 clsName += ' active';
549 if (!isValid(currentDate, 'd')) {
550 clsName += ' disabled';
552 if (currentDate.isSame(moment(), 'd')) {
555 if (currentDate.day() === 0 || currentDate.day() === 6) {
556 clsName += ' weekend';
558 row.append('<td data-action="selectDay" class="day' + clsName + '">' + currentDate.date() + '</td>');
559 currentDate.add(1, 'd');
562 daysView.find('tbody').empty().append(html);
569 fillHours = function () {
570 var table = widget.find('.timepicker-hours table'),
571 currentHour = viewDate.clone().startOf('d'),
575 if (viewDate.hour() > 11 && !use24Hours) {
576 currentHour.hour(12);
578 while (currentHour.isSame(viewDate, 'd') && (use24Hours || (viewDate.hour() < 12 && currentHour.hour() < 12) || viewDate.hour() > 11)) {
579 if (currentHour.hour() % 4 === 0) {
583 row.append('<td data-action="selectHour" class="hour' + (!isValid(currentHour, 'h') ? ' disabled' : '') + '">' + currentHour.format(use24Hours ? 'HH' : 'hh') + '</td>');
584 currentHour.add(1, 'h');
586 table.empty().append(html);
589 fillMinutes = function () {
590 var table = widget.find('.timepicker-minutes table'),
591 currentMinute = viewDate.clone().startOf('h'),
594 step = options.stepping === 1 ? 5 : options.stepping;
596 while (viewDate.isSame(currentMinute, 'h')) {
597 if (currentMinute.minute() % (step * 4) === 0) {
601 row.append('<td data-action="selectMinute" class="minute' + (!isValid(currentMinute, 'm') ? ' disabled' : '') + '">' + currentMinute.format('mm') + '</td>');
602 currentMinute.add(step, 'm');
604 table.empty().append(html);
607 fillSeconds = function () {
608 var table = widget.find('.timepicker-seconds table'),
609 currentSecond = viewDate.clone().startOf('m'),
613 while (viewDate.isSame(currentSecond, 'm')) {
614 if (currentSecond.second() % 20 === 0) {
618 row.append('<td data-action="selectSecond" class="second' + (!isValid(currentSecond, 's') ? ' disabled' : '') + '">' + currentSecond.format('ss') + '</td>');
619 currentSecond.add(5, 's');
622 table.empty().append(html);
625 fillTime = function () {
626 var timeComponents = widget.find('.timepicker span[data-time-component]');
628 widget.find('.timepicker [data-action=togglePeriod]').text(date.format('A'));
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'));
639 update = function () {
647 setValue = function (targetMoment) {
648 var oldDate = unset ? null : date;
650 // case of calling setValue(null or false)
654 element.data('date', '');
664 targetMoment = targetMoment.clone().locale(options.locale);
666 if (options.stepping !== 1) {
667 targetMoment.minutes((Math.round(targetMoment.minutes() / options.stepping) * options.stepping) % 60).seconds(0);
670 if (isValid(targetMoment)) {
672 viewDate = date.clone();
673 input.val(date.format(actualFormat));
674 element.data('date', date.format(actualFormat));
683 input.val(unset ? '' : date.format(actualFormat));
692 var transitioning = false;
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;
707 if (component && component.hasClass('btn')) {
708 component.toggleClass('active');
712 $(window).off('resize', place);
713 widget.off('click', '[data-action]');
714 widget.off('mousedown', false);
726 /********************************************************************************
728 * Widget UI interaction functions
730 ********************************************************************************/
733 viewDate.add(datePickerModes[currentViewMode].navStep, datePickerModes[currentViewMode].navFnc);
737 previous: function () {
738 viewDate.subtract(datePickerModes[currentViewMode].navStep, datePickerModes[currentViewMode].navFnc);
742 pickerSwitch: function () {
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()));
757 selectYear: function (e) {
758 var year = parseInt($(e.target).text(), 10) || 0;
760 if (currentViewMode === minViewModeNumber) {
761 setValue(date.clone().year(viewDate.year()));
768 selectDay: function (e) {
769 var day = viewDate.clone();
770 if ($(e.target).is('.old')) {
771 day.subtract(1, 'M');
773 if ($(e.target).is('.new')) {
776 setValue(day.date(parseInt($(e.target).text(), 10)));
777 if (!hasTime() && !options.keepOpen) {
782 incrementHours: function () {
783 setValue(date.clone().add(1, 'h'));
786 incrementMinutes: function () {
787 setValue(date.clone().add(options.stepping, 'm'));
790 incrementSeconds: function () {
791 setValue(date.clone().add(1, 's'));
794 decrementHours: function () {
795 setValue(date.clone().subtract(1, 'h'));
798 decrementMinutes: function () {
799 setValue(date.clone().subtract(options.stepping, 'm'));
802 decrementSeconds: function () {
803 setValue(date.clone().subtract(1, 's'));
806 togglePeriod: function () {
807 setValue(date.clone().add((date.hours() >= 12) ? -12 : 12, 'h'));
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)'),
817 if (expanded && expanded.length) {
818 collapseData = expanded.data('collapse');
819 if (collapseData && collapseData.transitioning) {
822 expanded.collapse('hide');
823 closed.collapse('show');
824 if ($this.is('span')) {
825 $this.toggleClass(options.icons.time + ' ' + options.icons.date);
827 $this.find('span').toggleClass(options.icons.time + ' ' + options.icons.date);
830 // NOTE: uncomment if toggled state will be restored in show()
832 // component.find('span').toggleClass(options.icons.time + ' ' + options.icons.date);
837 showPicker: function () {
838 widget.find('.timepicker > div:not(.timepicker-picker)').hide();
839 widget.find('.timepicker .timepicker-picker').show();
842 showHours: function () {
843 widget.find('.timepicker .timepicker-picker').hide();
844 widget.find('.timepicker .timepicker-hours').show();
847 showMinutes: function () {
848 widget.find('.timepicker .timepicker-picker').hide();
849 widget.find('.timepicker .timepicker-minutes').show();
852 showSeconds: function () {
853 widget.find('.timepicker .timepicker-picker').hide();
854 widget.find('.timepicker .timepicker-seconds').show();
857 selectHour: function (e) {
858 var hour = parseInt($(e.target).text(), 10);
861 if (date.hours() >= 12) {
871 setValue(date.clone().hours(hour));
872 actions.showPicker.call(picker);
875 selectMinute: function (e) {
876 setValue(date.clone().minutes(parseInt($(e.target).text(), 10)));
877 actions.showPicker.call(picker);
880 selectSecond: function (e) {
881 setValue(date.clone().seconds(parseInt($(e.target).text(), 10)));
882 actions.showPicker.call(picker);
894 doAction = function (e) {
895 if ($(e.currentTarget).is('.disabled')) {
898 actions[$(e.currentTarget).data('action')].apply(picker, arguments);
904 useCurrentGranularity = {
905 'year': function (m) {
906 return m.month(0).date(1).hours(0).seconds(0).minutes(0);
908 'month': function (m) {
909 return m.date(1).hours(0).seconds(0).minutes(0);
911 'day': function (m) {
912 return m.hours(0).seconds(0).minutes(0);
914 'hour': function (m) {
915 return m.seconds(0).minutes(0);
917 'minute': function (m) {
922 if (input.prop('disabled') || input.prop('readonly') || widget) {
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);
930 setValue(currentMoment);
933 widget = getTemplate();
938 widget.find('.timepicker-hours').hide();
939 widget.find('.timepicker-minutes').hide();
940 widget.find('.timepicker-seconds').hide();
945 $(window).on('resize', place);
946 widget.on('click', '[data-action]', doAction); // this handles clicks on the widget
947 widget.on('mousedown', false);
949 if (component && component.hasClass('btn')) {
950 component.toggleClass('active');
955 if (!input.is(':focus')) {
965 toggle = function () {
966 return (widget ? hide() : show());
969 parseInputDate = function (date) {
970 if (moment.isMoment(date) || date instanceof Date) {
973 date = moment(date, parseFormats, options.useStrict);
975 date.locale(options.locale);
979 keydown = function (e) {
980 if (e.keyCode === 27) { // allow escape to hide picker
985 change = function (e) {
986 var val = $(e.target).val().trim(),
987 parsedDate = val ? parseInputDate(val) : null;
988 setValue(parsedDate);
989 e.stopImmediatePropagation();
993 attachDatePickerElementEvents = function () {
1000 if (element.is('input')) {
1004 } else if (component) {
1005 component.on('click', toggle);
1006 component.on('mousedown', false);
1010 detachDatePickerElementEvents = function () {
1017 if (element.is('input')) {
1021 } else if (component) {
1022 component.off('click', toggle);
1023 component.off('mousedown', false);
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;
1038 return (Object.keys(givenDatesIndexed).length) ? givenDatesIndexed : false;
1041 initFormatting = function () {
1042 var format = options.format || 'L LT';
1044 actualFormat = format.replace(/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g, function (input) {
1045 return date.localeData().longDateFormat(input) || input;
1048 parseFormats = options.extraFormats ? options.extraFormats.slice() : [];
1049 if (parseFormats.indexOf(format) < 0 && parseFormats.indexOf(actualFormat) < 0) {
1050 parseFormats.push(actualFormat);
1053 use24Hours = (actualFormat.toLowerCase().indexOf('a') < 1 && actualFormat.indexOf('h') < 1);
1055 if (isEnabled('y')) {
1056 minViewModeNumber = 2;
1058 if (isEnabled('M')) {
1059 minViewModeNumber = 1;
1061 if (isEnabled('d')) {
1062 minViewModeNumber = 0;
1065 currentViewMode = Math.max(minViewModeNumber, currentViewMode);
1072 /********************************************************************************
1074 * Public API functions
1075 * =====================
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.
1081 ********************************************************************************/
1082 picker.destroy = function () {
1084 detachDatePickerElementEvents();
1085 element.removeData('DateTimePicker');
1086 element.removeData('date');
1089 picker.toggle = toggle;
1095 picker.disable = function () {
1097 if (component && component.hasClass('btn')) {
1098 component.addClass('disabled');
1100 input.prop('disabled', true);
1104 picker.enable = function () {
1105 if (component && component.hasClass('btn')) {
1106 component.removeClass('disabled');
1108 input.prop('disabled', false);
1112 picker.options = function (newOptions) {
1113 if (arguments.length === 0) {
1114 return $.extend(true, {}, options);
1117 if (!(newOptions instanceof Object)) {
1118 throw new TypeError('options() options parameter should be an object');
1120 $.extend(true, options, newOptions);
1121 $.each(options, function (key, value) {
1122 if (picker[key] !== undefined) {
1125 throw new TypeError('option ' + key + ' is not recognized!');
1131 picker.date = function (newDate) {
1132 if (arguments.length === 0) {
1136 return date.clone();
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]');
1143 setValue(newDate === null ? null : parseInputDate(newDate));
1147 picker.format = function (newFormat) {
1148 if (arguments.length === 0) {
1149 return options.format;
1152 if ((typeof newFormat !== 'string') && ((typeof newFormat !== 'boolean') || (newFormat !== false))) {
1153 throw new TypeError('format() expects a sting or boolean:false parameter ' + newFormat);
1156 options.format = newFormat;
1158 initFormatting(); // reinit formatting
1163 picker.dayViewHeaderFormat = function (newFormat) {
1164 if (arguments.length === 0) {
1165 return options.dayViewHeaderFormat;
1168 if (typeof newFormat !== 'string') {
1169 throw new TypeError('dayViewHeaderFormat() expects a string parameter');
1172 options.dayViewHeaderFormat = newFormat;
1176 picker.extraFormats = function (formats) {
1177 if (arguments.length === 0) {
1178 return options.extraFormats;
1181 if (formats !== false && !(formats instanceof Array)) {
1182 throw new TypeError('extraFormats() expects an array or false parameter');
1185 options.extraFormats = formats;
1187 initFormatting(); // reinit formatting
1192 picker.disabledDates = function (dates) {
1193 if (arguments.length === 0) {
1194 return (options.disabledDates ? $.extend({}, options.disabledDates) : options.disabledDates);
1198 options.disabledDates = false;
1202 if (!(dates instanceof Array)) {
1203 throw new TypeError('disabledDates() expects an array parameter');
1205 options.disabledDates = indexGivenDates(dates);
1206 options.enabledDates = false;
1211 picker.enabledDates = function (dates) {
1212 if (arguments.length === 0) {
1213 return (options.enabledDates ? $.extend({}, options.enabledDates) : options.enabledDates);
1217 options.enabledDates = false;
1221 if (!(dates instanceof Array)) {
1222 throw new TypeError('enabledDates() expects an array parameter');
1224 options.enabledDates = indexGivenDates(dates);
1225 options.disabledDates = false;
1230 picker.daysOfWeekDisabled = function (daysOfWeekDisabled) {
1231 if (arguments.length === 0) {
1232 return options.daysOfWeekDisabled.splice(0);
1235 if (!(daysOfWeekDisabled instanceof Array)) {
1236 throw new TypeError('daysOfWeekDisabled() expects an array parameter');
1238 options.daysOfWeekDisabled = daysOfWeekDisabled.reduce(function (previousValue, currentValue) {
1239 currentValue = parseInt(currentValue, 10);
1240 if (currentValue > 6 || currentValue < 0 || isNaN(currentValue)) {
1241 return previousValue;
1243 if (previousValue.indexOf(currentValue) === -1) {
1244 previousValue.push(currentValue);
1246 return previousValue;
1252 picker.maxDate = function (date) {
1253 if (arguments.length === 0) {
1254 return options.maxDate ? options.maxDate.clone() : options.maxDate;
1257 if ((typeof date === 'boolean') && date === false) {
1258 options.maxDate = false;
1263 var parsedDate = parseInputDate(date);
1265 if (!parsedDate.isValid()) {
1266 throw new TypeError('maxDate() Could not parse date parameter: ' + date);
1268 if (options.minDate && parsedDate.isBefore(options.minDate)) {
1269 throw new TypeError('maxDate() date parameter is before options.minDate: ' + parsedDate.format(actualFormat));
1271 options.maxDate = parsedDate;
1272 if (options.maxDate.isBefore(date)) {
1273 setValue(options.maxDate);
1279 picker.minDate = function (date) {
1280 if (arguments.length === 0) {
1281 return options.minDate ? options.minDate.clone() : options.minDate;
1284 if ((typeof date === 'boolean') && date === false) {
1285 options.minDate = false;
1290 var parsedDate = parseInputDate(date);
1292 if (!parsedDate.isValid()) {
1293 throw new TypeError('minDate() Could not parse date parameter: ' + date);
1295 if (options.maxDate && parsedDate.isAfter(options.maxDate)) {
1296 throw new TypeError('minDate() date parameter is after options.maxDate: ' + parsedDate.format(actualFormat));
1298 options.minDate = parsedDate;
1299 if (options.minDate.isAfter(date)) {
1300 setValue(options.minDate);
1306 picker.defaultDate = function (defaultDate) {
1307 if (arguments.length === 0) {
1308 return options.defaultDate ? options.defaultDate.clone() : options.defaultDate;
1311 options.defaultDate = false;
1314 var parsedDate = parseInputDate(defaultDate);
1315 if (!parsedDate.isValid()) {
1316 throw new TypeError('defaultDate() Could not parse date parameter: ' + defaultDate);
1318 if (!isValid(parsedDate)) {
1319 throw new TypeError('defaultDate() date passed is invalid according to component setup validations');
1322 options.defaultDate = parsedDate;
1324 if (options.defaultDate && input.val().trim() === '') {
1325 setValue(options.defaultDate);
1330 picker.locale = function (locale) {
1331 if (arguments.length === 0) {
1332 return options.locale;
1335 if (!moment.localeData(locale)) {
1336 throw new TypeError('locale() locale ' + locale + ' is not loaded from moment locales!');
1339 options.locale = locale;
1340 date.locale(options.locale);
1341 viewDate.locale(options.locale);
1344 initFormatting(); // reinit formatting
1353 picker.stepping = function (stepping) {
1354 if (arguments.length === 0) {
1355 return options.stepping;
1358 stepping = parseInt(stepping, 10);
1359 if (isNaN(stepping) || stepping < 1) {
1362 options.stepping = stepping;
1366 picker.useCurrent = function (useCurrent) {
1367 var useCurrentOptions = ['year', 'month', 'day', 'hour', 'minute'];
1368 if (arguments.length === 0) {
1369 return options.useCurrent;
1372 if ((typeof useCurrent !== 'boolean') && (typeof useCurrent !== 'string')) {
1373 throw new TypeError('useCurrent() expects a boolean or string parameter');
1375 if (typeof useCurrent === 'string' && useCurrentOptions.indexOf(useCurrent.toLowerCase()) === -1) {
1376 throw new TypeError('useCurrent() expects a string parameter of ' + useCurrentOptions.join(', '));
1378 options.useCurrent = useCurrent;
1382 picker.collapse = function (collapse) {
1383 if (arguments.length === 0) {
1384 return options.collapse;
1387 if (typeof collapse !== 'boolean') {
1388 throw new TypeError('collapse() expects a boolean parameter');
1390 if (options.collapse === collapse) {
1393 options.collapse = collapse;
1401 picker.icons = function (icons) {
1402 if (arguments.length === 0) {
1403 return $.extend({}, options.icons);
1406 if (!(icons instanceof Object)) {
1407 throw new TypeError('icons() expects parameter to be an Object');
1409 $.extend(options.icons, icons);
1417 picker.useStrict = function (useStrict) {
1418 if (arguments.length === 0) {
1419 return options.useStrict;
1422 if (typeof useStrict !== 'boolean') {
1423 throw new TypeError('useStrict() expects a boolean parameter');
1425 options.useStrict = useStrict;
1429 picker.sideBySide = function (sideBySide) {
1430 if (arguments.length === 0) {
1431 return options.sideBySide;
1434 if (typeof sideBySide !== 'boolean') {
1435 throw new TypeError('sideBySide() expects a boolean parameter');
1437 options.sideBySide = sideBySide;
1445 picker.viewMode = function (newViewMode) {
1446 if (arguments.length === 0) {
1447 return options.viewMode;
1450 if (typeof newViewMode !== 'string') {
1451 throw new TypeError('viewMode() expects a string parameter');
1454 if (viewModes.indexOf(newViewMode) === -1) {
1455 throw new TypeError('viewMode() parameter must be one of (' + viewModes.join(', ') + ') value');
1458 options.viewMode = newViewMode;
1459 currentViewMode = Math.max(viewModes.indexOf(newViewMode), minViewModeNumber);
1465 picker.toolbarPlacement = function (toolbarPlacement) {
1466 if (arguments.length === 0) {
1467 return options.toolbarPlacement;
1470 if (typeof toolbarPlacement !== 'string') {
1471 throw new TypeError('toolbarPlacement() expects a string parameter');
1473 if (toolbarPlacements.indexOf(toolbarPlacement) === -1) {
1474 throw new TypeError('toolbarPlacement() parameter must be one of (' + toolbarPlacements.join(', ') + ') value');
1476 options.toolbarPlacement = toolbarPlacement;
1485 picker.widgetPositioning = function (widgetPositioning) {
1486 if (arguments.length === 0) {
1487 return $.extend({}, options.widgetPositioning);
1490 if (({}).toString.call(widgetPositioning) !== '[object Object]') {
1491 throw new TypeError('widgetPositioning() expects an object variable');
1493 if (widgetPositioning.horizontal) {
1494 if (typeof widgetPositioning.horizontal !== 'string') {
1495 throw new TypeError('widgetPositioning() horizontal variable must be a string');
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(', ') + ')');
1501 options.widgetPositioning.horizontal = widgetPositioning.horizontal;
1503 if (widgetPositioning.vertical) {
1504 if (typeof widgetPositioning.vertical !== 'string') {
1505 throw new TypeError('widgetPositioning() vertical variable must be a string');
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(', ') + ')');
1511 options.widgetPositioning.vertical = widgetPositioning.vertical;
1517 picker.calendarWeeks = function (showCalendarWeeks) {
1518 if (arguments.length === 0) {
1519 return options.calendarWeeks;
1522 if (typeof showCalendarWeeks !== 'boolean') {
1523 throw new TypeError('calendarWeeks() expects parameter to be a boolean value');
1526 options.calendarWeeks = showCalendarWeeks;
1531 picker.showTodayButton = function (showTodayButton) {
1532 if (arguments.length === 0) {
1533 return options.showTodayButton;
1536 if (typeof showTodayButton !== 'boolean') {
1537 throw new TypeError('showTodayButton() expects a boolean parameter');
1540 options.showTodayButton = showTodayButton;
1548 picker.showClear = function (showClear) {
1549 if (arguments.length === 0) {
1550 return options.showClear;
1553 if (typeof showClear !== 'boolean') {
1554 throw new TypeError('showClear() expects a boolean parameter');
1557 options.showClear = showClear;
1565 picker.widgetParent = function (widgetParent) {
1566 if (arguments.length === 0) {
1567 return options.widgetParent;
1570 if (typeof widgetParent === 'string') {
1571 widgetParent = $(widgetParent);
1574 if (widgetParent !== null && (typeof widgetParent !== 'string' && !(widgetParent instanceof jQuery))) {
1575 throw new TypeError('widgetParent() expects a string or a jQuery object parameter');
1578 options.widgetParent = widgetParent;
1586 picker.keepOpen = function (keepOpen) {
1587 if (arguments.length === 0) {
1588 return options.format;
1591 if (typeof keepOpen !== 'boolean') {
1592 throw new TypeError('keepOpen() expects a boolean parameter');
1595 options.keepOpen = keepOpen;
1599 // initializing element and component attributes
1600 if (element.is('input')) {
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');
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-"]');
1616 component = element.find('.datepickerbutton');
1620 if (!input.is('input')) {
1621 throw new Error('Could not initialize DateTimePicker without an input element');
1624 $.extend(true, options, dataToOptions());
1626 picker.options(options);
1630 attachDatePickerElementEvents();
1632 if (input.prop('disabled')) {
1636 if (input.val().trim().length !== 0) {
1637 setValue(parseInputDate(input.val().trim()));
1638 } else if (options.defaultDate) {
1639 setValue(options.defaultDate);
1645 /********************************************************************************
1647 * jQuery plugin constructor and defaults object
1649 ********************************************************************************/
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));
1662 $.fn.datetimepicker.defaults = {
1664 dayViewHeaderFormat: 'MMMM YYYY',
1665 extraFormats: false,
1671 locale: moment.locale(),
1673 disabledDates: false,
1674 enabledDates: false,
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'
1687 daysOfWeekDisabled: [],
1688 calendarWeeks: false,
1690 toolbarPlacement: 'default',
1691 showTodayButton: false,
1693 widgetPositioning: {