


angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootstrap.position'])

.value('$datepickerSuppressError', false)

.constant('uibDatepickerConfig', {
    formatDay: 'dd',
    formatMonth: 'MMMM',
    formatYear: 'yyyy',
    formatDayHeader: 'EEE',
    formatDayTitle: 'MMMM yyyy',
    formatMonthTitle: 'yyyy',
    datepickerMode: 'day',
    minMode: 'day',
    maxMode: 'year',
    showWeeks: true,
    startingDay: 0,
    yearRange: 20,
    minDate: null,
    maxDate: null,
    shortcutPropagation: false
})

.controller('UibDatepickerController', ['$scope', '$attrs', '$parse', '$interpolate', '$log', 'dateFilter', 'uibDatepickerConfig', '$datepickerSuppressError', function ($scope, $attrs, $parse, $interpolate, $log, dateFilter, datepickerConfig, $datepickerSuppressError) {
    var self = this,
        ngModelCtrl = { $setViewValue: angular.noop }; // nullModelCtrl;

    // Modes chain
    this.modes = ['day', 'month', 'year'];

    // Interpolated configuration attributes
    angular.forEach(['formatDay', 'formatMonth', 'formatYear', 'formatDayHeader', 'formatDayTitle', 'formatMonthTitle'], function (key) {
        self[key] = angular.isDefined($attrs[key]) ? $interpolate($attrs[key])($scope.$parent) : datepickerConfig[key];
    });

    // Evaled configuration attributes
    angular.forEach(['showWeeks', 'startingDay', 'yearRange', 'shortcutPropagation'], function (key) {
        self[key] = angular.isDefined($attrs[key]) ? $scope.$parent.$eval($attrs[key]) : datepickerConfig[key];
    });

    // Watchable date attributes
    angular.forEach(['minDate', 'maxDate'], function (key) {
        if ($attrs[key]) {
            $scope.$parent.$watch($attrs[key], function (value) {
                self[key] = value ? new Date(value) : null;
                self.refreshView();
            });
        } else {
            self[key] = datepickerConfig[key] ? new Date(datepickerConfig[key]) : null;
        }
    });

    angular.forEach(['minMode', 'maxMode'], function (key) {
        if ($attrs[key]) {
            $scope.$parent.$watch($attrs[key], function (value) {
                self[key] = $scope[key] = angular.isDefined(value) ? value : $attrs[key];
                if (key === 'minMode' && self.modes.indexOf($scope.datepickerMode) < self.modes.indexOf(self[key]) ||
                  key === 'maxMode' && self.modes.indexOf($scope.datepickerMode) > self.modes.indexOf(self[key])) {
                    $scope.datepickerMode = self[key];
                }
            });
        } else {
            self[key] = $scope[key] = datepickerConfig[key] || null;
        }
    });

    $scope.datepickerMode = $scope.datepickerMode || datepickerConfig.datepickerMode;
    $scope.uniqueId = 'datepicker-' + $scope.$id + '-' + Math.floor(Math.random() * 10000);

    if (angular.isDefined($attrs.initDate)) {
        this.activeDate = $scope.$parent.$eval($attrs.initDate) || new Date();
        $scope.$parent.$watch($attrs.initDate, function (initDate) {
            if (initDate && (ngModelCtrl.$isEmpty(ngModelCtrl.$modelValue) || ngModelCtrl.$invalid)) {
                self.activeDate = initDate;
                self.refreshView();
            }
        });
    } else {
        this.activeDate = new Date();
    }

    $scope.disabled = angular.isDefined($attrs.disabled) || false;
    if (angular.isDefined($attrs.ngDisabled)) {
        $scope.$parent.$watch($attrs.ngDisabled, function (disabled) {
            $scope.disabled = disabled;
            self.refreshView();
        });
    }

    $scope.isActive = function (dateObject) {
        if (self.compare(dateObject.date, self.activeDate) === 0) {
            $scope.activeDateId = dateObject.uid;
            return true;
        }
        return false;
    };

    this.init = function (ngModelCtrl_) {
        ngModelCtrl = ngModelCtrl_;

        ngModelCtrl.$render = function () {
            self.render();
        };
    };

    this.render = function () {
        if (ngModelCtrl.$viewValue) {
            var date = new Date(ngModelCtrl.$viewValue),
                isValid = !isNaN(date);

            if (isValid) {
                this.activeDate = date;
            } else if (!$datepickerSuppressError) {
                $log.error('Datepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.');
            }
        }
        this.refreshView();
    };

    this.refreshView = function () {
        if (this.element) {
            this._refreshView();

            var date = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null;
            ngModelCtrl.$setValidity('dateDisabled', !date ||
              this.element && !this.isDisabled(date));
        }
    };

    this.createDateObject = function (date, format) {
        var model = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null;
        return {
            date: date,
            label: dateFilter(date, format),
            selected: model && this.compare(date, model) === 0,
            disabled: this.isDisabled(date),
            current: this.compare(date, new Date()) === 0,
            customClass: this.customClass(date)
        };
    };

    this.isDisabled = function (date) {
        return $scope.disabled ||
          this.minDate && this.compare(date, this.minDate) < 0 ||
          this.maxDate && this.compare(date, this.maxDate) > 0 ||
          $attrs.dateDisabled && $scope.dateDisabled({ date: date, mode: $scope.datepickerMode });
    };

    this.customClass = function (date) {
        return $scope.customClass({ date: date, mode: $scope.datepickerMode });
    };

    // Split array into smaller arrays
    this.split = function (arr, size) {
        var arrays = [];
        while (arr.length > 0) {
            arrays.push(arr.splice(0, size));
        }
        return arrays;
    };

    $scope.select = function (date) {
        if ($scope.datepickerMode === self.minMode) {
            var dt = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : new Date(0, 0, 0, 0, 0, 0, 0);
            dt.setFullYear(date.getFullYear(), date.getMonth(), date.getDate());
            ngModelCtrl.$setViewValue(dt);
            ngModelCtrl.$render();
        } else {
            self.activeDate = date;
            $scope.datepickerMode = self.modes[self.modes.indexOf($scope.datepickerMode) - 1];
        }
    };

    $scope.move = function (direction) {
        var year = self.activeDate.getFullYear() + direction * (self.step.years || 0),
            month = self.activeDate.getMonth() + direction * (self.step.months || 0);
        self.activeDate.setFullYear(year, month, 1);
        self.refreshView();
    };

    $scope.toggleMode = function (direction) {
        direction = direction || 1;

        if ($scope.datepickerMode === self.maxMode && direction === 1 ||
          $scope.datepickerMode === self.minMode && direction === -1) {
            return;
        }

        $scope.datepickerMode = self.modes[self.modes.indexOf($scope.datepickerMode) + direction];
    };

    // Key event mapper
    $scope.keys = { 13: 'enter', 32: 'space', 33: 'pageup', 34: 'pagedown', 35: 'end', 36: 'home', 37: 'left', 38: 'up', 39: 'right', 40: 'down' };

    var focusElement = function () {
        self.element[0].focus();
    };

    // Listen for focus requests from popup directive
    $scope.$on('uib:datepicker.focus', focusElement);

    $scope.keydown = function (evt) {
        var key = $scope.keys[evt.which];

        if (!key || evt.shiftKey || evt.altKey || $scope.disabled) {
            return;
        }

        evt.preventDefault();
        if (!self.shortcutPropagation) {
            evt.stopPropagation();
        }

        if (key === 'enter' || key === 'space') {
            if (self.isDisabled(self.activeDate)) {
                return; // do nothing
            }
            $scope.select(self.activeDate);
        } else if (evt.ctrlKey && (key === 'up' || key === 'down')) {
            $scope.toggleMode(key === 'up' ? 1 : -1);
        } else {
            self.handleKeyDown(key, evt);
            self.refreshView();
        }
    };
}])

