diff --git a/src/dateparser/dateparser.js b/src/dateparser/dateparser.js index fa3004533b..eb323623b6 100644 --- a/src/dateparser/dateparser.js +++ b/src/dateparser/dateparser.js @@ -106,7 +106,7 @@ angular.module('ui.bootstrap.dateparser', []) }; } - this.parse = function(input, format) { + this.parse = function(input, format, baseDate) { if ( !angular.isString(input) || !format ) { return input; } @@ -124,7 +124,20 @@ angular.module('ui.bootstrap.dateparser', []) results = input.match(regex); if ( results && results.length ) { - var fields = { year: 1900, month: 0, date: 1, hours: 0, minutes: 0, seconds: 0, milliseconds: 0 }, dt; + var fields, dt; + if (baseDate) { + fields = { + year: baseDate.getFullYear(), + month: baseDate.getMonth(), + date: baseDate.getDate(), + hours: baseDate.getHours(), + minutes: baseDate.getMinutes(), + seconds: baseDate.getSeconds(), + milliseconds: baseDate.getMilliseconds() + }; + } else { + fields = { year: 1900, month: 0, date: 1, hours: 0, minutes: 0, seconds: 0, milliseconds: 0 }; + } for( var i = 1, n = results.length; i < n; i++ ) { var mapper = map[i-1]; @@ -134,7 +147,8 @@ angular.module('ui.bootstrap.dateparser', []) } if ( isValid(fields.year, fields.month, fields.date) ) { - dt = new Date(fields.year, fields.month, fields.date, fields.hours, fields.minutes, fields.seconds, fields.milliseconds); + dt = new Date(fields.year, fields.month, fields.date, fields.hours, fields.minutes, fields.seconds, + fields.milliseconds || 0); } return dt; diff --git a/src/datepicker/datepicker.js b/src/datepicker/datepicker.js index 830edb2f8d..d18c86d944 100644 --- a/src/datepicker/datepicker.js +++ b/src/datepicker/datepicker.js @@ -444,6 +444,11 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst .constant('datepickerPopupConfig', { datepickerPopup: 'yyyy-MM-dd', + html5Types: { + date: 'yyyy-MM-dd', + 'datetime-local': 'yyyy-MM-ddTHH:mm:ss.sss', + 'month': 'yyyy-MM' + }, currentText: 'Today', clearText: 'Clear', closeText: 'Done', @@ -476,16 +481,34 @@ function ($compile, $parse, $document, $position, dateFilter, dateParser, datepi return scope[key + 'Text'] || datepickerPopupConfig[key + 'Text']; }; - dateFormat = attrs.datepickerPopup || datepickerPopupConfig.datepickerPopup; - attrs.$observe('datepickerPopup', 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; - } - }); + var isHtml5DateInput = false; + if (datepickerPopupConfig.html5Types[attrs.type]) { + dateFormat = datepickerPopupConfig.html5Types[attrs.type]; + isHtml5DateInput = true; + } else { + dateFormat = attrs.datepickerPopup || datepickerPopupConfig.datepickerPopup; + attrs.$observe('datepickerPopup', 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('datepickerPopup must have a date format specified.'); + } + } + }); + } + + if (!dateFormat) { + throw new Error('datepickerPopup must have a date format specified.'); + } + + if (isHtml5DateInput && attrs.datepickerPopup) { + throw new Error('HTML5 date input types do not support custom formats.'); + } // popup element used to display calendar var popupEl = angular.element('
'); @@ -500,6 +523,13 @@ function ($compile, $parse, $document, $position, dateFilter, dateParser, datepi // datepicker element var datepickerEl = angular.element(popupEl.children()[0]); + 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.initDate) { @@ -544,8 +574,6 @@ function ($compile, $parse, $document, $position, dateFilter, dateParser, datepi datepickerEl.attr('custom-class', 'customClass({ date: date, mode: mode })'); } - // Internal API to maintain the correct ng-invalid-[key] class - ngModel.$$parserName = 'date'; function parseDate(viewValue) { if (angular.isNumber(viewValue)) { // presumably timestamp to date object @@ -557,7 +585,7 @@ function ($compile, $parse, $document, $position, dateFilter, dateParser, datepi } else if (angular.isDate(viewValue) && !isNaN(viewValue)) { return viewValue; } else if (angular.isString(viewValue)) { - var date = dateParser.parse(viewValue, dateFormat) || new Date(viewValue); + var date = dateParser.parse(viewValue, dateFormat, scope.date) || new Date(viewValue); if (isNaN(date)) { return undefined; } else { @@ -585,24 +613,31 @@ function ($compile, $parse, $document, $position, dateFilter, dateParser, datepi } } - ngModel.$validators.date = validator; - ngModel.$parsers.unshift(parseDate); - - ngModel.$formatters.push(function (value) { - scope.date = value; - return ngModel.$isEmpty(value) ? value : dateFilter(value, dateFormat); - }); + 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; + }); + } // Inner change scope.dateSelection = function(dt) { if (angular.isDefined(dt)) { scope.date = dt; } - if (dateFormat) { - var date = scope.date ? dateFilter(scope.date, dateFormat) : ''; - element.val(date); - } - ngModel.$setViewValue(scope.date); + var date = scope.date ? dateFilter(scope.date, dateFormat) : ''; + element.val(date); + ngModel.$setViewValue(date); if ( closeOnDateSelection ) { scope.isOpen = false; @@ -612,7 +647,7 @@ function ($compile, $parse, $document, $position, dateFilter, dateParser, datepi // Detect changes in the view from the text box ngModel.$viewChangeListeners.push(function () { - scope.date = ngModel.$viewValue; + scope.date = dateParser.parse(ngModel.$viewValue, dateFormat, scope.date) || new Date(ngModel.$viewValue); }); var documentClickBind = function(event) { diff --git a/src/datepicker/docs/demo.html b/src/datepicker/docs/demo.html index 5257bf5c46..7ea3b98fd2 100644 --- a/src/datepicker/docs/demo.html +++ b/src/datepicker/docs/demo.html @@ -28,6 +28,15 @@

