From 71e840b6dfda7fc886f02822c56c8cafcc53ac5e Mon Sep 17 00:00:00 2001
From: Chris Chua
Date: Mon, 6 Apr 2015 00:12:59 -0700
Subject: [PATCH 1/2] feat(datepicker): support HTML5 date input type
---
src/dateparser/dateparser.js | 20 ++++++-
src/datepicker/datepicker.js | 80 +++++++++++++++++---------
src/datepicker/docs/demo.html | 9 +++
src/datepicker/test/datepicker.spec.js | 68 ++++++++++++++++++++++
4 files changed, 148 insertions(+), 29 deletions(-)
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..4f0462c43d 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'
+ // TODO: Add other formats/type support
+ },
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('');
@@ -544,8 +567,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 +578,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 +606,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 +640,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..e04cda2e3d 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,65 @@ 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 );
+ });
+
+ function setupInputWithType(type) {
+ var wrapElement = $compile('
')($rootScope);
+ $rootScope.$digest();
+ assignElements(wrapElement);
+ }
+ });
+
});
describe('attribute `datepickerOptions`', function () {
From 8a0526899fadd98a0cce915bdb6c983abbb2a8a1 Mon Sep 17 00:00:00 2001
From: Chris Chua
Date: Mon, 13 Apr 2015 23:01:18 -0700
Subject: [PATCH 2/2] feat(datepicker): support HTML5 month input type
---
src/datepicker/datepicker.js | 11 +++++++++--
src/datepicker/test/datepicker.spec.js | 19 +++++++++++++++++++
2 files changed, 28 insertions(+), 2 deletions(-)
diff --git a/src/datepicker/datepicker.js b/src/datepicker/datepicker.js
index 4f0462c43d..d18c86d944 100644
--- a/src/datepicker/datepicker.js
+++ b/src/datepicker/datepicker.js
@@ -446,8 +446,8 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
datepickerPopup: 'yyyy-MM-dd',
html5Types: {
date: 'yyyy-MM-dd',
- 'datetime-local': 'yyyy-MM-ddTHH:mm:ss.sss'
- // TODO: Add other formats/type support
+ 'datetime-local': 'yyyy-MM-ddTHH:mm:ss.sss',
+ 'month': 'yyyy-MM'
},
currentText: 'Today',
clearText: 'Clear',
@@ -523,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) {
diff --git a/src/datepicker/test/datepicker.spec.js b/src/datepicker/test/datepicker.spec.js
index e04cda2e3d..4f551113d2 100644
--- a/src/datepicker/test/datepicker.spec.js
+++ b/src/datepicker/test/datepicker.spec.js
@@ -1462,6 +1462,25 @@ describe('datepicker directive', function () {
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('