.controller('UibDaypickerController', ['$scope', '$element', 'dateFilter', function (scope, $element, dateFilter) {
    var DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];

    this.step = { months: 1 };
    this.element = $element;
    function getDaysInMonth(year, month) {
        return month === 1 && year % 4 === 0 &&
          (year % 100 !== 0 || year % 400 === 0) ? 29 : DAYS_IN_MONTH[month];
    }

    this.init = function (ctrl) {
        angular.extend(ctrl, this);
        scope.showWeeks = ctrl.showWeeks;
        ctrl.refreshView();
    };

    this.getDates = function (startDate, n) {
        var dates = new Array(n), current = new Date(startDate), i = 0, date;
        while (i < n) {
            date = new Date(current);
            dates[i++] = date;
            current.setDate(current.getDate() + 1);
        }
        return dates;
    };

    this._refreshView = function () {
        var year = this.activeDate.getFullYear(),
          month = this.activeDate.getMonth(),
          firstDayOfMonth = new Date(this.activeDate);

        firstDayOfMonth.setFullYear(year, month, 1);

        var difference = this.startingDay - firstDayOfMonth.getDay(),
          numDisplayedFromPreviousMonth = difference > 0 ?
            7 - difference : -difference,
          firstDate = new Date(firstDayOfMonth);

        if (numDisplayedFromPreviousMonth > 0) {
            firstDate.setDate(-numDisplayedFromPreviousMonth + 1);
        }

        // 42 is the number of days on a six-week calendar
        var days = this.getDates(firstDate, 42);
        for (var i = 0; i < 42; i++) {
            days[i] = angular.extend(this.createDateObject(days[i], this.formatDay), {
                secondary: days[i].getMonth() !== month,
                uid: scope.uniqueId + '-' + i
            });
        }

        scope.labels = new Array(7);
        for (var j = 0; j < 7; j++) {
            scope.labels[j] = {
                abbr: dateFilter(days[j].date, this.formatDayHeader),
                full: dateFilter(days[j].date, 'EEEE')
            };
        }

        scope.title = dateFilter(this.activeDate, this.formatDayTitle);
        scope.rows = this.split(days, 7);

        if (scope.showWeeks) {
            scope.weekNumbers = [];
            var thursdayIndex = (4 + 7 - this.startingDay) % 7,
                numWeeks = scope.rows.length;
            for (var curWeek = 0; curWeek < numWeeks; curWeek++) {
                scope.weekNumbers.push(
                  getISO8601WeekNumber(scope.rows[curWeek][thursdayIndex].date));
            }
        }
    };

    this.compare = function (date1, date2) {
        var _date1 = new Date(date1.getFullYear(), date1.getMonth(), date1.getDate());
        var _date2 = new Date(date2.getFullYear(), date2.getMonth(), date2.getDate());
        _date1.setFullYear(date1.getFullYear());
        _date2.setFullYear(date2.getFullYear());
        return _date1 - _date2;
    };

    function getISO8601WeekNumber(date) {
        var checkDate = new Date(date);
        checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7)); // Thursday
        var time = checkDate.getTime();
        checkDate.setMonth(0); // Compare with Jan 1
        checkDate.setDate(1);
        return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1;
    }

    this.handleKeyDown = function (key, evt) {
        var date = this.activeDate.getDate();

        if (key === 'left') {
            date = date - 1;
        } else if (key === 'up') {
            date = date - 7;
        } else if (key === 'right') {
            date = date + 1;
        } else if (key === 'down') {
            date = date + 7;
        } else if (key === 'pageup' || key === 'pagedown') {
            var month = this.activeDate.getMonth() + (key === 'pageup' ? -1 : 1);
            this.activeDate.setMonth(month, 1);
            date = Math.min(getDaysInMonth(this.activeDate.getFullYear(), this.activeDate.getMonth()), date);
        } else if (key === 'home') {
            date = 1;
        } else if (key === 'end') {
            date = getDaysInMonth(this.activeDate.getFullYear(), this.activeDate.getMonth());
        }
        this.activeDate.setDate(date);
    };
}])

