diff --git a/src/.eslintrc.json b/src/.eslintrc.json index e053a4dfb972..6cfa489bf0d0 100644 --- a/src/.eslintrc.json +++ b/src/.eslintrc.json @@ -79,6 +79,7 @@ "toJsonReplacer": false, "toJson": false, "fromJson": false, + "addDateMinutes": false, "convertTimezoneToLocal": false, "timezoneToOffset": false, "startingTag": false, diff --git a/src/Angular.js b/src/Angular.js index 7eda3a028b6b..38c564f62df5 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -75,6 +75,7 @@ fromJson, convertTimezoneToLocal, timezoneToOffset, + addDateMinutes, startingTag, tryDecodeURIComponent, parseKeyValue, diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index 5d9bf66fa468..93119d305d42 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -1468,20 +1468,17 @@ function createDateInputType(type, regexp, parseDate, format) { return function dynamicDateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter) { badInputChecker(scope, element, attr, ctrl, type); baseInputType(scope, element, attr, ctrl, $sniffer, $browser); - var timezone = ctrl && ctrl.$options.getOption('timezone'); var previousDate; + var previousTimezone; ctrl.$parsers.push(function(value) { if (ctrl.$isEmpty(value)) return null; + if (regexp.test(value)) { // Note: We cannot read ctrl.$modelValue, as there might be a different // parser/formatter in the processing chain so that the model // contains some different data format! - var parsedDate = parseDate(value, previousDate); - if (timezone) { - parsedDate = convertTimezoneToLocal(parsedDate, timezone); - } - return parsedDate; + return parseDateAndConvertTimeZoneToLocal(value, previousDate); } ctrl.$$parserName = type; return undefined; @@ -1493,12 +1490,15 @@ function createDateInputType(type, regexp, parseDate, format) { } if (isValidDate(value)) { previousDate = value; - if (previousDate && timezone) { + var timezone = ctrl.$options.getOption('timezone'); + if (timezone) { + previousTimezone = timezone; previousDate = convertTimezoneToLocal(previousDate, timezone, true); } return $filter('date')(value, format, timezone); } else { previousDate = null; + previousTimezone = null; return ''; } }); @@ -1531,7 +1531,24 @@ function createDateInputType(type, regexp, parseDate, format) { } function parseObservedDateValue(val) { - return isDefined(val) && !isDate(val) ? parseDate(val) || undefined : val; + return isDefined(val) && !isDate(val) ? parseDateAndConvertTimeZoneToLocal(val) || undefined : val; + } + + function parseDateAndConvertTimeZoneToLocal(value, previousDate) { + var timezone = ctrl.$options.getOption('timezone'); + + if (previousTimezone && previousTimezone !== timezone) { + // If the timezone has changed, adjust the previousDate to the default timezone + // so that the new date is converted with the correct timezone offset + previousDate = addDateMinutes(previousDate, timezoneToOffset(previousTimezone)); + } + + var parsedDate = parseDate(value, previousDate); + + if (!isNaN(parsedDate) && timezone) { + parsedDate = convertTimezoneToLocal(parsedDate, timezone); + } + return parsedDate; } }; } diff --git a/src/ng/directive/ngModelOptions.js b/src/ng/directive/ngModelOptions.js index 02a328ff3748..62408d2ea84a 100644 --- a/src/ng/directive/ngModelOptions.js +++ b/src/ng/directive/ngModelOptions.js @@ -462,6 +462,8 @@ defaultModelOptions = new ModelOptions({ * continental US time zone abbreviations, but for general use, use a time zone offset, for * example, `'+0430'` (4 hours, 30 minutes east of the Greenwich meridian) * If not specified, the timezone of the browser will be used. + * Note that changing the timezone will have no effect on the current date, and is only applied after + * the next input / model change. * */ var ngModelOptionsDirective = function() { diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js index 8c86b44ceada..658ce6adf955 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -718,6 +718,21 @@ describe('input', function() { }); + it('should be possible to override the timezone', function() { + var inputElm = helper.compileInput(''); + + helper.changeInputValueTo('2013-07'); + expect(+$rootScope.value).toBe(Date.UTC(2013, 6, 1)); + + inputElm.controller('ngModel').$overrideModelOptions({timezone: '-0500'}); + + $rootScope.$apply(function() { + $rootScope.value = new Date(Date.UTC(2013, 6, 1)); + }); + expect(inputElm.val()).toBe('2013-06'); + }); + + they('should use any timezone if specified in the options (format: $prop)', {'+HHmm': '+0500', '+HH:mm': '+05:00'}, function(tz) { @@ -866,6 +881,23 @@ describe('input', function() { expect($rootScope.form.alias.$error.max).toBeFalsy(); }); + + it('should validate when timezone is provided.', function() { + inputElm = helper.compileInput(''); + $rootScope.maxVal = '2013-01'; + $rootScope.value = new Date(Date.UTC(2013, 0, 1, 0, 0, 0)); + $rootScope.$digest(); + + expect($rootScope.form.alias.$error.max).toBeFalsy(); + expect($rootScope.form.alias.$valid).toBeTruthy(); + + $rootScope.value = ''; + helper.changeInputValueTo('2013-01'); + expect(inputElm).toBeValid(); + expect($rootScope.form.alias.$error.max).toBeFalsy(); + expect($rootScope.form.alias.$valid).toBeTruthy(); + }); }); }); @@ -987,6 +1019,30 @@ describe('input', function() { }); + it('should be possible to override the timezone', function() { + var inputElm = helper.compileInput(''); + + // January 19 2013 is a Saturday + $rootScope.$apply(function() { + $rootScope.value = new Date(Date.UTC(2013, 0, 19)); + }); + + expect(inputElm.val()).toBe('2013-W03'); + + inputElm.controller('ngModel').$overrideModelOptions({timezone: '+2400'}); + + // To check that the timezone overwrite works, apply an offset of +24 hours. + // Since January 19 is a Saturday, +24 will turn the formatted Date into January 20 - Sunday - + // which is in calendar week 4 instead of 3. + $rootScope.$apply(function() { + $rootScope.value = new Date(Date.UTC(2013, 0, 19)); + }); + + // Verifying that the displayed week is week 4 confirms that overriding the timezone worked + expect(inputElm.val()).toBe('2013-W04'); + }); + + they('should use any timezone if specified in the options (format: $prop)', {'+HHmm': '+0500', '+HH:mm': '+05:00'}, function(tz) { @@ -1099,6 +1155,25 @@ describe('input', function() { expect($rootScope.form.alias.$error.max).toBeFalsy(); }); + + it('should validate when timezone is provided.', function() { + inputElm = helper.compileInput(''); + // The calendar week comparison date is January 17. Setting the timezone to -2400 + // makes the January 18 date value valid. + $rootScope.maxVal = '2013-W03'; + $rootScope.value = new Date(Date.UTC(2013, 0, 18)); + $rootScope.$digest(); + + expect($rootScope.form.alias.$error.max).toBeFalsy(); + expect($rootScope.form.alias.$valid).toBeTruthy(); + + $rootScope.value = ''; + helper.changeInputValueTo('2013-W03'); + expect(inputElm).toBeValid(); + expect($rootScope.form.alias.$error.max).toBeFalsy(); + expect($rootScope.form.alias.$valid).toBeTruthy(); + }); }); }); @@ -1193,6 +1268,25 @@ describe('input', function() { }); + it('should be possible to override the timezone', function() { + var inputElm = helper.compileInput(''); + + helper.changeInputValueTo('2000-01-01T01:02'); + expect(+$rootScope.value).toBe(Date.UTC(2000, 0, 1, 1, 2, 0)); + + inputElm.controller('ngModel').$overrideModelOptions({timezone: '+0500'}); + $rootScope.$apply(function() { + $rootScope.value = new Date(Date.UTC(2001, 0, 1, 1, 2, 0)); + }); + expect(inputElm.val()).toBe('2001-01-01T06:02:00.000'); + + inputElm.controller('ngModel').$overrideModelOptions({timezone: 'UTC'}); + + helper.changeInputValueTo('2000-01-01T01:02'); + expect(+$rootScope.value).toBe(Date.UTC(2000, 0, 1, 1, 2, 0)); + }); + + they('should use any timezone if specified in the options (format: $prop)', {'+HHmm': '+0500', '+HH:mm': '+05:00'}, function(tz) { @@ -1368,6 +1462,23 @@ describe('input', function() { expect($rootScope.form.alias.$error.max).toBeFalsy(); }); + + it('should validate when timezone is provided.', function() { + inputElm = helper.compileInput(''); + $rootScope.maxVal = '2013-01-01T00:00:00'; + $rootScope.value = new Date(Date.UTC(2013, 0, 1, 0, 0, 0)); + $rootScope.$digest(); + + expect($rootScope.form.alias.$error.max).toBeFalsy(); + expect($rootScope.form.alias.$valid).toBeTruthy(); + + $rootScope.value = ''; + helper.changeInputValueTo('2013-01-01T00:00:00'); + expect(inputElm).toBeValid(); + expect($rootScope.form.alias.$error.max).toBeFalsy(); + expect($rootScope.form.alias.$valid).toBeTruthy(); + }); }); @@ -1538,6 +1649,25 @@ describe('input', function() { }); + it('should be possible to override the timezone', function() { + var inputElm = helper.compileInput(''); + + helper.changeInputValueTo('23:02:00'); + expect(+$rootScope.value).toBe(Date.UTC(1970, 0, 1, 23, 2, 0)); + + inputElm.controller('ngModel').$overrideModelOptions({timezone: '-0500'}); + $rootScope.$apply(function() { + $rootScope.value = new Date(Date.UTC(1971, 0, 1, 23, 2, 0)); + }); + expect(inputElm.val()).toBe('18:02:00.000'); + + inputElm.controller('ngModel').$overrideModelOptions({timezone: 'UTC'}); + helper.changeInputValueTo('23:02:00'); + // The year is still set from the previous date + expect(+$rootScope.value).toBe(Date.UTC(1971, 0, 1, 23, 2, 0)); + }); + + they('should use any timezone if specified in the options (format: $prop)', {'+HHmm': '+0500', '+HH:mm': '+05:00'}, function(tz) { @@ -1686,6 +1816,23 @@ describe('input', function() { expect($rootScope.form.alias.$error.max).toBeFalsy(); }); + + it('should validate when timezone is provided.', function() { + inputElm = helper.compileInput(''); + $rootScope.maxVal = '22:30:00'; + $rootScope.value = new Date(Date.UTC(1970, 0, 1, 22, 30, 0)); + $rootScope.$digest(); + + expect($rootScope.form.alias.$error.max).toBeFalsy(); + expect($rootScope.form.alias.$valid).toBeTruthy(); + + $rootScope.value = ''; + helper.changeInputValueTo('22:30:00'); + expect(inputElm).toBeValid(); + expect($rootScope.form.alias.$error.max).toBeFalsy(); + expect($rootScope.form.alias.$valid).toBeTruthy(); + }); }); @@ -1850,6 +1997,24 @@ describe('input', function() { }); + it('should be possible to override the timezone', function() { + var inputElm = helper.compileInput(''); + + helper.changeInputValueTo('2000-01-01'); + expect(+$rootScope.value).toBe(Date.UTC(2000, 0, 1)); + + inputElm.controller('ngModel').$overrideModelOptions({timezone: '-0500'}); + $rootScope.$apply(function() { + $rootScope.value = new Date(Date.UTC(2001, 0, 1)); + }); + expect(inputElm.val()).toBe('2000-12-31'); + + inputElm.controller('ngModel').$overrideModelOptions({timezone: 'UTC'}); + helper.changeInputValueTo('2000-01-01'); + expect(+$rootScope.value).toBe(Date.UTC(2000, 0, 1, 0)); + }); + + they('should use any timezone if specified in the options (format: $prop)', {'+HHmm': '+0500', '+HH:mm': '+05:00'}, function(tz) { @@ -1935,6 +2100,34 @@ describe('input', function() { dealoc(formElm); }); + it('should not reuse the hours part of a previous date object after changing the timezone', function() { + var inputElm = helper.compileInput(''); + + helper.changeInputValueTo('2000-01-01'); + // The Date parser sets the hours part of the Date to 0 (00:00) (UTC) + expect(+$rootScope.value).toBe(Date.UTC(2000, 0, 1, 0)); + + // Change the timezone offset so that the display date is a day earlier + // This does not change the model, but our implementation + // internally caches a Date object with this offset + // and re-uses it if part of the Date changes. + // See https://github.com/angular/angular.js/commit/1a1ef62903c8fdf4ceb81277d966a8eff67f0a96 + inputElm.controller('ngModel').$overrideModelOptions({timezone: '-0500'}); + $rootScope.$apply(function() { + $rootScope.value = new Date(Date.UTC(2000, 0, 1, 0)); + }); + expect(inputElm.val()).toBe('1999-12-31'); + + // At this point, the cached Date has its hours set to to 19 (00:00 - 05:00 = 19:00) + inputElm.controller('ngModel').$overrideModelOptions({timezone: 'UTC'}); + + // When changing the timezone back to UTC, the hours part of the Date should be set to + // the default 0 (UTC) and not use the modified value of the cached Date object. + helper.changeInputValueTo('2000-01-01'); + expect(+$rootScope.value).toBe(Date.UTC(2000, 0, 1, 0)); + }); + + describe('min', function() { it('should invalidate', function() { @@ -2031,6 +2224,24 @@ describe('input', function() { expect($rootScope.form.alias.$error.max).toBeFalsy(); }); + + it('should validate when timezone is provided.', function() { + var inputElm = helper.compileInput(''); + + $rootScope.maxVal = '2013-12-01'; + $rootScope.value = new Date(Date.UTC(2013, 11, 1, 0, 0, 0)); + $rootScope.$digest(); + + expect($rootScope.form.alias.$error.max).toBeFalsy(); + expect($rootScope.form.alias.$valid).toBeTruthy(); + + $rootScope.value = ''; + helper.changeInputValueTo('2013-12-01'); + expect(inputElm).toBeValid(); + expect($rootScope.form.alias.$error.max).toBeFalsy(); + expect($rootScope.form.alias.$valid).toBeTruthy(); + }); });