vendor/assets/javascripts/bootstrap-datetimepicker.js in bootstrap3-datetimepicker-rails-3.1.3 vs vendor/assets/javascripts/bootstrap-datetimepicker.js in bootstrap3-datetimepicker-rails-4.0.0

- old
+ new

@@ -1,1384 +1,1700 @@ /* -//! version : 3.1.3 -========================================================= -bootstrap-datetimepicker.js -https://github.com/Eonasdan/bootstrap-datetimepicker -========================================================= -The MIT License (MIT) + //! version : 4.0.0 + ========================================================= + bootstrap-datetimejs + https://github.com/Eonasdan/bootstrap-datetimepicker + ========================================================= + The MIT License (MIT) -Copyright (c) 2014 Jonathan Peterson + Copyright (c) 2015 Jonathan Peterson -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -*/ -;(function (root, factory) { + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ +(function (factory) { 'use strict'; if (typeof define === 'function' && define.amd) { // AMD is used - Register as an anonymous module. define(['jquery', 'moment'], factory); } else if (typeof exports === 'object') { factory(require('jquery'), require('moment')); - } - else { - // Neither AMD or CommonJS used. Use global variables. + } else { + // Neither AMD nor CommonJS used. Use global variables. if (!jQuery) { - throw new Error('bootstrap-datetimepicker requires jQuery to be loaded first'); + throw 'bootstrap-datetimepicker requires jQuery to be loaded first'; } if (!moment) { - throw new Error('bootstrap-datetimepicker requires moment.js to be loaded first'); + throw 'bootstrap-datetimepicker requires Moment.js to be loaded first'; } - factory(root.jQuery, moment); + factory(jQuery, moment); } -}(this, function ($, moment) { +}(function ($, moment) { 'use strict'; - if (typeof moment === 'undefined') { - throw new Error('momentjs is required'); + if (!moment) { + throw new Error('bootstrap-datetimepicker requires Moment.js to be loaded first'); } - var dpgId = 0, + var dateTimePicker = function (element, options) { + var picker = {}, + date = moment(), + viewDate = date.clone(), + unset = true, + input, + component = false, + widget = false, + use24Hours, + minViewModeNumber = 0, + actualFormat, + parseFormats, + currentViewMode, + datePickerModes = [ + { + clsName: 'days', + navFnc: 'M', + navStep: 1 + }, + { + clsName: 'months', + navFnc: 'y', + navStep: 1 + }, + { + clsName: 'years', + navFnc: 'y', + navStep: 10 + } + ], + viewModes = ['days', 'months', 'years'], + verticalModes = ['top', 'bottom', 'auto'], + horizontalModes = ['left', 'right', 'auto'], + toolbarPlacements = ['default', 'top', 'bottom'], - DateTimePicker = function (element, options) { - var defaults = $.fn.datetimepicker.defaults, + /******************************************************************************** + * + * Private functions + * + ********************************************************************************/ + isEnabled = function (granularity) { + if (typeof granularity !== 'string' || granularity.length > 1) { + throw new TypeError('isEnabled expects a single character string parameter'); + } + switch (granularity) { + case 'y': + return actualFormat.indexOf('Y') !== -1; + case 'M': + return actualFormat.indexOf('M') !== -1; + case 'd': + return actualFormat.toLowerCase().indexOf('d') !== -1; + case 'h': + case 'H': + return actualFormat.toLowerCase().indexOf('h') !== -1; + case 'm': + return actualFormat.indexOf('m') !== -1; + case 's': + return actualFormat.indexOf('s') !== -1; + default: + return false; + } + }, - icons = { - time: 'glyphicon glyphicon-time', - date: 'glyphicon glyphicon-calendar', - up: 'glyphicon glyphicon-chevron-up', - down: 'glyphicon glyphicon-chevron-down' + hasTime = function () { + return (isEnabled('h') || isEnabled('m') || isEnabled('s')); }, - picker = this, - errored = false, - dDate, + hasDate = function () { + return (isEnabled('y') || isEnabled('M') || isEnabled('d')); + }, - init = function () { - var icon = false, localeData, rInterval; - picker.options = $.extend({}, defaults, options); - picker.options.icons = $.extend({}, icons, picker.options.icons); + getDatePickerTemplate = function () { + var headTemplate = $('<thead>') + .append($('<tr>') + .append($('<th>').addClass('prev').attr('data-action', 'previous') + .append($('<span>').addClass(options.icons.previous)) + ) + .append($('<th>').addClass('picker-switch').attr('data-action', 'pickerSwitch').attr('colspan', (options.calendarWeeks ? '6' : '5'))) + .append($('<th>').addClass('next').attr('data-action', 'next') + .append($('<span>').addClass(options.icons.next)) + ) + ), + contTemplate = $('<tbody>') + .append($('<tr>') + .append($('<td>').attr('colspan', (options.calendarWeeks ? '8' : '7'))) + ); - picker.element = $(element); + return [ + $('<div>').addClass('datepicker-days') + .append($('<table>').addClass('table-condensed') + .append(headTemplate) + .append($('<tbody>')) + ), + $('<div>').addClass('datepicker-months') + .append($('<table>').addClass('table-condensed') + .append(headTemplate.clone()) + .append(contTemplate.clone()) + ), + $('<div>').addClass('datepicker-years') + .append($('<table>').addClass('table-condensed') + .append(headTemplate.clone()) + .append(contTemplate.clone()) + ) + ]; + }, - dataToOptions(); + getTimePickerMainTemplate = function () { + var topRow = $('<tr>'), + middleRow = $('<tr>'), + bottomRow = $('<tr>'); - if (!(picker.options.pickTime || picker.options.pickDate)) { - throw new Error('Must choose at least one picker'); - } + if (isEnabled('h')) { + topRow.append($('<td>') + .append($('<a>').attr('href', '#').addClass('btn').attr('data-action', 'incrementHours') + .append($('<span>').addClass(options.icons.up)))); + middleRow.append($('<td>') + .append($('<span>').addClass('timepicker-hour').attr('data-time-component', 'hours').attr('data-action', 'showHours'))); + bottomRow.append($('<td>') + .append($('<a>').attr('href', '#').addClass('btn').attr('data-action', 'decrementHours') + .append($('<span>').addClass(options.icons.down)))); + } + if (isEnabled('m')) { + if (isEnabled('h')) { + topRow.append($('<td>').addClass('separator')); + middleRow.append($('<td>').addClass('separator').html(':')); + bottomRow.append($('<td>').addClass('separator')); + } + topRow.append($('<td>') + .append($('<a>').attr('href', '#').addClass('btn').attr('data-action', 'incrementMinutes') + .append($('<span>').addClass(options.icons.up)))); + middleRow.append($('<td>') + .append($('<span>').addClass('timepicker-minute').attr('data-time-component', 'minutes').attr('data-action', 'showMinutes'))); + bottomRow.append($('<td>') + .append($('<a>').attr('href', '#').addClass('btn').attr('data-action', 'decrementMinutes') + .append($('<span>').addClass(options.icons.down)))); + } + if (isEnabled('s')) { + if (isEnabled('m')) { + topRow.append($('<td>').addClass('separator')); + middleRow.append($('<td>').addClass('separator').html(':')); + bottomRow.append($('<td>').addClass('separator')); + } + topRow.append($('<td>') + .append($('<a>').attr('href', '#').addClass('btn').attr('data-action', 'incrementSeconds') + .append($('<span>').addClass(options.icons.up)))); + middleRow.append($('<td>') + .append($('<span>').addClass('timepicker-second').attr('data-time-component', 'seconds').attr('data-action', 'showSeconds'))); + bottomRow.append($('<td>') + .append($('<a>').attr('href', '#').addClass('btn').attr('data-action', 'decrementSeconds') + .append($('<span>').addClass(options.icons.down)))); + } - picker.id = dpgId++; - moment.locale(picker.options.language); - picker.date = moment(); - picker.unset = false; - picker.isInput = picker.element.is('input'); - picker.component = false; + if (!use24Hours) { + topRow.append($('<td>').addClass('separator')); + middleRow.append($('<td>') + .append($('<button>').addClass('btn btn-primary').attr('data-action', 'togglePeriod'))); + bottomRow.append($('<td>').addClass('separator')); + } - if (picker.element.hasClass('input-group')) { - if (picker.element.find('.datepickerbutton').size() === 0) {//in case there is more then one 'input-group-addon' Issue #48 - picker.component = picker.element.find('[class^="input-group-"]'); + return $('<div>').addClass('timepicker-picker') + .append($('<table>').addClass('table-condensed') + .append([topRow, middleRow, bottomRow])); + }, + + getTimePickerTemplate = function () { + var hoursView = $('<div>').addClass('timepicker-hours') + .append($('<table>').addClass('table-condensed')), + minutesView = $('<div>').addClass('timepicker-minutes') + .append($('<table>').addClass('table-condensed')), + secondsView = $('<div>').addClass('timepicker-seconds') + .append($('<table>').addClass('table-condensed')), + ret = [getTimePickerMainTemplate()]; + + if (isEnabled('h')) { + ret.push(hoursView); } - else { - picker.component = picker.element.find('.datepickerbutton'); + if (isEnabled('m')) { + ret.push(minutesView); } - } - picker.format = picker.options.format; + if (isEnabled('s')) { + ret.push(secondsView); + } - localeData = moment().localeData(); + return ret; + }, - if (!picker.format) { - picker.format = (picker.options.pickDate ? localeData.longDateFormat('L') : ''); - if (picker.options.pickDate && picker.options.pickTime) { - picker.format += ' '; + getToolbar = function () { + var row = []; + if (options.showTodayButton) { + row.push($('<td>').append($('<a>').attr('data-action', 'today').append($('<span>').addClass(options.icons.today)))); } - picker.format += (picker.options.pickTime ? localeData.longDateFormat('LT') : ''); - if (picker.options.useSeconds) { - if (localeData.longDateFormat('LT').indexOf(' A') !== -1) { - picker.format = picker.format.split(' A')[0] + ':ss A'; - } - else { - picker.format += ':ss'; - } + if (!options.sideBySide && hasDate() && hasTime()) { + row.push($('<td>').append($('<a>').attr('data-action', 'togglePicker').append($('<span>').addClass(options.icons.time)))); } - } - picker.use24hours = (picker.format.toLowerCase().indexOf('a') < 0 && picker.format.indexOf('h') < 0); + if (options.showClear) { + row.push($('<td>').append($('<a>').attr('data-action', 'clear').append($('<span>').addClass(options.icons.clear)))); + } + return $('<table>').addClass('table-condensed').append($('<tbody>').append($('<tr>').append(row))); + }, - if (picker.component) { - icon = picker.component.find('span'); - } + getTemplate = function () { + var template = $('<div>').addClass('bootstrap-datetimepicker-widget dropdown-menu'), + dateView = $('<div>').addClass('datepicker').append(getDatePickerTemplate()), + timeView = $('<div>').addClass('timepicker').append(getTimePickerTemplate()), + content = $('<ul>').addClass('list-unstyled'), + toolbar = $('<li>').addClass('picker-switch' + (options.collapse ? ' accordion-toggle' : '')).append(getToolbar()); - if (picker.options.pickTime) { - if (icon) { - icon.addClass(picker.options.icons.time); + if (use24Hours) { + template.addClass('usetwentyfour'); } - } - if (picker.options.pickDate) { - if (icon) { - icon.removeClass(picker.options.icons.time); - icon.addClass(picker.options.icons.date); + if (options.sideBySide && hasDate() && hasTime()) { + template.addClass('timepicker-sbs'); + template.append( + $('<div>').addClass('row') + .append(dateView.addClass('col-sm-6')) + .append(timeView.addClass('col-sm-6')) + ); + template.append(toolbar); + return template; } - } - picker.options.widgetParent = - typeof picker.options.widgetParent === 'string' && picker.options.widgetParent || - picker.element.parents().filter(function () { - return 'scroll' === $(this).css('overflow-y'); - }).get(0) || - 'body'; - - picker.widget = $(getTemplate()).appendTo(picker.options.widgetParent); - - picker.minViewMode = picker.options.minViewMode || 0; - if (typeof picker.minViewMode === 'string') { - switch (picker.minViewMode) { - case 'months': - picker.minViewMode = 1; - break; - case 'years': - picker.minViewMode = 2; - break; - default: - picker.minViewMode = 0; - break; + if (options.toolbarPlacement === 'top') { + content.append(toolbar); } - } - picker.viewMode = picker.options.viewMode || 0; - if (typeof picker.viewMode === 'string') { - switch (picker.viewMode) { - case 'months': - picker.viewMode = 1; - break; - case 'years': - picker.viewMode = 2; - break; - default: - picker.viewMode = 0; - break; + if (hasDate()) { + content.append($('<li>').addClass((options.collapse && hasTime() ? 'collapse in' : '')).append(dateView)); } - } + if (options.toolbarPlacement === 'default') { + content.append(toolbar); + } + if (hasTime()) { + content.append($('<li>').addClass((options.collapse && hasDate() ? 'collapse' : '')).append(timeView)); + } + if (options.toolbarPlacement === 'bottom') { + content.append(toolbar); + } + return template.append(content); + }, - picker.viewMode = Math.max(picker.viewMode, picker.minViewMode); + dataToOptions = function () { + var eData = element.data(), + dataOptions = {}; - picker.options.disabledDates = indexGivenDates(picker.options.disabledDates); - picker.options.enabledDates = indexGivenDates(picker.options.enabledDates); + if (eData.dateOptions && eData.dateOptions instanceof Object) { + dataOptions = $.extend(true, dataOptions, eData.dateOptions); + } - picker.startViewMode = picker.viewMode; - picker.setMinDate(picker.options.minDate); - picker.setMaxDate(picker.options.maxDate); - fillDow(); - fillMonths(); - fillHours(); - fillMinutes(); - fillSeconds(); - update(); - showMode(); - if (!getPickerInput().prop('disabled')) { - attachDatePickerEvents(); - } - if (picker.options.defaultDate !== '' && getPickerInput().val() === '') { - picker.setValue(picker.options.defaultDate); - } - if (picker.options.minuteStepping !== 1) { - rInterval = picker.options.minuteStepping; - picker.date.minutes((Math.round(picker.date.minutes() / rInterval) * rInterval) % 60).seconds(0); - } - }, + $.each(options, function (key) { + var attributeName = 'date' + key.charAt(0).toUpperCase() + key.slice(1); + if (eData[attributeName] !== undefined) { + dataOptions[key] = eData[attributeName]; + } + }); + return dataOptions; + }, - getPickerInput = function () { - var input; + place = function () { + var offset = (component || element).position(), + vertical = options.widgetPositioning.vertical, + horizontal = options.widgetPositioning.horizontal, + parent; - if (picker.isInput) { - return picker.element; - } - input = picker.element.find('.datepickerinput'); - if (input.size() === 0) { - input = picker.element.find('input'); - } - else if (!input.is('input')) { - throw new Error('CSS class "datepickerinput" cannot be applied to non input element'); - } - return input; - }, + if (options.widgetParent) { + parent = options.widgetParent.append(widget); + } else if (element.is('input')) { + parent = element.parent().append(widget); + } else { + parent = element; + element.children().first().after(widget); + } - dataToOptions = function () { - var eData; - if (picker.element.is('input')) { - eData = picker.element.data(); - } - else { - eData = picker.element.find('input').data(); - } - if (eData.dateFormat !== undefined) { - picker.options.format = eData.dateFormat; - } - if (eData.datePickdate !== undefined) { - picker.options.pickDate = eData.datePickdate; - } - if (eData.datePicktime !== undefined) { - picker.options.pickTime = eData.datePicktime; - } - if (eData.dateUseminutes !== undefined) { - picker.options.useMinutes = eData.dateUseminutes; - } - if (eData.dateUseseconds !== undefined) { - picker.options.useSeconds = eData.dateUseseconds; - } - if (eData.dateUsecurrent !== undefined) { - picker.options.useCurrent = eData.dateUsecurrent; - } - if (eData.calendarWeeks !== undefined) { - picker.options.calendarWeeks = eData.calendarWeeks; - } - if (eData.dateMinutestepping !== undefined) { - picker.options.minuteStepping = eData.dateMinutestepping; - } - if (eData.dateMindate !== undefined) { - picker.options.minDate = eData.dateMindate; - } - if (eData.dateMaxdate !== undefined) { - picker.options.maxDate = eData.dateMaxdate; - } - if (eData.dateShowtoday !== undefined) { - picker.options.showToday = eData.dateShowtoday; - } - if (eData.dateCollapse !== undefined) { - picker.options.collapse = eData.dateCollapse; - } - if (eData.dateLanguage !== undefined) { - picker.options.language = eData.dateLanguage; - } - if (eData.dateDefaultdate !== undefined) { - picker.options.defaultDate = eData.dateDefaultdate; - } - if (eData.dateDisableddates !== undefined) { - picker.options.disabledDates = eData.dateDisableddates; - } - if (eData.dateEnableddates !== undefined) { - picker.options.enabledDates = eData.dateEnableddates; - } - if (eData.dateIcons !== undefined) { - picker.options.icons = eData.dateIcons; - } - if (eData.dateUsestrict !== undefined) { - picker.options.useStrict = eData.dateUsestrict; - } - if (eData.dateDirection !== undefined) { - picker.options.direction = eData.dateDirection; - } - if (eData.dateSidebyside !== undefined) { - picker.options.sideBySide = eData.dateSidebyside; - } - if (eData.dateDaysofweekdisabled !== undefined) { - picker.options.daysOfWeekDisabled = eData.dateDaysofweekdisabled; - } - }, + // Top and bottom logic + if (vertical === 'auto') { + if ((component || element).offset().top + widget.height() > $(window).height() + $(window).scrollTop() && + widget.height() + element.outerHeight() < (component || element).offset().top) { + vertical = 'top'; + } else { + vertical = 'bottom'; + } + } - place = function () { - var position = 'absolute', - offset = picker.component ? picker.component.offset() : picker.element.offset(), - $window = $(window), - placePosition; + // Left and right logic + if (horizontal === 'auto') { + if (parent.width() < offset.left + widget.outerWidth()) { + horizontal = 'right'; + } else { + horizontal = 'left'; + } + } - picker.width = picker.component ? picker.component.outerWidth() : picker.element.outerWidth(); - offset.top = offset.top + picker.element.outerHeight(); + if (vertical === 'top') { + widget.addClass('top').removeClass('bottom'); + } else { + widget.addClass('bottom').removeClass('top'); + } - if (picker.options.direction === 'up') { - placePosition = 'top'; - } else if (picker.options.direction === 'bottom') { - placePosition = 'bottom'; - } else if (picker.options.direction === 'auto') { - if (offset.top + picker.widget.height() > $window.height() + $window.scrollTop() && picker.widget.height() + picker.element.outerHeight() < offset.top) { - placePosition = 'top'; + if (horizontal === 'right') { + widget.addClass('pull-right'); } else { - placePosition = 'bottom'; + widget.removeClass('pull-right'); } - } - if (placePosition === 'top') { - offset.bottom = $window.height() - offset.top + picker.element.outerHeight() + 3; - picker.widget.addClass('top').removeClass('bottom'); - } else { - offset.top += 1; - picker.widget.addClass('bottom').removeClass('top'); - } - if (picker.options.width !== undefined) { - picker.widget.width(picker.options.width); - } + // find the first parent element that has a relative css positioning + if (parent.css('position') !== 'relative') { + parent = parent.parents().filter(function () { + return $(this).css('position') === 'relative'; + }).first(); + } - if (picker.options.orientation === 'left') { - picker.widget.addClass('left-oriented'); - offset.left = offset.left - picker.widget.width() + 20; - } + if (parent.length === 0) { + throw new Error('datetimepicker component should be placed within a relative positioned container'); + } - if (isInFixed()) { - position = 'fixed'; - offset.top -= $window.scrollTop(); - offset.left -= $window.scrollLeft(); - } + widget.css({ + top: vertical === 'top' ? 'auto' : offset.top + element.outerHeight(), + bottom: vertical === 'top' ? offset.top + element.outerHeight() : 'auto', + left: horizontal === 'left' ? parent.css('padding-left') : 'auto', + right: horizontal === 'left' ? 'auto' : parent.css('padding-right') + }); + }, - if ($window.width() < offset.left + picker.widget.outerWidth()) { - offset.right = $window.width() - offset.left - picker.width; - offset.left = 'auto'; - picker.widget.addClass('pull-right'); - } else { - offset.right = 'auto'; - picker.widget.removeClass('pull-right'); - } + notifyEvent = function (e) { + if (e.type === 'dp.change' && ((e.date && e.date.isSame(e.oldDate)) || (!e.date && !e.oldDate))) { + return; + } + element.trigger(e); + }, - if (placePosition === 'top') { - picker.widget.css({ - position: position, - bottom: offset.bottom, - top: 'auto', - left: offset.left, - right: offset.right - }); - } else { - picker.widget.css({ - position: position, - top: offset.top, - bottom: 'auto', - left: offset.left, - right: offset.right - }); - } - }, + showMode = function (dir) { + if (!widget) { + return; + } + if (dir) { + currentViewMode = Math.max(minViewModeNumber, Math.min(2, currentViewMode + dir)); + } + widget.find('.datepicker > div').hide().filter('.datepicker-' + datePickerModes[currentViewMode].clsName).show(); + }, - notifyChange = function (oldDate, eventType) { - if (moment(picker.date).isSame(moment(oldDate)) && !errored) { - return; - } - errored = false; - picker.element.trigger({ - type: 'dp.change', - date: moment(picker.date), - oldDate: moment(oldDate) - }); + fillDow = function () { + var row = $('<tr>'), + currentDate = viewDate.clone().startOf('w'); - if (eventType !== 'change') { - picker.element.change(); - } - }, + if (options.calendarWeeks === true) { + row.append($('<th>').addClass('cw').text('#')); + } - notifyError = function (date) { - errored = true; - picker.element.trigger({ - type: 'dp.error', - date: moment(date, picker.format, picker.options.useStrict) - }); - }, + while (currentDate.isBefore(viewDate.clone().endOf('w'))) { + row.append($('<th>').addClass('dow').text(currentDate.format('dd'))); + currentDate.add(1, 'd'); + } + widget.find('.datepicker-days thead').append(row); + }, - update = function (newDate) { - moment.locale(picker.options.language); - var dateStr = newDate; - if (!dateStr) { - dateStr = getPickerInput().val(); - if (dateStr) { - picker.date = moment(dateStr, picker.format, picker.options.useStrict); + isInDisabledDates = function (date) { + if (!options.disabledDates) { + return false; } - if (!picker.date) { - picker.date = moment(); + return options.disabledDates[date.format('YYYY-MM-DD')] === true; + }, + + isInEnabledDates = function (date) { + if (!options.enabledDates) { + return false; } - } - picker.viewDate = moment(picker.date).startOf('month'); - fillDate(); - fillTime(); - }, + return options.enabledDates[date.format('YYYY-MM-DD')] === true; + }, - fillDow = function () { - moment.locale(picker.options.language); - var html = $('<tr>'), weekdaysMin = moment.weekdaysMin(), i; - if (picker.options.calendarWeeks === true) { - html.append('<th class="cw">#</th>'); - } - if (moment().localeData()._week.dow === 0) { // starts on Sunday - for (i = 0; i < 7; i++) { - html.append('<th class="dow">' + weekdaysMin[i] + '</th>'); + isValid = function (targetMoment, granularity) { + if (!targetMoment.isValid()) { + return false; } - } else { - for (i = 1; i < 8; i++) { - if (i === 7) { - html.append('<th class="dow">' + weekdaysMin[0] + '</th>'); - } else { - html.append('<th class="dow">' + weekdaysMin[i] + '</th>'); - } + if (options.disabledDates && isInDisabledDates(targetMoment)) { + return false; } - } - picker.widget.find('.datepicker-days thead').append(html); - }, + if (options.enabledDates && isInEnabledDates(targetMoment)) { + return true; + } + if (options.minDate && targetMoment.isBefore(options.minDate, granularity)) { + return false; + } + if (options.maxDate && targetMoment.isAfter(options.maxDate, granularity)) { + return false; + } + if (granularity === 'd' && options.daysOfWeekDisabled.indexOf(targetMoment.day()) !== -1) { + return false; + } + return true; + }, - fillMonths = function () { - moment.locale(picker.options.language); - var html = '', i, monthsShort = moment.monthsShort(); - for (i = 0; i < 12; i++) { - html += '<span class="month">' + monthsShort[i] + '</span>'; - } - picker.widget.find('.datepicker-months td').append(html); - }, + fillMonths = function () { + var spans = [], + monthsShort = viewDate.clone().startOf('y').hour(12); // hour is changed to avoid DST issues in some browsers + while (monthsShort.isSame(viewDate, 'y')) { + spans.push($('<span>').attr('data-action', 'selectMonth').addClass('month').text(monthsShort.format('MMM'))); + monthsShort.add(1, 'M'); + } + widget.find('.datepicker-months td').empty().append(spans); + }, - fillDate = function () { - if (!picker.options.pickDate) { - return; - } - moment.locale(picker.options.language); - var year = picker.viewDate.year(), - month = picker.viewDate.month(), - startYear = picker.options.minDate.year(), - startMonth = picker.options.minDate.month(), - endYear = picker.options.maxDate.year(), - endMonth = picker.options.maxDate.month(), - currentDate, - prevMonth, nextMonth, html = [], row, clsName, i, days, yearCont, currentYear, months = moment.months(); + updateMonths = function () { + var monthsView = widget.find('.datepicker-months'), + monthsViewHeader = monthsView.find('th'), + months = monthsView.find('tbody').find('span'); - picker.widget.find('.datepicker-days').find('.disabled').removeClass('disabled'); - picker.widget.find('.datepicker-months').find('.disabled').removeClass('disabled'); - picker.widget.find('.datepicker-years').find('.disabled').removeClass('disabled'); + monthsView.find('.disabled').removeClass('disabled'); - picker.widget.find('.datepicker-days th:eq(1)').text( - months[month] + ' ' + year); + if (!isValid(viewDate.clone().subtract(1, 'y'), 'y')) { + monthsViewHeader.eq(0).addClass('disabled'); + } - prevMonth = moment(picker.viewDate, picker.format, picker.options.useStrict).subtract(1, 'months'); - days = prevMonth.daysInMonth(); - prevMonth.date(days).startOf('week'); - if ((year === startYear && month <= startMonth) || year < startYear) { - picker.widget.find('.datepicker-days th:eq(0)').addClass('disabled'); - } - if ((year === endYear && month >= endMonth) || year > endYear) { - picker.widget.find('.datepicker-days th:eq(2)').addClass('disabled'); - } + monthsViewHeader.eq(1).text(viewDate.year()); - nextMonth = moment(prevMonth).add(42, 'd'); - while (prevMonth.isBefore(nextMonth)) { - if (prevMonth.weekday() === moment().startOf('week').weekday()) { - row = $('<tr>'); - html.push(row); - if (picker.options.calendarWeeks === true) { - row.append('<td class="cw">' + prevMonth.week() + '</td>'); + if (!isValid(viewDate.clone().add(1, 'y'), 'y')) { + monthsViewHeader.eq(2).addClass('disabled'); + } + + months.removeClass('active'); + if (date.isSame(viewDate, 'y')) { + months.eq(date.month()).addClass('active'); + } + + months.each(function (index) { + if (!isValid(viewDate.clone().month(index), 'M')) { + $(this).addClass('disabled'); } + }); + }, + + updateYears = function () { + var yearsView = widget.find('.datepicker-years'), + yearsViewHeader = yearsView.find('th'), + startYear = viewDate.clone().subtract(5, 'y'), + endYear = viewDate.clone().add(6, 'y'), + html = ''; + + yearsView.find('.disabled').removeClass('disabled'); + + if (options.minDate && options.minDate.isAfter(startYear, 'y')) { + yearsViewHeader.eq(0).addClass('disabled'); } - clsName = ''; - if (prevMonth.year() < year || (prevMonth.year() === year && prevMonth.month() < month)) { - clsName += ' old'; - } else if (prevMonth.year() > year || (prevMonth.year() === year && prevMonth.month() > month)) { - clsName += ' new'; + + yearsViewHeader.eq(1).text(startYear.year() + '-' + endYear.year()); + + if (options.maxDate && options.maxDate.isBefore(endYear, 'y')) { + yearsViewHeader.eq(2).addClass('disabled'); } - if (prevMonth.isSame(moment({y: picker.date.year(), M: picker.date.month(), d: picker.date.date()}))) { - clsName += ' active'; + + while (!startYear.isAfter(endYear, 'y')) { + html += '<span data-action="selectYear" class="year' + (startYear.isSame(date, 'y') ? ' active' : '') + (!isValid(startYear, 'y') ? ' disabled' : '') + '">' + startYear.year() + '</span>'; + startYear.add(1, 'y'); } - if (isInDisableDates(prevMonth, 'day') || !isInEnableDates(prevMonth)) { - clsName += ' disabled'; + + yearsView.find('td').html(html); + }, + + fillDate = function () { + var daysView = widget.find('.datepicker-days'), + daysViewHeader = daysView.find('th'), + currentDate, + html = [], + row, + clsName; + + if (!hasDate()) { + return; } - if (picker.options.showToday === true) { - if (prevMonth.isSame(moment(), 'day')) { - clsName += ' today'; - } + + daysView.find('.disabled').removeClass('disabled'); + daysViewHeader.eq(1).text(viewDate.format(options.dayViewHeaderFormat)); + + if (!isValid(viewDate.clone().subtract(1, 'M'), 'M')) { + daysViewHeader.eq(0).addClass('disabled'); } - if (picker.options.daysOfWeekDisabled) { - for (i = 0; i < picker.options.daysOfWeekDisabled.length; i++) { - if (prevMonth.day() === picker.options.daysOfWeekDisabled[i]) { - clsName += ' disabled'; - break; + if (!isValid(viewDate.clone().add(1, 'M'), 'M')) { + daysViewHeader.eq(2).addClass('disabled'); + } + + currentDate = viewDate.clone().startOf('M').startOf('week'); + + while (!viewDate.clone().endOf('M').endOf('w').isBefore(currentDate, 'd')) { + if (currentDate.weekday() === 0) { + row = $('<tr>'); + if (options.calendarWeeks) { + row.append('<td class="cw">' + currentDate.week() + '</td>'); } + html.push(row); } + clsName = ''; + if (currentDate.isBefore(viewDate, 'M')) { + clsName += ' old'; + } + if (currentDate.isAfter(viewDate, 'M')) { + clsName += ' new'; + } + if (currentDate.isSame(date, 'd') && !unset) { + clsName += ' active'; + } + if (!isValid(currentDate, 'd')) { + clsName += ' disabled'; + } + if (currentDate.isSame(moment(), 'd')) { + clsName += ' today'; + } + if (currentDate.day() === 0 || currentDate.day() === 6) { + clsName += ' weekend'; + } + row.append('<td data-action="selectDay" class="day' + clsName + '">' + currentDate.date() + '</td>'); + currentDate.add(1, 'd'); } - row.append('<td class="day' + clsName + '">' + prevMonth.date() + '</td>'); - currentDate = prevMonth.date(); - prevMonth.add(1, 'd'); + daysView.find('tbody').empty().append(html); - if (currentDate === prevMonth.date()) { - prevMonth.add(1, 'd'); + updateMonths(); + + updateYears(); + }, + + fillHours = function () { + var table = widget.find('.timepicker-hours table'), + currentHour = viewDate.clone().startOf('d'), + html = [], + row = $('<tr>'); + + if (viewDate.hour() > 11 && !use24Hours) { + currentHour.hour(12); } - } - picker.widget.find('.datepicker-days tbody').empty().append(html); - currentYear = picker.date.year(); - months = picker.widget.find('.datepicker-months').find('th:eq(1)').text(year).end().find('span').removeClass('active'); - if (currentYear === year) { - months.eq(picker.date.month()).addClass('active'); - } - if (year - 1 < startYear) { - picker.widget.find('.datepicker-months th:eq(0)').addClass('disabled'); - } - if (year + 1 > endYear) { - picker.widget.find('.datepicker-months th:eq(2)').addClass('disabled'); - } - for (i = 0; i < 12; i++) { - if ((year === startYear && startMonth > i) || (year < startYear)) { - $(months[i]).addClass('disabled'); - } else if ((year === endYear && endMonth < i) || (year > endYear)) { - $(months[i]).addClass('disabled'); + while (currentHour.isSame(viewDate, 'd') && (use24Hours || (viewDate.hour() < 12 && currentHour.hour() < 12) || viewDate.hour() > 11)) { + if (currentHour.hour() % 4 === 0) { + row = $('<tr>'); + html.push(row); + } + row.append('<td data-action="selectHour" class="hour' + (!isValid(currentHour, 'h') ? ' disabled' : '') + '">' + currentHour.format(use24Hours ? 'HH' : 'hh') + '</td>'); + currentHour.add(1, 'h'); } - } + table.empty().append(html); + }, - html = ''; - year = parseInt(year / 10, 10) * 10; - yearCont = picker.widget.find('.datepicker-years').find( - 'th:eq(1)').text(year + '-' + (year + 9)).parents('table').find('td'); - picker.widget.find('.datepicker-years').find('th').removeClass('disabled'); - if (startYear > year) { - picker.widget.find('.datepicker-years').find('th:eq(0)').addClass('disabled'); - } - if (endYear < year + 9) { - picker.widget.find('.datepicker-years').find('th:eq(2)').addClass('disabled'); - } - year -= 1; - for (i = -1; i < 11; i++) { - html += '<span class="year' + (i === -1 || i === 10 ? ' old' : '') + (currentYear === year ? ' active' : '') + ((year < startYear || year > endYear) ? ' disabled' : '') + '">' + year + '</span>'; - year += 1; - } - yearCont.html(html); - }, + fillMinutes = function () { + var table = widget.find('.timepicker-minutes table'), + currentMinute = viewDate.clone().startOf('h'), + html = [], + row = $('<tr>'), + step = options.stepping === 1 ? 5 : options.stepping; - fillHours = function () { - moment.locale(picker.options.language); - var table = picker.widget.find('.timepicker .timepicker-hours table'), html = '', current, i, j; - table.parent().hide(); - if (picker.use24hours) { - current = 0; - for (i = 0; i < 6; i += 1) { - html += '<tr>'; - for (j = 0; j < 4; j += 1) { - html += '<td class="hour">' + padLeft(current.toString()) + '</td>'; - current++; + while (viewDate.isSame(currentMinute, 'h')) { + if (currentMinute.minute() % (step * 4) === 0) { + row = $('<tr>'); + html.push(row); } - html += '</tr>'; + row.append('<td data-action="selectMinute" class="minute' + (!isValid(currentMinute, 'm') ? ' disabled' : '') + '">' + currentMinute.format('mm') + '</td>'); + currentMinute.add(step, 'm'); } - } - else { - current = 1; - for (i = 0; i < 3; i += 1) { - html += '<tr>'; - for (j = 0; j < 4; j += 1) { - html += '<td class="hour">' + padLeft(current.toString()) + '</td>'; - current++; + table.empty().append(html); + }, + + fillSeconds = function () { + var table = widget.find('.timepicker-seconds table'), + currentSecond = viewDate.clone().startOf('m'), + html = [], + row = $('<tr>'); + + while (viewDate.isSame(currentSecond, 'm')) { + if (currentSecond.second() % 20 === 0) { + row = $('<tr>'); + html.push(row); } - html += '</tr>'; + row.append('<td data-action="selectSecond" class="second' + (!isValid(currentSecond, 's') ? ' disabled' : '') + '">' + currentSecond.format('ss') + '</td>'); + currentSecond.add(5, 's'); } - } - table.html(html); - }, - fillMinutes = function () { - var table = picker.widget.find('.timepicker .timepicker-minutes table'), html = '', current = 0, i, j, step = picker.options.minuteStepping; - table.parent().hide(); - if (step === 1) { - step = 5; - } - for (i = 0; i < Math.ceil(60 / step / 4) ; i++) { - html += '<tr>'; - for (j = 0; j < 4; j += 1) { - if (current < 60) { - html += '<td class="minute">' + padLeft(current.toString()) + '</td>'; - current += step; - } else { - html += '<td></td>'; - } + table.empty().append(html); + }, + + fillTime = function () { + var timeComponents = widget.find('.timepicker span[data-time-component]'); + if (!use24Hours) { + widget.find('.timepicker [data-action=togglePeriod]').text(date.format('A')); } - html += '</tr>'; - } - table.html(html); - }, + timeComponents.filter('[data-time-component=hours]').text(date.format(use24Hours ? 'HH' : 'hh')); + timeComponents.filter('[data-time-component=minutes]').text(date.format('mm')); + timeComponents.filter('[data-time-component=seconds]').text(date.format('ss')); - fillSeconds = function () { - var table = picker.widget.find('.timepicker .timepicker-seconds table'), html = '', current = 0, i, j; - table.parent().hide(); - for (i = 0; i < 3; i++) { - html += '<tr>'; - for (j = 0; j < 4; j += 1) { - html += '<td class="second">' + padLeft(current.toString()) + '</td>'; - current += 5; + fillHours(); + fillMinutes(); + fillSeconds(); + }, + + update = function () { + if (!widget) { + return; } - html += '</tr>'; - } - table.html(html); - }, + fillDate(); + fillTime(); + }, - fillTime = function () { - if (!picker.date) { - return; - } - var timeComponents = picker.widget.find('.timepicker span[data-time-component]'), - hour = picker.date.hours(), - period = picker.date.format('A'); - if (!picker.use24hours) { - if (hour === 0) { - hour = 12; - } else if (hour !== 12) { - hour = hour % 12; + setValue = function (targetMoment) { + var oldDate = unset ? null : date; + + // case of calling setValue(null or false) + if (!targetMoment) { + unset = true; + input.val(''); + element.data('date', ''); + notifyEvent({ + type: 'dp.change', + date: null, + oldDate: oldDate + }); + update(); + return; } - picker.widget.find('.timepicker [data-action=togglePeriod]').text(period); - } - timeComponents.filter('[data-time-component=hours]').text(padLeft(hour)); - timeComponents.filter('[data-time-component=minutes]').text(padLeft(picker.date.minutes())); - timeComponents.filter('[data-time-component=seconds]').text(padLeft(picker.date.second())); - }, - click = function (e) { - e.stopPropagation(); - e.preventDefault(); - picker.unset = false; - var target = $(e.target).closest('span, td, th'), month, year, step, day, oldDate = moment(picker.date); - if (target.length === 1) { - if (!target.is('.disabled')) { - switch (target[0].nodeName.toLowerCase()) { - case 'th': - switch (target[0].className) { - case 'picker-switch': - showMode(1); - break; - case 'prev': - case 'next': - step = dpGlobal.modes[picker.viewMode].navStep; - if (target[0].className === 'prev') { - step = step * -1; - } - picker.viewDate.add(step, dpGlobal.modes[picker.viewMode].navFnc); - fillDate(); - break; - } - break; - case 'span': - if (target.is('.month')) { - month = target.parent().find('span').index(target); - picker.viewDate.month(month); - } else { - year = parseInt(target.text(), 10) || 0; - picker.viewDate.year(year); - } - if (picker.viewMode === picker.minViewMode) { - picker.date = moment({ - y: picker.viewDate.year(), - M: picker.viewDate.month(), - d: picker.viewDate.date(), - h: picker.date.hours(), - m: picker.date.minutes(), - s: picker.date.seconds() - }); - set(); - notifyChange(oldDate, e.type); - } - showMode(-1); - fillDate(); - break; - case 'td': - if (target.is('.day')) { - day = parseInt(target.text(), 10) || 1; - month = picker.viewDate.month(); - year = picker.viewDate.year(); - if (target.is('.old')) { - if (month === 0) { - month = 11; - year -= 1; - } else { - month -= 1; - } - } else if (target.is('.new')) { - if (month === 11) { - month = 0; - year += 1; - } else { - month += 1; - } - } - picker.date = moment({ - y: year, - M: month, - d: day, - h: picker.date.hours(), - m: picker.date.minutes(), - s: picker.date.seconds() - } - ); - picker.viewDate = moment({ - y: year, M: month, d: Math.min(28, day) - }); - fillDate(); - set(); - notifyChange(oldDate, e.type); - } - break; - } + targetMoment = targetMoment.clone().locale(options.locale); + + if (options.stepping !== 1) { + targetMoment.minutes((Math.round(targetMoment.minutes() / options.stepping) * options.stepping) % 60).seconds(0); } - } - }, - actions = { - incrementHours: function () { - checkDate('add', 'hours', 1); + if (isValid(targetMoment)) { + date = targetMoment; + viewDate = date.clone(); + input.val(date.format(actualFormat)); + element.data('date', date.format(actualFormat)); + update(); + unset = false; + notifyEvent({ + type: 'dp.change', + date: date.clone(), + oldDate: oldDate + }); + } else { + input.val(unset ? '' : date.format(actualFormat)); + notifyEvent({ + type: 'dp.error', + date: targetMoment + }); + } }, - incrementMinutes: function () { - checkDate('add', 'minutes', picker.options.minuteStepping); + hide = function () { + var transitioning = false; + if (!widget) { + return picker; + } + // Ignore event if in the middle of a picker transition + widget.find('.collapse').each(function () { + var collapseData = $(this).data('collapse'); + if (collapseData && collapseData.transitioning) { + transitioning = true; + return false; + } + }); + if (transitioning) { + return picker; + } + if (component && component.hasClass('btn')) { + component.toggleClass('active'); + } + widget.hide(); + + $(window).off('resize', place); + widget.off('click', '[data-action]'); + widget.off('mousedown', false); + + widget.remove(); + widget = false; + + notifyEvent({ + type: 'dp.hide', + date: date.clone() + }); + return picker; }, - incrementSeconds: function () { - checkDate('add', 'seconds', 1); + /******************************************************************************** + * + * Widget UI interaction functions + * + ********************************************************************************/ + actions = { + next: function () { + viewDate.add(datePickerModes[currentViewMode].navStep, datePickerModes[currentViewMode].navFnc); + fillDate(); + }, + + previous: function () { + viewDate.subtract(datePickerModes[currentViewMode].navStep, datePickerModes[currentViewMode].navFnc); + fillDate(); + }, + + pickerSwitch: function () { + showMode(1); + }, + + selectMonth: function (e) { + var month = $(e.target).closest('tbody').find('span').index($(e.target)); + viewDate.month(month); + if (currentViewMode === minViewModeNumber) { + setValue(date.clone().year(viewDate.year()).month(viewDate.month())); + hide(); + } + showMode(-1); + fillDate(); + }, + + selectYear: function (e) { + var year = parseInt($(e.target).text(), 10) || 0; + viewDate.year(year); + if (currentViewMode === minViewModeNumber) { + setValue(date.clone().year(viewDate.year())); + hide(); + } + showMode(-1); + fillDate(); + }, + + selectDay: function (e) { + var day = viewDate.clone(); + if ($(e.target).is('.old')) { + day.subtract(1, 'M'); + } + if ($(e.target).is('.new')) { + day.add(1, 'M'); + } + setValue(day.date(parseInt($(e.target).text(), 10))); + if (!hasTime() && !options.keepOpen) { + hide(); + } + }, + + incrementHours: function () { + setValue(date.clone().add(1, 'h')); + }, + + incrementMinutes: function () { + setValue(date.clone().add(options.stepping, 'm')); + }, + + incrementSeconds: function () { + setValue(date.clone().add(1, 's')); + }, + + decrementHours: function () { + setValue(date.clone().subtract(1, 'h')); + }, + + decrementMinutes: function () { + setValue(date.clone().subtract(options.stepping, 'm')); + }, + + decrementSeconds: function () { + setValue(date.clone().subtract(1, 's')); + }, + + togglePeriod: function () { + setValue(date.clone().add((date.hours() >= 12) ? -12 : 12, 'h')); + }, + + togglePicker: function (e) { + var $this = $(e.target), + $parent = $this.closest('ul'), + expanded = $parent.find('.in'), + closed = $parent.find('.collapse:not(.in)'), + collapseData; + + if (expanded && expanded.length) { + collapseData = expanded.data('collapse'); + if (collapseData && collapseData.transitioning) { + return; + } + expanded.collapse('hide'); + closed.collapse('show'); + if ($this.is('span')) { + $this.toggleClass(options.icons.time + ' ' + options.icons.date); + } else { + $this.find('span').toggleClass(options.icons.time + ' ' + options.icons.date); + } + + // NOTE: uncomment if toggled state will be restored in show() + //if (component) { + // component.find('span').toggleClass(options.icons.time + ' ' + options.icons.date); + //} + } + }, + + showPicker: function () { + widget.find('.timepicker > div:not(.timepicker-picker)').hide(); + widget.find('.timepicker .timepicker-picker').show(); + }, + + showHours: function () { + widget.find('.timepicker .timepicker-picker').hide(); + widget.find('.timepicker .timepicker-hours').show(); + }, + + showMinutes: function () { + widget.find('.timepicker .timepicker-picker').hide(); + widget.find('.timepicker .timepicker-minutes').show(); + }, + + showSeconds: function () { + widget.find('.timepicker .timepicker-picker').hide(); + widget.find('.timepicker .timepicker-seconds').show(); + }, + + selectHour: function (e) { + var hour = parseInt($(e.target).text(), 10); + + if (!use24Hours) { + if (date.hours() >= 12) { + if (hour !== 12) { + hour += 12; + } + } else { + if (hour === 12) { + hour = 0; + } + } + } + setValue(date.clone().hours(hour)); + actions.showPicker.call(picker); + }, + + selectMinute: function (e) { + setValue(date.clone().minutes(parseInt($(e.target).text(), 10))); + actions.showPicker.call(picker); + }, + + selectSecond: function (e) { + setValue(date.clone().seconds(parseInt($(e.target).text(), 10))); + actions.showPicker.call(picker); + }, + + clear: function () { + setValue(null); + }, + + today: function () { + setValue(moment()); + } }, - decrementHours: function () { - checkDate('subtract', 'hours', 1); + doAction = function (e) { + if ($(e.currentTarget).is('.disabled')) { + return false; + } + actions[$(e.currentTarget).data('action')].apply(picker, arguments); + return false; }, - decrementMinutes: function () { - checkDate('subtract', 'minutes', picker.options.minuteStepping); + show = function () { + var currentMoment, + useCurrentGranularity = { + 'year': function (m) { + return m.month(0).date(1).hours(0).seconds(0).minutes(0); + }, + 'month': function (m) { + return m.date(1).hours(0).seconds(0).minutes(0); + }, + 'day': function (m) { + return m.hours(0).seconds(0).minutes(0); + }, + 'hour': function (m) { + return m.seconds(0).minutes(0); + }, + 'minute': function (m) { + return m.seconds(0); + } + }; + + if (input.prop('disabled') || input.prop('readonly') || widget) { + return picker; + } + if (options.useCurrent && unset) { // && input.val().trim().length !== 0) { this broke the jasmine test + currentMoment = moment(); + if (typeof options.useCurrent === 'string') { + currentMoment = useCurrentGranularity[options.useCurrent](currentMoment); + } + setValue(currentMoment); + } + + widget = getTemplate(); + + fillDow(); + fillMonths(); + + widget.find('.timepicker-hours').hide(); + widget.find('.timepicker-minutes').hide(); + widget.find('.timepicker-seconds').hide(); + + update(); + showMode(); + + $(window).on('resize', place); + widget.on('click', '[data-action]', doAction); // this handles clicks on the widget + widget.on('mousedown', false); + + if (component && component.hasClass('btn')) { + component.toggleClass('active'); + } + widget.show(); + place(); + + if (!input.is(':focus')) { + input.focus(); + } + + notifyEvent({ + type: 'dp.show' + }); + return picker; }, - decrementSeconds: function () { - checkDate('subtract', 'seconds', 1); + toggle = function () { + return (widget ? hide() : show()); }, - togglePeriod: function () { - var hour = picker.date.hours(); - if (hour >= 12) { - hour -= 12; + parseInputDate = function (date) { + if (moment.isMoment(date) || date instanceof Date) { + date = moment(date); } else { - hour += 12; + date = moment(date, parseFormats, options.useStrict); } - picker.date.hours(hour); + date.locale(options.locale); + return date; }, - showPicker: function () { - picker.widget.find('.timepicker > div:not(.timepicker-picker)').hide(); - picker.widget.find('.timepicker .timepicker-picker').show(); + keydown = function (e) { + if (e.keyCode === 27) { // allow escape to hide picker + hide(); + } }, - showHours: function () { - picker.widget.find('.timepicker .timepicker-picker').hide(); - picker.widget.find('.timepicker .timepicker-hours').show(); + change = function (e) { + var val = $(e.target).val().trim(), + parsedDate = val ? parseInputDate(val) : null; + setValue(parsedDate); + e.stopImmediatePropagation(); + return false; }, - showMinutes: function () { - picker.widget.find('.timepicker .timepicker-picker').hide(); - picker.widget.find('.timepicker .timepicker-minutes').show(); - }, + attachDatePickerElementEvents = function () { + input.on({ + 'change': change, + 'blur': hide, + 'keydown': keydown + }); - showSeconds: function () { - picker.widget.find('.timepicker .timepicker-picker').hide(); - picker.widget.find('.timepicker .timepicker-seconds').show(); + if (element.is('input')) { + input.on({ + 'focus': show + }); + } else if (component) { + component.on('click', toggle); + component.on('mousedown', false); + } }, - selectHour: function (e) { - var hour = parseInt($(e.target).text(), 10); - if (!picker.use24hours) { - if (picker.date.hours() >= 12) { - if (hour !== 12) { - hour += 12; - } - } else { - if (hour === 12) { - hour = 0; - } - } + detachDatePickerElementEvents = function () { + input.off({ + 'change': change, + 'blur': hide, + 'keydown': keydown + }); + + if (element.is('input')) { + input.off({ + 'focus': show + }); + } else if (component) { + component.off('click', toggle); + component.off('mousedown', false); } - picker.date.hours(hour); - actions.showPicker.call(picker); }, - selectMinute: function (e) { - picker.date.minutes(parseInt($(e.target).text(), 10)); - actions.showPicker.call(picker); + indexGivenDates = function (givenDatesArray) { + // Store given enabledDates and disabledDates as keys. + // This way we can check their existence in O(1) time instead of looping through whole array. + // (for example: options.enabledDates['2014-02-27'] === true) + var givenDatesIndexed = {}; + $.each(givenDatesArray, function () { + var dDate = parseInputDate(this); + if (dDate.isValid()) { + givenDatesIndexed[dDate.format('YYYY-MM-DD')] = true; + } + }); + return (Object.keys(givenDatesIndexed).length) ? givenDatesIndexed : false; }, - selectSecond: function (e) { - picker.date.seconds(parseInt($(e.target).text(), 10)); - actions.showPicker.call(picker); + initFormatting = function () { + var format = options.format || 'L LT'; + + actualFormat = format.replace(/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g, function (input) { + return date.localeData().longDateFormat(input) || input; + }); + + parseFormats = options.extraFormats ? options.extraFormats.slice() : []; + if (parseFormats.indexOf(format) < 0 && parseFormats.indexOf(actualFormat) < 0) { + parseFormats.push(actualFormat); + } + + use24Hours = (actualFormat.toLowerCase().indexOf('a') < 1 && actualFormat.indexOf('h') < 1); + + if (isEnabled('y')) { + minViewModeNumber = 2; + } + if (isEnabled('M')) { + minViewModeNumber = 1; + } + if (isEnabled('d')) { + minViewModeNumber = 0; + } + + currentViewMode = Math.max(minViewModeNumber, currentViewMode); + + if (!unset) { + setValue(date); + } + }; + + /******************************************************************************** + * + * Public API functions + * ===================== + * + * Important: Do not expose direct references to private objects or the options + * object to the outer world. Always return a clone when returning values or make + * a clone when setting a private variable. + * + ********************************************************************************/ + picker.destroy = function () { + hide(); + detachDatePickerElementEvents(); + element.removeData('DateTimePicker'); + element.removeData('date'); + }; + + picker.toggle = toggle; + + picker.show = show; + + picker.hide = hide; + + picker.disable = function () { + hide(); + if (component && component.hasClass('btn')) { + component.addClass('disabled'); } - }, + input.prop('disabled', true); + return picker; + }; - doAction = function (e) { - var oldDate = moment(picker.date), - action = $(e.currentTarget).data('action'), - rv = actions[action].apply(picker, arguments); - stopEvent(e); - if (!picker.date) { - picker.date = moment({y: 1970}); + picker.enable = function () { + if (component && component.hasClass('btn')) { + component.removeClass('disabled'); } - set(); - fillTime(); - notifyChange(oldDate, e.type); - return rv; - }, + input.prop('disabled', false); + return picker; + }; - stopEvent = function (e) { - e.stopPropagation(); - e.preventDefault(); - }, + picker.options = function (newOptions) { + if (arguments.length === 0) { + return $.extend(true, {}, options); + } - keydown = function (e) { - if (e.keyCode === 27) { // allow escape to hide picker - picker.hide(); + if (!(newOptions instanceof Object)) { + throw new TypeError('options() options parameter should be an object'); } - }, + $.extend(true, options, newOptions); + $.each(options, function (key, value) { + if (picker[key] !== undefined) { + picker[key](value); + } else { + throw new TypeError('option ' + key + ' is not recognized!'); + } + }); + return picker; + }; - change = function (e) { - moment.locale(picker.options.language); - var input = $(e.target), oldDate = moment(picker.date), newDate = moment(input.val(), picker.format, picker.options.useStrict); - if (newDate.isValid() && !isInDisableDates(newDate) && isInEnableDates(newDate)) { - update(); - picker.setValue(newDate); - notifyChange(oldDate, e.type); - set(); + picker.date = function (newDate) { + if (arguments.length === 0) { + if (unset) { + return null; + } + return date.clone(); } - else { - picker.viewDate = oldDate; - picker.unset = true; - notifyChange(oldDate, e.type); - notifyError(newDate); + + if (newDate !== null && typeof newDate !== 'string' && !moment.isMoment(newDate) && !(newDate instanceof Date)) { + throw new TypeError('date() parameter must be one of [null, string, moment or Date]'); } - }, - showMode = function (dir) { - if (dir) { - picker.viewMode = Math.max(picker.minViewMode, Math.min(2, picker.viewMode + dir)); + setValue(newDate === null ? null : parseInputDate(newDate)); + return picker; + }; + + picker.format = function (newFormat) { + if (arguments.length === 0) { + return options.format; } - picker.widget.find('.datepicker > div').hide().filter('.datepicker-' + dpGlobal.modes[picker.viewMode].clsName).show(); - }, - attachDatePickerEvents = function () { - var $this, $parent, expanded, closed, collapseData; - picker.widget.on('click', '.datepicker *', $.proxy(click, this)); // this handles date picker clicks - picker.widget.on('click', '[data-action]', $.proxy(doAction, this)); // this handles time picker clicks - picker.widget.on('mousedown', $.proxy(stopEvent, this)); - picker.element.on('keydown', $.proxy(keydown, this)); - if (picker.options.pickDate && picker.options.pickTime) { - picker.widget.on('click.togglePicker', '.accordion-toggle', function (e) { - e.stopPropagation(); - $this = $(this); - $parent = $this.closest('ul'); - expanded = $parent.find('.in'); - closed = $parent.find('.collapse:not(.in)'); + if ((typeof newFormat !== 'string') && ((typeof newFormat !== 'boolean') || (newFormat !== false))) { + throw new TypeError('format() expects a sting or boolean:false parameter ' + newFormat); + } - if (expanded && expanded.length) { - collapseData = expanded.data('collapse'); - if (collapseData && collapseData.transitioning) { - return; - } - expanded.collapse('hide'); - closed.collapse('show'); - $this.find('span').toggleClass(picker.options.icons.time + ' ' + picker.options.icons.date); - if (picker.component) { - picker.component.find('span').toggleClass(picker.options.icons.time + ' ' + picker.options.icons.date); - } - } - }); + options.format = newFormat; + if (actualFormat) { + initFormatting(); // reinit formatting } - if (picker.isInput) { - picker.element.on({ - 'click': $.proxy(picker.show, this), - 'focus': $.proxy(picker.show, this), - 'change': $.proxy(change, this), - 'blur': $.proxy(picker.hide, this) - }); - } else { - picker.element.on({ - 'change': $.proxy(change, this) - }, 'input'); - if (picker.component) { - picker.component.on('click', $.proxy(picker.show, this)); - picker.component.on('mousedown', $.proxy(stopEvent, this)); - } else { - picker.element.on('click', $.proxy(picker.show, this)); - } + return picker; + }; + + picker.dayViewHeaderFormat = function (newFormat) { + if (arguments.length === 0) { + return options.dayViewHeaderFormat; } - }, - attachDatePickerGlobalEvents = function () { - $(window).on( - 'resize.datetimepicker' + picker.id, $.proxy(place, this)); - if (!picker.isInput) { - $(document).on( - 'mousedown.datetimepicker' + picker.id, $.proxy(picker.hide, this)); + if (typeof newFormat !== 'string') { + throw new TypeError('dayViewHeaderFormat() expects a string parameter'); } - }, - detachDatePickerEvents = function () { - picker.widget.off('click', '.datepicker *', picker.click); - picker.widget.off('click', '[data-action]'); - picker.widget.off('mousedown', picker.stopEvent); - if (picker.options.pickDate && picker.options.pickTime) { - picker.widget.off('click.togglePicker'); + options.dayViewHeaderFormat = newFormat; + return picker; + }; + + picker.extraFormats = function (formats) { + if (arguments.length === 0) { + return options.extraFormats; } - if (picker.isInput) { - picker.element.off({ - 'focus': picker.show, - 'change': change, - 'click': picker.show, - 'blur' : picker.hide - }); - } else { - picker.element.off({ - 'change': change - }, 'input'); - if (picker.component) { - picker.component.off('click', picker.show); - picker.component.off('mousedown', picker.stopEvent); - } else { - picker.element.off('click', picker.show); - } + + if (formats !== false && !(formats instanceof Array)) { + throw new TypeError('extraFormats() expects an array or false parameter'); } - }, - detachDatePickerGlobalEvents = function () { - $(window).off('resize.datetimepicker' + picker.id); - if (!picker.isInput) { - $(document).off('mousedown.datetimepicker' + picker.id); + options.extraFormats = formats; + if (parseFormats) { + initFormatting(); // reinit formatting } - }, + return picker; + }; - isInFixed = function () { - if (picker.element) { - var parents = picker.element.parents(), inFixed = false, i; - for (i = 0; i < parents.length; i++) { - if ($(parents[i]).css('position') === 'fixed') { - inFixed = true; - break; - } - } - return inFixed; - } else { - return false; + picker.disabledDates = function (dates) { + if (arguments.length === 0) { + return (options.disabledDates ? $.extend({}, options.disabledDates) : options.disabledDates); } - }, - set = function () { - moment.locale(picker.options.language); - var formatted = ''; - if (!picker.unset) { - formatted = moment(picker.date).format(picker.format); + if (!dates) { + options.disabledDates = false; + update(); + return picker; } - getPickerInput().val(formatted); - picker.element.data('date', formatted); - if (!picker.options.pickTime) { - picker.hide(); + if (!(dates instanceof Array)) { + throw new TypeError('disabledDates() expects an array parameter'); } - }, + options.disabledDates = indexGivenDates(dates); + options.enabledDates = false; + update(); + return picker; + }; - checkDate = function (direction, unit, amount) { - moment.locale(picker.options.language); - var newDate; - if (direction === 'add') { - newDate = moment(picker.date); - if (newDate.hours() === 23) { - newDate.add(amount, unit); - } - newDate.add(amount, unit); + picker.enabledDates = function (dates) { + if (arguments.length === 0) { + return (options.enabledDates ? $.extend({}, options.enabledDates) : options.enabledDates); } - else { - newDate = moment(picker.date).subtract(amount, unit); + + if (!dates) { + options.enabledDates = false; + update(); + return picker; } - if (isInDisableDates(moment(newDate.subtract(amount, unit))) || isInDisableDates(newDate)) { - notifyError(newDate.format(picker.format)); - return; + if (!(dates instanceof Array)) { + throw new TypeError('enabledDates() expects an array parameter'); } + options.enabledDates = indexGivenDates(dates); + options.disabledDates = false; + update(); + return picker; + }; - if (direction === 'add') { - picker.date.add(amount, unit); + picker.daysOfWeekDisabled = function (daysOfWeekDisabled) { + if (arguments.length === 0) { + return options.daysOfWeekDisabled.splice(0); } - else { - picker.date.subtract(amount, unit); + + if (!(daysOfWeekDisabled instanceof Array)) { + throw new TypeError('daysOfWeekDisabled() expects an array parameter'); } - picker.unset = false; - }, + options.daysOfWeekDisabled = daysOfWeekDisabled.reduce(function (previousValue, currentValue) { + currentValue = parseInt(currentValue, 10); + if (currentValue > 6 || currentValue < 0 || isNaN(currentValue)) { + return previousValue; + } + if (previousValue.indexOf(currentValue) === -1) { + previousValue.push(currentValue); + } + return previousValue; + }, []).sort(); + update(); + return picker; + }; - isInDisableDates = function (date, timeUnit) { - moment.locale(picker.options.language); - var maxDate = moment(picker.options.maxDate, picker.format, picker.options.useStrict), - minDate = moment(picker.options.minDate, picker.format, picker.options.useStrict); + picker.maxDate = function (date) { + if (arguments.length === 0) { + return options.maxDate ? options.maxDate.clone() : options.maxDate; + } - if (timeUnit) { - maxDate = maxDate.endOf(timeUnit); - minDate = minDate.startOf(timeUnit); + if ((typeof date === 'boolean') && date === false) { + options.maxDate = false; + update(); + return picker; } - if (date.isAfter(maxDate) || date.isBefore(minDate)) { - return true; + var parsedDate = parseInputDate(date); + + if (!parsedDate.isValid()) { + throw new TypeError('maxDate() Could not parse date parameter: ' + date); } - if (picker.options.disabledDates === false) { - return false; + if (options.minDate && parsedDate.isBefore(options.minDate)) { + throw new TypeError('maxDate() date parameter is before options.minDate: ' + parsedDate.format(actualFormat)); } - return picker.options.disabledDates[date.format('YYYY-MM-DD')] === true; - }, - isInEnableDates = function (date) { - moment.locale(picker.options.language); - if (picker.options.enabledDates === false) { - return true; + options.maxDate = parsedDate; + if (options.maxDate.isBefore(date)) { + setValue(options.maxDate); } - return picker.options.enabledDates[date.format('YYYY-MM-DD')] === true; - }, + update(); + return picker; + }; - indexGivenDates = function (givenDatesArray) { - // Store given enabledDates and disabledDates as keys. - // This way we can check their existence in O(1) time instead of looping through whole array. - // (for example: picker.options.enabledDates['2014-02-27'] === true) - var givenDatesIndexed = {}, givenDatesCount = 0, i; - for (i = 0; i < givenDatesArray.length; i++) { - if (moment.isMoment(givenDatesArray[i]) || givenDatesArray[i] instanceof Date) { - dDate = moment(givenDatesArray[i]); - } else { - dDate = moment(givenDatesArray[i], picker.format, picker.options.useStrict); - } - if (dDate.isValid()) { - givenDatesIndexed[dDate.format('YYYY-MM-DD')] = true; - givenDatesCount++; - } + picker.minDate = function (date) { + if (arguments.length === 0) { + return options.minDate ? options.minDate.clone() : options.minDate; } - if (givenDatesCount > 0) { - return givenDatesIndexed; + + if ((typeof date === 'boolean') && date === false) { + options.minDate = false; + update(); + return picker; } - return false; - }, - padLeft = function (string) { - string = string.toString(); - if (string.length >= 2) { - return string; + var parsedDate = parseInputDate(date); + + if (!parsedDate.isValid()) { + throw new TypeError('minDate() Could not parse date parameter: ' + date); } - return '0' + string; - }, + if (options.maxDate && parsedDate.isAfter(options.maxDate)) { + throw new TypeError('minDate() date parameter is after options.maxDate: ' + parsedDate.format(actualFormat)); + } + options.minDate = parsedDate; + if (options.minDate.isAfter(date)) { + setValue(options.minDate); + } + update(); + return picker; + }; - getTemplate = function () { - var - headTemplate = - '<thead>' + - '<tr>' + - '<th class="prev">&lsaquo;</th><th colspan="' + (picker.options.calendarWeeks ? '6' : '5') + '" class="picker-switch"></th><th class="next">&rsaquo;</th>' + - '</tr>' + - '</thead>', - contTemplate = - '<tbody><tr><td colspan="' + (picker.options.calendarWeeks ? '8' : '7') + '"></td></tr></tbody>', - template = '<div class="datepicker-days">' + - '<table class="table-condensed">' + headTemplate + '<tbody></tbody></table>' + - '</div>' + - '<div class="datepicker-months">' + - '<table class="table-condensed">' + headTemplate + contTemplate + '</table>' + - '</div>' + - '<div class="datepicker-years">' + - '<table class="table-condensed">' + headTemplate + contTemplate + '</table>' + - '</div>', - ret = ''; - if (picker.options.pickDate && picker.options.pickTime) { - ret = '<div class="bootstrap-datetimepicker-widget' + (picker.options.sideBySide ? ' timepicker-sbs' : '') + (picker.use24hours ? ' usetwentyfour' : '') + ' dropdown-menu" style="z-index:9999 !important;">'; - if (picker.options.sideBySide) { - ret += '<div class="row">' + - '<div class="col-sm-6 datepicker">' + template + '</div>' + - '<div class="col-sm-6 timepicker">' + tpGlobal.getTemplate() + '</div>' + - '</div>'; - } else { - ret += '<ul class="list-unstyled">' + - '<li' + (picker.options.collapse ? ' class="collapse in"' : '') + '>' + - '<div class="datepicker">' + template + '</div>' + - '</li>' + - '<li class="picker-switch accordion-toggle"><a class="btn" style="width:100%"><span class="' + picker.options.icons.time + '"></span></a></li>' + - '<li' + (picker.options.collapse ? ' class="collapse"' : '') + '>' + - '<div class="timepicker">' + tpGlobal.getTemplate() + '</div>' + - '</li>' + - '</ul>'; - } - ret += '</div>'; - return ret; + picker.defaultDate = function (defaultDate) { + if (arguments.length === 0) { + return options.defaultDate ? options.defaultDate.clone() : options.defaultDate; } - if (picker.options.pickTime) { - return ( - '<div class="bootstrap-datetimepicker-widget dropdown-menu">' + - '<div class="timepicker">' + tpGlobal.getTemplate() + '</div>' + - '</div>' - ); + if (!defaultDate) { + options.defaultDate = false; + return picker; } - return ( - '<div class="bootstrap-datetimepicker-widget dropdown-menu">' + - '<div class="datepicker">' + template + '</div>' + - '</div>' - ); - }, + var parsedDate = parseInputDate(defaultDate); + if (!parsedDate.isValid()) { + throw new TypeError('defaultDate() Could not parse date parameter: ' + defaultDate); + } + if (!isValid(parsedDate)) { + throw new TypeError('defaultDate() date passed is invalid according to component setup validations'); + } - dpGlobal = { - modes: [ - { - clsName: 'days', - navFnc: 'month', - navStep: 1 - }, - { - clsName: 'months', - navFnc: 'year', - navStep: 1 - }, - { - clsName: 'years', - navFnc: 'year', - navStep: 10 - } - ] - }, + options.defaultDate = parsedDate; - tpGlobal = { - hourTemplate: '<span data-action="showHours" data-time-component="hours" class="timepicker-hour"></span>', - minuteTemplate: '<span data-action="showMinutes" data-time-component="minutes" class="timepicker-minute"></span>', - secondTemplate: '<span data-action="showSeconds" data-time-component="seconds" class="timepicker-second"></span>' + if (options.defaultDate && input.val().trim() === '') { + setValue(options.defaultDate); + } + return picker; }; - tpGlobal.getTemplate = function () { - return ( - '<div class="timepicker-picker">' + - '<table class="table-condensed">' + - '<tr>' + - '<td><a href="#" class="btn" data-action="incrementHours"><span class="' + picker.options.icons.up + '"></span></a></td>' + - '<td class="separator"></td>' + - '<td>' + (picker.options.useMinutes ? '<a href="#" class="btn" data-action="incrementMinutes"><span class="' + picker.options.icons.up + '"></span></a>' : '') + '</td>' + - (picker.options.useSeconds ? - '<td class="separator"></td><td><a href="#" class="btn" data-action="incrementSeconds"><span class="' + picker.options.icons.up + '"></span></a></td>' : '') + - (picker.use24hours ? '' : '<td class="separator"></td>') + - '</tr>' + - '<tr>' + - '<td>' + tpGlobal.hourTemplate + '</td> ' + - '<td class="separator">:</td>' + - '<td>' + (picker.options.useMinutes ? tpGlobal.minuteTemplate : '<span class="timepicker-minute">00</span>') + '</td> ' + - (picker.options.useSeconds ? - '<td class="separator">:</td><td>' + tpGlobal.secondTemplate + '</td>' : '') + - (picker.use24hours ? '' : '<td class="separator"></td>' + - '<td><button type="button" class="btn btn-primary" data-action="togglePeriod"></button></td>') + - '</tr>' + - '<tr>' + - '<td><a href="#" class="btn" data-action="decrementHours"><span class="' + picker.options.icons.down + '"></span></a></td>' + - '<td class="separator"></td>' + - '<td>' + (picker.options.useMinutes ? '<a href="#" class="btn" data-action="decrementMinutes"><span class="' + picker.options.icons.down + '"></span></a>' : '') + '</td>' + - (picker.options.useSeconds ? - '<td class="separator"></td><td><a href="#" class="btn" data-action="decrementSeconds"><span class="' + picker.options.icons.down + '"></span></a></td>' : '') + - (picker.use24hours ? '' : '<td class="separator"></td>') + - '</tr>' + - '</table>' + - '</div>' + - '<div class="timepicker-hours" data-action="selectHour">' + - '<table class="table-condensed"></table>' + - '</div>' + - '<div class="timepicker-minutes" data-action="selectMinute">' + - '<table class="table-condensed"></table>' + - '</div>' + - (picker.options.useSeconds ? - '<div class="timepicker-seconds" data-action="selectSecond"><table class="table-condensed"></table></div>' : '') - ); + picker.locale = function (locale) { + if (arguments.length === 0) { + return options.locale; + } + + if (!moment.localeData(locale)) { + throw new TypeError('locale() locale ' + locale + ' is not loaded from moment locales!'); + } + + options.locale = locale; + date.locale(options.locale); + viewDate.locale(options.locale); + + if (actualFormat) { + initFormatting(); // reinit formatting + } + if (widget) { + hide(); + show(); + } + return picker; }; - picker.destroy = function () { - detachDatePickerEvents(); - detachDatePickerGlobalEvents(); - picker.widget.remove(); - picker.element.removeData('DateTimePicker'); - if (picker.component) { - picker.component.removeData('DateTimePicker'); + picker.stepping = function (stepping) { + if (arguments.length === 0) { + return options.stepping; } + + stepping = parseInt(stepping, 10); + if (isNaN(stepping) || stepping < 1) { + stepping = 1; + } + options.stepping = stepping; + return picker; }; - picker.show = function (e) { - if (getPickerInput().prop('disabled')) { - return; + picker.useCurrent = function (useCurrent) { + var useCurrentOptions = ['year', 'month', 'day', 'hour', 'minute']; + if (arguments.length === 0) { + return options.useCurrent; } - if (picker.options.useCurrent) { - if (getPickerInput().val() === '') { - if (picker.options.minuteStepping !== 1) { - var mDate = moment(), - rInterval = picker.options.minuteStepping; - mDate.minutes((Math.round(mDate.minutes() / rInterval) * rInterval) % 60).seconds(0); - picker.setValue(mDate.format(picker.format)); - } else { - picker.setValue(moment().format(picker.format)); - } - notifyChange('', e.type); - } + + if ((typeof useCurrent !== 'boolean') && (typeof useCurrent !== 'string')) { + throw new TypeError('useCurrent() expects a boolean or string parameter'); } - // if this is a click event on the input field and picker is already open don't hide it - if (e && e.type === 'click' && picker.isInput && picker.widget.hasClass('picker-open')) { - return; + if (typeof useCurrent === 'string' && useCurrentOptions.indexOf(useCurrent.toLowerCase()) === -1) { + throw new TypeError('useCurrent() expects a string parameter of ' + useCurrentOptions.join(', ')); } - if (picker.widget.hasClass('picker-open')) { - picker.widget.hide(); - picker.widget.removeClass('picker-open'); + options.useCurrent = useCurrent; + return picker; + }; + + picker.collapse = function (collapse) { + if (arguments.length === 0) { + return options.collapse; } - else { - picker.widget.show(); - picker.widget.addClass('picker-open'); + + if (typeof collapse !== 'boolean') { + throw new TypeError('collapse() expects a boolean parameter'); } - picker.height = picker.component ? picker.component.outerHeight() : picker.element.outerHeight(); - place(); - picker.element.trigger({ - type: 'dp.show', - date: moment(picker.date) - }); - attachDatePickerGlobalEvents(); - if (e) { - stopEvent(e); + if (options.collapse === collapse) { + return picker; } + options.collapse = collapse; + if (widget) { + hide(); + show(); + } + return picker; }; - picker.disable = function () { - var input = getPickerInput(); - if (input.prop('disabled')) { - return; + picker.icons = function (icons) { + if (arguments.length === 0) { + return $.extend({}, options.icons); } - input.prop('disabled', true); - detachDatePickerEvents(); + + if (!(icons instanceof Object)) { + throw new TypeError('icons() expects parameter to be an Object'); + } + $.extend(options.icons, icons); + if (widget) { + hide(); + show(); + } + return picker; }; - picker.enable = function () { - var input = getPickerInput(); - if (!input.prop('disabled')) { - return; + picker.useStrict = function (useStrict) { + if (arguments.length === 0) { + return options.useStrict; } - input.prop('disabled', false); - attachDatePickerEvents(); + + if (typeof useStrict !== 'boolean') { + throw new TypeError('useStrict() expects a boolean parameter'); + } + options.useStrict = useStrict; + return picker; }; - picker.hide = function () { - // Ignore event if in the middle of a picker transition - var collapse = picker.widget.find('.collapse'), i, collapseData; - for (i = 0; i < collapse.length; i++) { - collapseData = collapse.eq(i).data('collapse'); - if (collapseData && collapseData.transitioning) { - return; - } + picker.sideBySide = function (sideBySide) { + if (arguments.length === 0) { + return options.sideBySide; } - picker.widget.hide(); - picker.widget.removeClass('picker-open'); - picker.viewMode = picker.startViewMode; + + if (typeof sideBySide !== 'boolean') { + throw new TypeError('sideBySide() expects a boolean parameter'); + } + options.sideBySide = sideBySide; + if (widget) { + hide(); + show(); + } + return picker; + }; + + picker.viewMode = function (newViewMode) { + if (arguments.length === 0) { + return options.viewMode; + } + + if (typeof newViewMode !== 'string') { + throw new TypeError('viewMode() expects a string parameter'); + } + + if (viewModes.indexOf(newViewMode) === -1) { + throw new TypeError('viewMode() parameter must be one of (' + viewModes.join(', ') + ') value'); + } + + options.viewMode = newViewMode; + currentViewMode = Math.max(viewModes.indexOf(newViewMode), minViewModeNumber); + showMode(); - picker.element.trigger({ - type: 'dp.hide', - date: moment(picker.date) - }); - detachDatePickerGlobalEvents(); + return picker; }; - picker.setValue = function (newDate) { - moment.locale(picker.options.language); - if (!newDate) { - picker.unset = true; - set(); - } else { - picker.unset = false; + picker.toolbarPlacement = function (toolbarPlacement) { + if (arguments.length === 0) { + return options.toolbarPlacement; } - if (!moment.isMoment(newDate)) { - newDate = (newDate instanceof Date) ? moment(newDate) : moment(newDate, picker.format, picker.options.useStrict); - } else { - newDate = newDate.locale(picker.options.language); + + if (typeof toolbarPlacement !== 'string') { + throw new TypeError('toolbarPlacement() expects a string parameter'); } - if (newDate.isValid()) { - picker.date = newDate; - set(); - picker.viewDate = moment({y: picker.date.year(), M: picker.date.month()}); - fillDate(); - fillTime(); + if (toolbarPlacements.indexOf(toolbarPlacement) === -1) { + throw new TypeError('toolbarPlacement() parameter must be one of (' + toolbarPlacements.join(', ') + ') value'); } - else { - notifyError(newDate); + options.toolbarPlacement = toolbarPlacement; + + if (widget) { + hide(); + show(); } + return picker; }; - picker.getDate = function () { - if (picker.unset) { - return null; + picker.widgetPositioning = function (widgetPositioning) { + if (arguments.length === 0) { + return $.extend({}, options.widgetPositioning); } - return moment(picker.date); - }; - picker.setDate = function (date) { - var oldDate = moment(picker.date); - if (!date) { - picker.setValue(null); - } else { - picker.setValue(date); + if (({}).toString.call(widgetPositioning) !== '[object Object]') { + throw new TypeError('widgetPositioning() expects an object variable'); } - notifyChange(oldDate, 'function'); + if (widgetPositioning.horizontal) { + if (typeof widgetPositioning.horizontal !== 'string') { + throw new TypeError('widgetPositioning() horizontal variable must be a string'); + } + widgetPositioning.horizontal = widgetPositioning.horizontal.toLowerCase(); + if (horizontalModes.indexOf(widgetPositioning.horizontal) === -1) { + throw new TypeError('widgetPositioning() expects horizontal parameter to be one of (' + horizontalModes.join(', ') + ')'); + } + options.widgetPositioning.horizontal = widgetPositioning.horizontal; + } + if (widgetPositioning.vertical) { + if (typeof widgetPositioning.vertical !== 'string') { + throw new TypeError('widgetPositioning() vertical variable must be a string'); + } + widgetPositioning.vertical = widgetPositioning.vertical.toLowerCase(); + if (verticalModes.indexOf(widgetPositioning.vertical) === -1) { + throw new TypeError('widgetPositioning() expects vertical parameter to be one of (' + verticalModes.join(', ') + ')'); + } + options.widgetPositioning.vertical = widgetPositioning.vertical; + } + update(); + return picker; }; - picker.setDisabledDates = function (dates) { - picker.options.disabledDates = indexGivenDates(dates); - if (picker.viewDate) { - update(); + picker.calendarWeeks = function (showCalendarWeeks) { + if (arguments.length === 0) { + return options.calendarWeeks; } + + if (typeof showCalendarWeeks !== 'boolean') { + throw new TypeError('calendarWeeks() expects parameter to be a boolean value'); + } + + options.calendarWeeks = showCalendarWeeks; + update(); + return picker; }; - picker.setEnabledDates = function (dates) { - picker.options.enabledDates = indexGivenDates(dates); - if (picker.viewDate) { - update(); + picker.showTodayButton = function (showTodayButton) { + if (arguments.length === 0) { + return options.showTodayButton; } + + if (typeof showTodayButton !== 'boolean') { + throw new TypeError('showTodayButton() expects a boolean parameter'); + } + + options.showTodayButton = showTodayButton; + if (widget) { + hide(); + show(); + } + return picker; }; - picker.setMaxDate = function (date) { - if (date === undefined) { - return; + picker.showClear = function (showClear) { + if (arguments.length === 0) { + return options.showClear; } - if (moment.isMoment(date) || date instanceof Date) { - picker.options.maxDate = moment(date); - } else { - picker.options.maxDate = moment(date, picker.format, picker.options.useStrict); + + if (typeof showClear !== 'boolean') { + throw new TypeError('showClear() expects a boolean parameter'); } - if (picker.viewDate) { - update(); + + options.showClear = showClear; + if (widget) { + hide(); + show(); } + return picker; }; - picker.setMinDate = function (date) { - if (date === undefined) { - return; + picker.widgetParent = function (widgetParent) { + if (arguments.length === 0) { + return options.widgetParent; } - if (moment.isMoment(date) || date instanceof Date) { - picker.options.minDate = moment(date); - } else { - picker.options.minDate = moment(date, picker.format, picker.options.useStrict); + + if (typeof widgetParent === 'string') { + widgetParent = $(widgetParent); } - if (picker.viewDate) { - update(); + + if (widgetParent !== null && (typeof widgetParent !== 'string' && !(widgetParent instanceof jQuery))) { + throw new TypeError('widgetParent() expects a string or a jQuery object parameter'); } + + options.widgetParent = widgetParent; + if (widget) { + hide(); + show(); + } + return picker; }; - init(); + picker.keepOpen = function (keepOpen) { + if (arguments.length === 0) { + return options.format; + } + + if (typeof keepOpen !== 'boolean') { + throw new TypeError('keepOpen() expects a boolean parameter'); + } + + options.keepOpen = keepOpen; + return picker; + }; + + // initializing element and component attributes + if (element.is('input')) { + input = element; + } else { + input = element.find('.datepickerinput'); + if (input.size() === 0) { + input = element.find('input'); + } else if (!input.is('input')) { + throw new Error('CSS class "datepickerinput" cannot be applied to non input element'); + } + } + + if (element.hasClass('input-group')) { + // in case there is more then one 'input-group-addon' Issue #48 + if (element.find('.datepickerbutton').size() === 0) { + component = element.find('[class^="input-group-"]'); + } else { + component = element.find('.datepickerbutton'); + } + } + + if (!input.is('input')) { + throw new Error('Could not initialize DateTimePicker without an input element'); + } + + $.extend(true, options, dataToOptions()); + + picker.options(options); + + initFormatting(); + + attachDatePickerElementEvents(); + + if (input.prop('disabled')) { + picker.disable(); + } + + if (input.val().trim().length !== 0) { + setValue(parseInputDate(input.val().trim())); + } else if (options.defaultDate) { + setValue(options.defaultDate); + } + + return picker; }; + /******************************************************************************** + * + * jQuery plugin constructor and defaults object + * + ********************************************************************************/ + $.fn.datetimepicker = function (options) { return this.each(function () { - var $this = $(this), - data = $this.data('DateTimePicker'); - if (!data) { - $this.data('DateTimePicker', new DateTimePicker(this, options)); + var $this = $(this); + if (!$this.data('DateTimePicker')) { + // create a private copy of the defaults object + options = $.extend(true, {}, $.fn.datetimepicker.defaults, options); + $this.data('DateTimePicker', dateTimePicker($this, options)); } }); }; $.fn.datetimepicker.defaults = { format: false, - pickDate: true, - pickTime: true, - useMinutes: true, - useSeconds: false, + dayViewHeaderFormat: 'MMMM YYYY', + extraFormats: false, + stepping: 1, + minDate: false, + maxDate: false, useCurrent: true, - calendarWeeks: false, - minuteStepping: 1, - minDate: moment({y: 1900}), - maxDate: moment().add(100, 'y'), - showToday: true, collapse: true, - language: moment.locale(), - defaultDate: '', + locale: moment.locale(), + defaultDate: false, disabledDates: false, enabledDates: false, - icons: {}, + icons: { + time: 'glyphicon glyphicon-time', + date: 'glyphicon glyphicon-calendar', + up: 'glyphicon glyphicon-chevron-up', + down: 'glyphicon glyphicon-chevron-down', + previous: 'glyphicon glyphicon-chevron-left', + next: 'glyphicon glyphicon-chevron-right', + today: 'glyphicon glyphicon-screenshot', + clear: 'glyphicon glyphicon-trash' + }, useStrict: false, - direction: 'auto', sideBySide: false, daysOfWeekDisabled: [], - widgetParent: false + calendarWeeks: false, + viewMode: 'days', + toolbarPlacement: 'default', + showTodayButton: false, + showClear: false, + widgetPositioning: { + horizontal: 'auto', + vertical: 'auto' + }, + widgetParent: null, + keepOpen: false }; }));