.controller('UibMonthpickerController', ['$scope', '$element', 'dateFilter', function (scope, $element, dateFilter) {
    this.step = { years: 1 };
    this.element = $element;

    this.init = function (ctrl) {
        angular.extend(ctrl, this);
        ctrl.refreshView();
    };

    this._refreshView = function () {
        var months = new Array(12),
            year = this.activeDate.getFullYear(),
            date;

        for (var i = 0; i < 12; i++) {
            date = new Date(this.activeDate);
            date.setFullYear(year, i, 1);
            months[i] = angular.extend(this.createDateObject(date, this.formatMonth), {
                uid: scope.uniqueId + '-' + i
            });
        }

        scope.title = dateFilter(this.activeDate, this.formatMonthTitle);
        scope.rows = this.split(months, 3);
    };

    this.compare = function (date1, date2) {
        var _date1 = new Date(date1.getFullYear(), date1.getMonth());
        var _date2 = new Date(date2.getFullYear(), date2.getMonth());
        _date1.setFullYear(date1.getFullYear());
        _date2.setFullYear(date2.getFullYear());
        return _date1 - _date2;
    };

    this.handleKeyDown = function (key, evt) {
        var date = this.activeDate.getMonth();

        if (key === 'left') {
            date = date - 1;
        } else if (key === 'up') {
            date = date - 3;
        } else if (key === 'right') {
            date = date + 1;
        } else if (key === 'down') {
            date = date + 3;
        } else if (key === 'pageup' || key === 'pagedown') {
            var year = this.activeDate.getFullYear() + (key === 'pageup' ? -1 : 1);
            this.activeDate.setFullYear(year);
        } else if (key === 'home') {
            date = 0;
        } else if (key === 'end') {
            date = 11;
        }
        this.activeDate.setMonth(date);
    };
}])

