diff --git a/src/datepicker/datepicker.js b/src/datepicker/datepicker.js index 3cbbfe9809..36f93cc319 100644 --- a/src/datepicker/datepicker.js +++ b/src/datepicker/datepicker.js @@ -206,7 +206,9 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst datepickerMode: '=?', dateDisabled: '&', customClass: '&', - shortcutPropagation: '&?' + shortcutPropagation: '&?', + minDate: '@', + maxDate: '@' }, require: ['datepicker', '?^ngModel'], controller: 'DatepickerController', @@ -577,6 +579,62 @@ function ($compile, $parse, $document, $position, dateFilter, dateParser, datepi datepickerEl.attr('custom-class', 'customClass({ date: date, mode: mode })'); } + function compareDates(date1, date2) { + if (isNaN(date1) || isNaN(date2)) { + return undefined; + } + else { + return (new Date(date1.getFullYear(), date1.getMonth(), date1.getDate()) - new Date(date2.getFullYear(), date2.getMonth(), date2.getDate()) ); + } + } + + function getDateLimitToCheck(limitName) { + var watchDate = scope.watchData[limitName]; + if (!watchDate) { + return null; + } else { + return new Date(watchDate); + } + } + + function isDateLimitMet(limitName, dateToCheck, viewValue) { + var parsedDate = dateParser.parse(viewValue, dateFormat); + var dateCompare = compareDates(parsedDate, dateToCheck); + if (limitName == 'minDate') { + return !dateCompare || dateCompare > 0; + } else if (limitName == 'maxDate') { + return !dateCompare || dateCompare < 0; + } + } + + function dateLimitParseFormat(limitName, viewValue, isFormatOnly) { + var dateLimit = getDateLimitToCheck(limitName); + if (dateLimit) { + var isMet = isDateLimitMet(limitName, dateLimit, viewValue); + ngModel.$setValidity(limitName, isMet); + return (isFormatOnly || isMet) ? viewValue : undefined; + } else { + return viewValue; + } + } + + function minLimitParse(viewValue) { + return dateLimitParseFormat('minDate', viewValue, false); + } + + function minLimitFormat(viewValue) { + return dateLimitParseFormat('minDate', viewValue, true); + } + + function maxLimitParse(viewValue) { + return dateLimitParseFormat('maxDate', viewValue, false); + } + + function maxLimitFormat(viewValue) { + return dateLimitParseFormat('maxDate', viewValue, true); + } + + function parseDate(viewValue) { if (angular.isNumber(viewValue)) { // presumably timestamp to date object @@ -616,14 +674,34 @@ function ($compile, $parse, $document, $position, dateFilter, dateParser, datepi } } + function parsing(viewValue) { + var result = parseDate(viewValue); + if (result) { + var minMet = minLimitParse(viewValue); + var maxMet = maxLimitParse(viewValue); + result = (minMet && maxMet) ? result : undefined; + } + return result; + } + if (!isHtml5DateInput) { // Internal API to maintain the correct ng-invalid-[key] class ngModel.$$parserName = 'date'; ngModel.$validators.date = validator; - ngModel.$parsers.unshift(parseDate); + ngModel.$parsers.unshift(parsing); ngModel.$formatters.push(function (value) { scope.date = value; - return ngModel.$isEmpty(value) ? value : dateFilter(value, dateFormat); + if (ngModel.$isEmpty(value)) { + return value; + } + else { + value = minLimitFormat(value); + if (value) { + value = maxLimitFormat(value); + } + value = maxLimitFormat(value); + return dateFilter(value, dateFormat); + } }); } else { diff --git a/src/datepicker/docs/demo.html b/src/datepicker/docs/demo.html index 7ea3b98fd2..ba0c0c41e3 100644 --- a/src/datepicker/docs/demo.html +++ b/src/datepicker/docs/demo.html @@ -31,7 +31,7 @@

Popup

- + @@ -49,4 +49,4 @@

Popup

-
\ No newline at end of file + diff --git a/src/datepicker/docs/demo.js b/src/datepicker/docs/demo.js index 1412d392c4..268b7f102f 100644 --- a/src/datepicker/docs/demo.js +++ b/src/datepicker/docs/demo.js @@ -1,11 +1,11 @@ angular.module('ui.bootstrap.demo').controller('DatepickerDemoCtrl', function ($scope) { $scope.today = function() { - $scope.dt = new Date(); + $scope.pickerDate.dt = new Date(); }; $scope.today(); $scope.clear = function () { - $scope.dt = null; + $scope.pickerDate.dt = null; }; // Disable weekend selection diff --git a/src/datepicker/test/datepicker.spec.js b/src/datepicker/test/datepicker.spec.js index 95a121347c..62db5dbd2b 100644 --- a/src/datepicker/test/datepicker.spec.js +++ b/src/datepicker/test/datepicker.spec.js @@ -1199,8 +1199,12 @@ describe('datepicker directive', function () { $document = _$document_; $sniffer = _$sniffer_; $rootScope.isopen = true; + $rootScope.format='yyyy-MM-dd'; $rootScope.date = new Date('September 30, 2010 15:30:00'); - var wrapElement = $compile('
')($rootScope); + $rootScope.minimumDate = new Date('January 1, 1970'); + $rootScope.maximumDate = new Date('January 1, 2020'); + $rootScope.dateDisabledHandler = jasmine.createSpy('dateDisabledHandler'); + var wrapElement = $compile('
')($rootScope); $rootScope.$digest(); assignElements(wrapElement); })); @@ -1238,8 +1242,46 @@ describe('datepicker directive', function () { $rootScope.date = new Date('January 10, 1983 10:00:00'); $rootScope.$digest(); expect(inputEl.val()).toBe('1983-01-10'); + expect(inputEl).not.toHaveClass('ng-invalid'); + expect(inputEl).not.toHaveClass('ng-invalid-min-date'); + expect(inputEl).not.toHaveClass('ng-invalid-max-date'); + }); + + it('updates the input correctly when model changes to a date prior to minimum date', function() { + $rootScope.date = new Date('January 10, 1963 10:00:00'); + $rootScope.$digest(); + expect(inputEl.val()).toBe('1963-01-10'); + expect(inputEl).toHaveClass('ng-invalid'); + expect(inputEl).toHaveClass('ng-invalid-min-date'); + expect(inputEl).not.toHaveClass('ng-invalid-max-date'); + }); + + it('input is not marked with invalid minimum date when minimum is not a valid date', function() { + expect(inputEl).not.toHaveClass('ng-invalid-min-date'); + $rootScope.minimumDate = 'pizza'; + $rootScope.$digest(); + expect(inputEl).not.toHaveClass('ng-invalid-min-date'); + expect(inputEl).not.toHaveClass('ng-invalid-date'); }); + it('updates the input correctly when model changes to a date past the maximum date', function() { + $rootScope.date = new Date('January 10, 2023 10:00:00'); + $rootScope.$digest(); + expect(inputEl.val()).toBe('2023-01-10'); + expect(inputEl).toHaveClass('ng-invalid'); + expect(inputEl).toHaveClass('ng-invalid-max-date'); + expect(inputEl).not.toHaveClass('ng-invalid-min-date'); + expect(inputEl).not.toHaveClass('ng-invalid-date'); + }); + + it('input is not marked with invalid maximum date when maximum is not a valid date', function() { + expect(inputEl).not.toHaveClass('ng-invalid-max-date'); + $rootScope.maximumDate = new Date('pizza'); + $rootScope.$digest(); + expect(inputEl).not.toHaveClass('ng-invalid-max-date'); + }); + + it('closes the dropdown when a day is clicked', function() { expect(dropdownEl.css('display')).not.toBe('none'); @@ -1290,6 +1332,40 @@ describe('datepicker directive', function () { expect(inputEl).not.toHaveClass('ng-invalid'); expect(inputEl).not.toHaveClass('ng-invalid-date'); }); + it('sets `ng-invalid` for date prior to minimum date', function() { + changeInputValueTo(inputEl, '1960-12-01'); + + expect(inputEl).toHaveClass('ng-invalid'); + expect(inputEl).toHaveClass('ng-invalid-min-date'); + expect(inputEl).not.toHaveClass('ng-invalid-max-date'); + expect(inputEl).toHaveClass('ng-invalid-date'); + expect($rootScope.date).toBeUndefined(); + expect(inputEl.val()).toBe('1960-12-01'); + }); + + it('sets `ng-invalid` for date (european format) prior to minimum date', function() { + $rootScope.format='dd.MM.yyyy'; + $rootScope.$digest(); + changeInputValueTo(inputEl, '20.12.1960'); + + expect(inputEl).toHaveClass('ng-invalid'); + expect(inputEl).toHaveClass('ng-invalid-min-date'); + expect(inputEl).not.toHaveClass('ng-invalid-max-date'); + expect(inputEl).toHaveClass('ng-invalid-date'); + expect($rootScope.date).toBeUndefined(); + expect(inputEl.val()).toBe('20.12.1960'); + }); + + it('sets `ng-invalid` for date past the maximum date', function() { + changeInputValueTo(inputEl, '2050-12-01'); + + expect(inputEl).toHaveClass('ng-invalid'); + expect(inputEl).not.toHaveClass('ng-invalid-min-date'); + expect(inputEl).toHaveClass('ng-invalid-max-date'); + expect(inputEl).toHaveClass('ng-invalid-date'); + expect($rootScope.date).toBeUndefined(); + expect(inputEl.val()).toBe('2050-12-01'); + }); describe('focus', function () { beforeEach(function() {