From bf6d4ae76ed6f472b0e792070f71e88cdcb460f0 Mon Sep 17 00:00:00 2001 From: Shahar Talmi Date: Sat, 7 Feb 2015 17:18:18 +0200 Subject: [PATCH 1/2] feat(ngModel): support conversion to timezone other than UTC --- src/ng/directive/input.js | 17 +++++-- src/ng/directive/ngModel.js | 6 ++- test/ng/directive/inputSpec.js | 87 ++++++++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 7 deletions(-) diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index eb58f295a7eb..127ff562cb75 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -1156,8 +1156,12 @@ function createDateInputType(type, regexp, parseDate, format) { // parser/formatter in the processing chain so that the model // contains some different data format! var parsedDate = parseDate(value, previousDate); - if (timezone === 'UTC') { - parsedDate.setMinutes(parsedDate.getMinutes() - parsedDate.getTimezoneOffset()); + if (timezone) { + var requestedTimezoneOffset = Date.parse('Jan 01, 1970 00:00:00 ' + timezone) / 60000; + if (!isNaN(requestedTimezoneOffset)) { + parsedDate = new Date(parsedDate.getTime()); + parsedDate.setMinutes(parsedDate.getMinutes() - parsedDate.getTimezoneOffset() + requestedTimezoneOffset); + } } return parsedDate; } @@ -1170,9 +1174,12 @@ function createDateInputType(type, regexp, parseDate, format) { } if (isValidDate(value)) { previousDate = value; - if (previousDate && timezone === 'UTC') { - var timezoneOffset = 60000 * previousDate.getTimezoneOffset(); - previousDate = new Date(previousDate.getTime() + timezoneOffset); + if (previousDate && timezone) { + var requestedTimezoneOffset = Date.parse('Jan 01, 1970 00:00:00 ' + timezone) / 60000; + if (!isNaN(requestedTimezoneOffset)) { + previousDate = new Date(previousDate.getTime()); + previousDate.setMinutes(previousDate.getMinutes() + previousDate.getTimezoneOffset() - requestedTimezoneOffset); + } } return $filter('date')(value, format, timezone); } else { diff --git a/src/ng/directive/ngModel.js b/src/ng/directive/ngModel.js index 2aa897624e55..6098cd08a829 100644 --- a/src/ng/directive/ngModel.js +++ b/src/ng/directive/ngModel.js @@ -1097,8 +1097,10 @@ var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/; * - `getterSetter`: boolean value which determines whether or not to treat functions bound to `ngModel` as getters/setters. * - `timezone`: Defines the timezone to be used to read/write the `Date` instance in the model for - * ``, ``, ... . Right now, the only supported value is `'UTC'`, - * otherwise the default timezone of the browser will be used. + * ``, ``, ... . It understands UTC/GMT and the + * 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. * * @example diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js index ad98d52abdf9..f9640cbf325f 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -613,6 +613,19 @@ describe('input', function() { }); + it('should use any timezone if specified in the options', function() { + var inputElm = helper.compileInput(''); + + helper.changeInputValueTo('2013-07'); + expect(+$rootScope.value).toBe(Date.UTC(2013, 5, 30, 19, 0, 0)); + + $rootScope.$apply(function() { + $rootScope.value = new Date(Date.UTC(2014, 5, 30, 19, 0, 0)); + }); + expect(inputElm.val()).toBe('2014-07'); + }); + + it('should label parse errors as `month`', function() { var inputElm = helper.compileInput('', { valid: false, @@ -636,6 +649,17 @@ describe('input', function() { expect(inputElm.val()).toBe('2013-12'); }); + it('should only change the month of a bound date in any timezone', function() { + var inputElm = helper.compileInput(''); + + $rootScope.$apply(function() { + $rootScope.value = new Date(Date.UTC(2013, 6, 31, 20, 0, 0)); + }); + helper.changeInputValueTo('2013-09'); + expect(+$rootScope.value).toBe(Date.UTC(2013, 7, 31, 20, 0, 0)); + expect(inputElm.val()).toBe('2013-09'); + }); + describe('min', function() { var inputElm; beforeEach(function() { @@ -814,6 +838,19 @@ describe('input', function() { }); + it('should use any timezone if specified in the options', function() { + var inputElm = helper.compileInput(''); + + helper.changeInputValueTo('2013-W03'); + expect(+$rootScope.value).toBe(Date.UTC(2013, 0, 16, 19, 0, 0)); + + $rootScope.$apply(function() { + $rootScope.value = new Date(Date.UTC(2014, 0, 16, 19, 0, 0)); + }); + expect(inputElm.val()).toBe('2014-W03'); + }); + + it('should label parse errors as `week`', function() { var inputElm = helper.compileInput('', { valid: false, @@ -990,6 +1027,30 @@ describe('input', function() { }); + it('should use any timezone if specified in the options', function() { + var inputElm = helper.compileInput(''); + + helper.changeInputValueTo('2000-01-01T06:02'); + expect(+$rootScope.value).toBe(Date.UTC(2000, 0, 1, 1, 2, 0)); + + $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'); + }); + + + it('should fallback to default timezone in case an unknown timezone was passed', function() { + var inputElm = helper.compileInput( + '' + + ''); + + helper.changeGivenInputTo(inputElm.eq(0), '2000-01-01T06:02'); + helper.changeGivenInputTo(inputElm.eq(1), '2000-01-01T06:02'); + expect($rootScope.value1).toEqual($rootScope.value2); + }); + + it('should allow to specify the milliseconds', function() { var inputElm = helper.compileInput(''); @@ -1278,6 +1339,19 @@ describe('input', function() { }); + it('should use any timezone if specified in the options', function() { + var inputElm = helper.compileInput(''); + + helper.changeInputValueTo('23:02:00'); + expect(+$rootScope.value).toBe(Date.UTC(1970, 0, 1, 18, 2, 0)); + + $rootScope.$apply(function() { + $rootScope.value = new Date(Date.UTC(1971, 0, 1, 18, 2, 0)); + }); + expect(inputElm.val()).toBe('23:02:00.000'); + }); + + it('should allow to specify the milliseconds', function() { var inputElm = helper.compileInput(''); @@ -1559,6 +1633,19 @@ describe('input', function() { }); + it('should use any timezone if specified in the options', function() { + var inputElm = helper.compileInput(''); + + helper.changeInputValueTo('2000-01-01'); + expect(+$rootScope.value).toBe(Date.UTC(1999, 11, 31, 19, 0, 0)); + + $rootScope.$apply(function() { + $rootScope.value = new Date(Date.UTC(2000, 11, 31, 19, 0, 0)); + }); + expect(inputElm.val()).toBe('2001-01-01'); + }); + + it('should label parse errors as `date`', function() { var inputElm = helper.compileInput('', { valid: false, From 9b31d65a3721d8b2c95527ccf62ac58dd475a666 Mon Sep 17 00:00:00 2001 From: Shahar Talmi Date: Sat, 7 Feb 2015 19:35:31 +0200 Subject: [PATCH 2/2] refactor(filters/ngModel): extract common methods --- src/.jshintrc | 3 +++ src/Angular.js | 28 ++++++++++++++++++++++++++++ src/ng/directive/input.js | 12 ++---------- src/ng/filter/filters.js | 8 ++------ test/ng/filter/filtersSpec.js | 4 ++-- 5 files changed, 37 insertions(+), 18 deletions(-) diff --git a/src/.jshintrc b/src/.jshintrc index d0561483c9ad..3ee5f1c440ca 100644 --- a/src/.jshintrc +++ b/src/.jshintrc @@ -70,6 +70,9 @@ "toJsonReplacer": false, "toJson": false, "fromJson": false, + "fromTimezoneToLocal": false, + "fromLocalToTimezone": false, + "timezoneToOffset": false, "startingTag": false, "tryDecodeURIComponent": false, "parseKeyValue": false, diff --git a/src/Angular.js b/src/Angular.js index 2a12f4c8e538..f9468b6d9b5f 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -65,6 +65,9 @@ toJsonReplacer: true, toJson: true, fromJson: true, + fromTimezoneToLocal: true, + fromLocalToTimezone: true, + timezoneToOffset: true, startingTag: true, tryDecodeURIComponent: true, parseKeyValue: true, @@ -1068,6 +1071,31 @@ function fromJson(json) { } +function timezoneToOffset(timezone, fallback) { + var requestedTimezoneOffset = Date.parse('Jan 01, 1970 00:00:00 ' + timezone) / 60000; + return isNaN(requestedTimezoneOffset) ? fallback : requestedTimezoneOffset; +} + + +function addDateMinutes(date, minutes) { + date = new Date(date.getTime()); + date.setMinutes(date.getMinutes() + minutes); + return date; +} + + +function fromTimezoneToLocal(date, timezone) { + var timezoneOffset = timezoneToOffset(timezone, date.getTimezoneOffset()); + return addDateMinutes(date, timezoneOffset - date.getTimezoneOffset()); +} + + +function fromLocalToTimezone(date, timezone) { + var timezoneOffset = timezoneToOffset(timezone, date.getTimezoneOffset()); + return addDateMinutes(date, date.getTimezoneOffset() - timezoneOffset); +} + + /** * @returns {string} Returns the string representation of the element. */ diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index 127ff562cb75..37a8c4c5a110 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -1157,11 +1157,7 @@ function createDateInputType(type, regexp, parseDate, format) { // contains some different data format! var parsedDate = parseDate(value, previousDate); if (timezone) { - var requestedTimezoneOffset = Date.parse('Jan 01, 1970 00:00:00 ' + timezone) / 60000; - if (!isNaN(requestedTimezoneOffset)) { - parsedDate = new Date(parsedDate.getTime()); - parsedDate.setMinutes(parsedDate.getMinutes() - parsedDate.getTimezoneOffset() + requestedTimezoneOffset); - } + parsedDate = fromTimezoneToLocal(parsedDate, timezone); } return parsedDate; } @@ -1175,11 +1171,7 @@ function createDateInputType(type, regexp, parseDate, format) { if (isValidDate(value)) { previousDate = value; if (previousDate && timezone) { - var requestedTimezoneOffset = Date.parse('Jan 01, 1970 00:00:00 ' + timezone) / 60000; - if (!isNaN(requestedTimezoneOffset)) { - previousDate = new Date(previousDate.getTime()); - previousDate.setMinutes(previousDate.getMinutes() + previousDate.getTimezoneOffset() - requestedTimezoneOffset); - } + previousDate = fromLocalToTimezone(previousDate, timezone); } return $filter('date')(value, format, timezone); } else { diff --git a/src/ng/filter/filters.js b/src/ng/filter/filters.js index 657d29ed7a91..8fd1a5ae44ae 100644 --- a/src/ng/filter/filters.js +++ b/src/ng/filter/filters.js @@ -488,12 +488,8 @@ function dateFilter($locale) { var dateTimezoneOffset = date.getTimezoneOffset(); if (timezone) { - var requestedTimezoneOffset = Date.parse('Jan 01, 1970 00:00:00 ' + timezone) / 60000; - if (!isNaN(requestedTimezoneOffset)) { - date = new Date(date.getTime()); - date.setMinutes(date.getMinutes() + dateTimezoneOffset - requestedTimezoneOffset); - dateTimezoneOffset = requestedTimezoneOffset; - } + dateTimezoneOffset = timezoneToOffset(timezone, date.getTimezoneOffset()); + date = fromLocalToTimezone(date, timezone); } forEach(parts, function(value) { fn = DATE_FORMATS[value]; diff --git a/test/ng/filter/filtersSpec.js b/test/ng/filter/filtersSpec.js index bd101b432882..0da8ff0af385 100644 --- a/test/ng/filter/filtersSpec.js +++ b/test/ng/filter/filtersSpec.js @@ -464,8 +464,8 @@ describe('filters', function() { }); it('should fallback to default timezone in case an unknown timezone was passed', function() { - var value = new angular.mock.TzDate(-2, '2003-09-10T01:02:04.000Z'); - expect(date(value, 'yyyy-MM-dd HH-mm-ssZ', 'WTF')).toEqual('2003-09-10 03-02-04+0200'); + var value = new Date(2003, 8, 10, 3, 2, 4); + expect(date(value, 'yyyy-MM-dd HH-mm-ssZ', 'WTF')).toEqual(date(value, 'yyyy-MM-dd HH-mm-ssZ')); }); }); });