.controller('UibYearpickerController', ['$scope', '$element', 'dateFilter', function (scope, $element, dateFilter) {
    var range;
    this.element = $element;

    function getStartingYear(year) {
        return parseInt((year - 1) / range, 10) * range + 1;
    }

    this.yearpickerInit = function () {
        range = this.yearRange;
        this.step = { years: range };
    };

    this._refreshView = function () {
        var years = new Array(range), date;

        for (var i = 0, start = getStartingYear(this.activeDate.getFullYear()) ; i < range; i++) {
            date = new Date(this.activeDate);
            date.setFullYear(start + i, 0, 1);
            years[i] = angular.extend(this.createDateObject(date, this.formatYear), {
                uid: scope.uniqueId + '-' + i
            });
        }

        scope.title = [years[0].label, years[range - 1].label].join(' - ');
        scope.rows = this.split(years, 5);
    };

    this.compare = function (date1, date2) {
        return date1.getFullYear() - date2.getFullYear();
    };

    this.handleKeyDown = function (key, evt) {
        var date = this.activeDate.getFullYear();

        if (key === 'left') {
            date = date - 1;
        } else if (key === 'up') {
            date = date - 5;
        } else if (key === 'right') {
            date = date + 1;
        } else if (key === 'down') {
            date = date + 5;
        } else if (key === 'pageup' || key === 'pagedown') {
            date += (key === 'pageup' ? -1 : 1) * this.step.years;
        } else if (key === 'home') {
            date = getStartingYear(this.activeDate.getFullYear());
        } else if (key === 'end') {
            date = getStartingYear(this.activeDate.getFullYear()) + range - 1;
        }
        this.activeDate.setFullYear(date);
    };
}])