Popup

+ +
+

+ + + + +

+
diff --git a/src/datepicker/test/datepicker.spec.js b/src/datepicker/test/datepicker.spec.js index be8eba3601..4f551113d2 100644 --- a/src/datepicker/test/datepicker.spec.js +++ b/src/datepicker/test/datepicker.spec.js @@ -99,6 +99,15 @@ describe('datepicker directive', function () { return element.find('tbody').find('button'); } + function selectedElementIndex() { + var buttons = getAllOptionsEl(); + for (var i = 0; i < buttons.length; i++) { + if (angular.element(buttons[i]).hasClass('btn-info')) { + return i; + } + } + } + function expectSelectedElement( index ) { var buttons = getAllOptionsEl(); angular.forEach( buttons, function( button, idx ) { @@ -1402,6 +1411,84 @@ describe('datepicker directive', function () { }); }); + describe('works with HTML5 date input types', function () { + var date2 = new Date('October 1, 2010 12:34:56.789'); + beforeEach(inject(function(_$document_) { + $document = _$document_; + $rootScope.isopen = true; + $rootScope.date = new Date('September 30, 2010 15:30:00'); + })); + + it('works as date', function() { + setupInputWithType('date'); + expect(dropdownEl).toBeHidden(); + expect(inputEl.val()).toBe('2010-09-30'); + + changeInputValueTo(inputEl, '1980-03-05'); + + expect($rootScope.date.getFullYear()).toEqual(1980); + expect($rootScope.date.getMonth()).toEqual(2); + expect($rootScope.date.getDate()).toEqual(5); + + expect(getOptions(true)).toEqual([ + ['24', '25', '26', '27', '28', '29', '01'], + ['02', '03', '04', '05', '06', '07', '08'], + ['09', '10', '11', '12', '13', '14', '15'], + ['16', '17', '18', '19', '20', '21', '22'], + ['23', '24', '25', '26', '27', '28', '29'], + ['30', '31', '01', '02', '03', '04', '05'] + ]); + expect(selectedElementIndex()).toEqual( 10 ); + }); + + it('works as datetime-local', function() { + setupInputWithType('datetime-local'); + expect(inputEl.val()).toBe('2010-09-30T15:30:00.000'); + + changeInputValueTo(inputEl, '1980-03-05T12:34:56.000'); + + expect($rootScope.date.getFullYear()).toEqual(1980); + expect($rootScope.date.getMonth()).toEqual(2); + expect($rootScope.date.getDate()).toEqual(5); + + expect(getOptions(true)).toEqual([ + ['24', '25', '26', '27', '28', '29', '01'], + ['02', '03', '04', '05', '06', '07', '08'], + ['09', '10', '11', '12', '13', '14', '15'], + ['16', '17', '18', '19', '20', '21', '22'], + ['23', '24', '25', '26', '27', '28', '29'], + ['30', '31', '01', '02', '03', '04', '05'] + ]); + expect(selectedElementIndex()).toEqual( 10 ); + }); + + it('works as month', function() { + setupInputWithType('month'); + expect(inputEl.val()).toBe('2010-09'); + + changeInputValueTo(inputEl, '1980-03'); + + expect($rootScope.date.getFullYear()).toEqual(1980); + expect($rootScope.date.getMonth()).toEqual(2); + expect($rootScope.date.getDate()).toEqual(30); + + expect(getOptions()).toEqual([ + ['January', 'February', 'March'], + ['April', 'May', 'June'], + ['July', 'August', 'September'], + ['October', 'November', 'December'] + ]); + expect(selectedElementIndex()).toEqual( 2 ); + }); + + function setupInputWithType(type) { + var wrapElement = $compile('
')($rootScope); + $rootScope.$digest(); + assignElements(wrapElement); + } + }); + }); describe('attribute `datepickerOptions`', function () {