.directive('uibDatepicker', function () {
    return {
        replace: true,
        templateUrl: function (element, attrs) {
            return attrs.templateUrl || 'uib/template/datepicker/datepicker.html';
        },
        scope: {
            datepickerMode: '=?',
            dateDisabled: '&',
            customClass: '&',
            shortcutPropagation: '&?'
        },
        require: ['uibDatepicker', '^ngModel'],
        controller: 'UibDatepickerController',
        controllerAs: 'datepicker',
        link: function (scope, element, attrs, ctrls) {
            var datepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1];

            datepickerCtrl.init(ngModelCtrl);
        }
    };
})

.directive('uibDaypicker', function () {
    return {
        replace: true,
        templateUrl: function (element, attrs) {
            return attrs.templateUrl || 'uib/template/datepicker/day.html';
        },
        require: ['^uibDatepicker', 'uibDaypicker'],
        controller: 'UibDaypickerController',
        link: function (scope, element, attrs, ctrls) {
            var datepickerCtrl = ctrls[0],
              daypickerCtrl = ctrls[1];

            daypickerCtrl.init(datepickerCtrl);
        }
    };
})

.directive('uibMonthpicker', function () {
    return {
        replace: true,
        templateUrl: function (element, attrs) {
            return attrs.templateUrl || 'uib/template/datepicker/month.html';
        },
        require: ['^uibDatepicker', 'uibMonthpicker'],
        controller: 'UibMonthpickerController',
        link: function (scope, element, attrs, ctrls) {
            var datepickerCtrl = ctrls[0],
              monthpickerCtrl = ctrls[1];

            monthpickerCtrl.init(datepickerCtrl);
        }
    };
})

.directive('uibYearpicker', function () {
    return {
        replace: true,
        templateUrl: function (element, attrs) {
            return attrs.templateUrl || 'uib/template/datepicker/year.html';
        },
        require: ['^uibDatepicker', 'uibYearpicker'],
        controller: 'UibYearpickerController',
        link: function (scope, element, attrs, ctrls) {
            var ctrl = ctrls[0];
            angular.extend(ctrl, ctrls[1]);
            ctrl.yearpickerInit();

            ctrl.refreshView();
        }
    };
})

.constant('uibDatepickerPopupConfig', {
    datepickerPopup: 'yyyy-MM-dd',
    datepickerPopupTemplateUrl: 'uib/template/datepicker/popup.html',
    datepickerTemplateUrl: 'uib/template/datepicker/datepicker.html',
    html5Types: {
        date: 'yyyy-MM-dd',
        'datetime-local': 'yyyy-MM-ddTHH:mm:ss.sss',
        'month': 'yyyy-MM'
    },
    currentText: 'Today',
    clearText: 'Clear',
    closeText: 'Done',
    closeOnDateSelection: true,
    appendToBody: false,
    showButtonBar: true,
    onOpenFocus: true,
    altInputFormats: []
})

.controller('UibDatepickerPopupController', ['$scope', '$element', '$attrs', '$compile', '$parse', '$document', '$rootScope', '$uibPosition', 'dateFilter', 'uibDateParser', 'uibDatepickerPopupConfig', '$timeout',
function (scope, element, attrs, $compile, $parse, $document, $rootScope, $position, dateFilter, dateParser, datepickerPopupConfig, $timeout) {
    var self = this;
    var cache = {},
      isHtml5DateInput = false;
    var dateFormat, closeOnDateSelection, appendToBody, onOpenFocus,
      datepickerPopupTemplateUrl, datepickerTemplateUrl, popupEl, datepickerEl,
      ngModel, $popup, altInputFormats;

    scope.watchData = {};

    this.init = function (_ngModel_) {
        ngModel = _ngModel_;
        closeOnDateSelection = angular.isDefined(attrs.closeOnDateSelection) ? scope.$parent.$eval(attrs.closeOnDateSelection) : datepickerPopupConfig.closeOnDateSelection;
        appendToBody = angular.isDefined(attrs.datepickerAppendToBody) ? scope.$parent.$eval(attrs.datepickerAppendToBody) : datepickerPopupConfig.appendToBody;
        onOpenFocus = angular.isDefined(attrs.onOpenFocus) ? scope.$parent.$eval(attrs.onOpenFocus) : datepickerPopupConfig.onOpenFocus;
        datepickerPopupTemplateUrl = angular.isDefined(attrs.datepickerPopupTemplateUrl) ? attrs.datepickerPopupTemplateUrl : datepickerPopupConfig.datepickerPopupTemplateUrl;
        datepickerTemplateUrl = angular.isDefined(attrs.datepickerTemplateUrl) ? attrs.datepickerTemplateUrl : datepickerPopupConfig.datepickerTemplateUrl;
        altInputFormats = angular.isDefined(attrs.altInputFormats) ? scope.$parent.$eval(attrs.altInputFormats) : datepickerPopupConfig.altInputFormats;

        scope.showButtonBar = angular.isDefined(attrs.showButtonBar) ? scope.$parent.$eval(attrs.showButtonBar) : datepickerPopupConfig.showButtonBar;

        if (datepickerPopupConfig.html5Types[attrs.type]) {
            dateFormat = datepickerPopupConfig.html5Types[attrs.type];
            isHtml5DateInput = true;
        } else {
            dateFormat = attrs.uibDatepickerPopup || datepickerPopupConfig.datepickerPopup;
            attrs.$observe('uibDatepickerPopup', function (value, oldValue) {
                var newDateFormat = value || datepickerPopupConfig.datepickerPopup;
                // Invalidate the $modelValue to ensure that formatters re-run
                // FIXME: Refactor when PR is merged: https://github.com/angular/angular.js/pull/10764
                if (newDateFormat !== dateFormat) {
                    dateFormat = newDateFormat;
                    ngModel.$modelValue = null;

                    if (!dateFormat) {
                        throw new Error('uibDatepickerPopup must have a date format specified.');
                    }
                }
            });
        }

        if (!dateFormat) {
            throw new Error('uibDatepickerPopup must have a date format specified.');
        }

        if (isHtml5DateInput && attrs.uibDatepickerPopup) {
            throw new Error('HTML5 date input types do not support custom formats.');
        }

        // popup element used to display calendar
        popupEl = angular.element('<div uib-datepicker-popup-wrap><div uib-datepicker></div></div>');
        popupEl.attr({
            'ng-model': 'date',
            'ng-change': 'dateSelection(date)',
            'template-url': datepickerPopupTemplateUrl
        });

        // datepicker element
        datepickerEl = angular.element(popupEl.children()[0]);
        datepickerEl.attr('template-url', datepickerTemplateUrl);

        if (isHtml5DateInput) {
            if (attrs.type === 'month') {
                datepickerEl.attr('datepicker-mode', '"month"');
                datepickerEl.attr('min-mode', 'month');
            }
        }

        if (attrs.datepickerOptions) {
            var options = scope.$parent.$eval(attrs.datepickerOptions);
            if (options && options.initDate) {
                scope.initDate = options.initDate;
                datepickerEl.attr('init-date', 'initDate');
                delete options.initDate;
            }
            angular.forEach(options, function (value, option) {
                datepickerEl.attr(cameltoDash(option), value);
            });
        }

        angular.forEach(['minMode', 'maxMode', 'minDate', 'maxDate', 'datepickerMode', 'initDate', 'shortcutPropagation'], function (key) {
            if (attrs[key]) {
                var getAttribute = $parse(attrs[key]);
                scope.$parent.$watch(getAttribute, function (value) {
                    scope.watchData[key] = value;
                    if (key === 'minDate' || key === 'maxDate') {
                        cache[key] = new Date(value);
                    }
                });
                datepickerEl.attr(cameltoDash(key), 'watchData.' + key);

                // Propagate changes from datepicker to outside
                if (key === 'datepickerMode') {
                    var setAttribute = getAttribute.assign;
                    scope.$watch('watchData.' + key, function (value, oldvalue) {
                        if (angular.isFunction(setAttribute) && value !== oldvalue) {
                            setAttribute(scope.$parent, value);
                        }
                    });
                }
            }
        });

        if (attrs.dateDisabled) {
            datepickerEl.attr('date-disabled', 'dateDisabled({ date: date, mode: mode })');
        }

        angular.forEach(['formatDay', 'formatMonth', 'formatYear', 'formatDayHeader', 'formatDayTitle', 'formatMonthTitle', 'showWeeks', 'startingDay', 'yearRange'], function (key) {
            if (angular.isDefined(attrs[key])) {
                datepickerEl.attr(cameltoDash(key), attrs[key]);
            }
        });

        if (attrs.customClass) {
            datepickerEl.attr('custom-class', 'customClass({ date: date, mode: mode })');
        }

        if (!isHtml5DateInput) {
            // Internal API to maintain the correct ng-invalid-[key] class
            ngModel.$$parserName = 'date';
            ngModel.$validators.date = validator;
            ngModel.$parsers.unshift(parseDate);
            ngModel.$formatters.push(function (value) {
                scope.date = value;
                return ngModel.$isEmpty(value) ? value : dateFilter(value, dateFormat);
            });
        } else {
            ngModel.$formatters.push(function (value) {
                scope.date = value;
                return value;
            });
        }

        // Detect changes in the view from the text box
        ngModel.$viewChangeListeners.push(function () {
            scope.date = parseDateString(ngModel.$viewValue);
        });

        element.bind('keydown', inputKeydownBind);

        $popup = $compile(popupEl)(scope);
        // Prevent jQuery cache memory leak (template is now redundant after linking)
        popupEl.remove();

        if (appendToBody) {
            $document.find('body').append($popup);
        } else {
            element.after($popup);
        }

        scope.$on('$destroy', function () {
            if (scope.isOpen === true) {
                if (!$rootScope.$$phase) {
                    scope.$apply(function () {
                        scope.isOpen = false;
                    });
                }
            }

            $popup.remove();
            element.unbind('keydown', inputKeydownBind);
            $document.unbind('click', documentClickBind);
        });
    };

    scope.getText = function (key) {
        return scope[key + 'Text'] || datepickerPopupConfig[key + 'Text'];
    };

    scope.isDisabled = function (date) {
        if (date === 'today') {
            date = new Date();
        }

        return scope.watchData.minDate && scope.compare(date, cache.minDate) < 0 ||
          scope.watchData.maxDate && scope.compare(date, cache.maxDate) > 0;
    };

    scope.compare = function (date1, date2) {
        return new Date(date1.getFullYear(), date1.getMonth(), date1.getDate()) - new Date(date2.getFullYear(), date2.getMonth(), date2.getDate());
    };

    // Inner change
    scope.dateSelection = function (dt) {
        if (angular.isDefined(dt)) {
            scope.date = dt;
        }
        var date = scope.date ? dateFilter(scope.date, dateFormat) : null; // Setting to NULL is necessary for form validators to function
        element.val(date);
        ngModel.$setViewValue(date);

        if (closeOnDateSelection) {
            scope.isOpen = false;
            element[0].focus();
        }
    };

    scope.keydown = function (evt) {
        if (evt.which === 27) {
            scope.isOpen = false;
            element[0].focus();
        }
    };

    scope.select = function (date) {
        if (date === 'today') {
            var today = new Date();
            if (angular.isDate(scope.date)) {
                date = new Date(scope.date);
                date.setFullYear(today.getFullYear(), today.getMonth(), today.getDate());
            } else {
                date = new Date(today.setHours(0, 0, 0, 0));
            }
        }
        scope.dateSelection(date);
    };

    scope.close = function () {
        scope.isOpen = false;
        element[0].focus();
    };

    scope.disabled = angular.isDefined(attrs.disabled) || false;
    if (attrs.ngDisabled) {
        scope.$parent.$watch($parse(attrs.ngDisabled), function (disabled) {
            scope.disabled = disabled;
        });
    }

    scope.$watch('isOpen', function (value) {
        if (value) {
            if (!scope.disabled) {
                scope.position = appendToBody ? $position.offset(element) : $position.position(element);
                scope.position.top = scope.position.top + element.prop('offsetHeight');

                $timeout(function () {
                    if (onOpenFocus) {
                        scope.$broadcast('uib:datepicker.focus');
                    }
                    $document.bind('click', documentClickBind);
                }, 0, false);
            } else {
                scope.isOpen = false;
            }
        } else {
            $document.unbind('click', documentClickBind);
        }
    });

    function cameltoDash(string) {
        return string.replace(/([A-Z])/g, function ($1) { return '-' + $1.toLowerCase(); });
    }

    function parseDateString(viewValue) {
        var date = dateParser.parse(viewValue, dateFormat, scope.date);
        if (isNaN(date)) {
            for (var i = 0; i < altInputFormats.length; i++) {
                date = dateParser.parse(viewValue, altInputFormats[i], scope.date);
                if (!isNaN(date)) {
                    return date;
                }
            }
        }
        return date;
    }

    function parseDate(viewValue) {
        if (angular.isNumber(viewValue)) {
            // presumably timestamp to date object
            viewValue = new Date(viewValue);
        }

        if (!viewValue) {
            return null;
        }

        if (angular.isDate(viewValue) && !isNaN(viewValue)) {
            return viewValue;
        }

        if (angular.isString(viewValue)) {
            var date = parseDateString(viewValue);
            if (!isNaN(date)) {
                return date;
            }
        }

        return ngModel.$options && ngModel.$options.allowInvalid ? viewValue : undefined;
    }

    function validator(modelValue, viewValue) {
        var value = modelValue || viewValue;

        if (!attrs.ngRequired && !value) {
            return true;
        }

        if (angular.isNumber(value)) {
            value = new Date(value);
        }

        if (!value) {
            return true;
        }

        if (angular.isDate(value) && !isNaN(value)) {
            return true;
        }

        if (angular.isString(value)) {
            return !isNaN(parseDateString(viewValue));
        }

        return false;
    }

    function documentClickBind(event) {
        if (!scope.isOpen && scope.disabled) {
            return;
        }

        var popup = $popup[0];
        var dpContainsTarget = element[0].contains(event.target);
        // The popup node may not be an element node
        // In some browsers (IE) only element nodes have the 'contains' function
        var popupContainsTarget = popup.contains !== undefined && popup.contains(event.target);
        if (scope.isOpen && !(dpContainsTarget || popupContainsTarget)) {
            scope.$apply(function () {
                scope.isOpen = false;
            });
        }
    }

    function inputKeydownBind(evt) {
        if (evt.which === 27 && scope.isOpen) {
            evt.preventDefault();
            evt.stopPropagation();
            scope.$apply(function () {
                scope.isOpen = false;
            });
            element[0].focus();
        } else if (evt.which === 40 && !scope.isOpen) {
            evt.preventDefault();
            evt.stopPropagation();
            scope.$apply(function () {
                scope.isOpen = true;
            });
        }
    }
}])

.directive('uibDatepickerPopup', function () {
    return {
        require: ['ngModel', 'uibDatepickerPopup'],
        controller: 'UibDatepickerPopupController',
        scope: {
            isOpen: '=?',
            currentText: '@',
            clearText: '@',
            closeText: '@',
            dateDisabled: '&',
            customClass: '&'
        },
        link: function (scope, element, attrs, ctrls) {
            var ngModel = ctrls[0],
              ctrl = ctrls[1];

            ctrl.init(ngModel);
        }
    };
})

.directive('uibDatepickerPopupWrap', function () {
    return {
        replace: true,
        transclude: true,
        templateUrl: function (element, attrs) {
            return attrs.templateUrl || 'uib/template/datepicker/popup.html';
        }
    };
});