Skip to content
This repository was archived by the owner on May 29, 2019. It is now read-only.

Commit 09098f8

Browse files
daviouswesleycho
authored andcommitted
feat(datepicker): add timezone support
- Add timezone support with ngModelOptions Closes #5062
1 parent 9019298 commit 09098f8

File tree

5 files changed

+386
-24
lines changed

5 files changed

+386
-24
lines changed

src/dateparser/dateparser.js

+32
Original file line numberDiff line numberDiff line change
@@ -330,4 +330,36 @@ angular.module('ui.bootstrap.dateparser', [])
330330
function toInt(str) {
331331
return parseInt(str, 10);
332332
}
333+
334+
this.toTimezone = toTimezone;
335+
this.fromTimezone = fromTimezone;
336+
this.timezoneToOffset = timezoneToOffset;
337+
this.addDateMinutes = addDateMinutes;
338+
this.convertTimezoneToLocal = convertTimezoneToLocal;
339+
340+
function toTimezone(date, timezone) {
341+
return date && timezone ? convertTimezoneToLocal(date, timezone) : date;
342+
}
343+
344+
function fromTimezone(date, timezone) {
345+
return date && timezone ? convertTimezoneToLocal(date, timezone, true) : date;
346+
}
347+
348+
//https://github.com/angular/angular.js/blob/4daafd3dbe6a80d578f5a31df1bb99c77559543e/src/Angular.js#L1207
349+
function timezoneToOffset(timezone, fallback) {
350+
var requestedTimezoneOffset = Date.parse('Jan 01, 1970 00:00:00 ' + timezone) / 60000;
351+
return isNaN(requestedTimezoneOffset) ? fallback : requestedTimezoneOffset;
352+
}
353+
354+
function addDateMinutes(date, minutes) {
355+
date = new Date(date.getTime());
356+
date.setMinutes(date.getMinutes() + minutes);
357+
return date;
358+
}
359+
360+
function convertTimezoneToLocal(date, timezone, reverse) {
361+
reverse = reverse ? -1 : 1;
362+
var timezoneOffset = timezoneToOffset(timezone, date.getTimezoneOffset());
363+
return addDateMinutes(date, reverse * (timezoneOffset - date.getTimezoneOffset()));
364+
}
333365
}]);

src/dateparser/test/dateparser.spec.js

+112
Original file line numberDiff line numberDiff line change
@@ -387,4 +387,116 @@ describe('date parser', function() {
387387

388388
expect(dateParser.init).toHaveBeenCalled();
389389
}));
390+
391+
392+
describe('timezone functions', function() {
393+
describe('toTimezone', function() {
394+
it('adjusts date: PST - EST', function() {
395+
var date = new Date('2008-01-01T00:00:00.000Z');
396+
var toWestDate = dateParser.toTimezone(date, 'PST');
397+
var toEastDate = dateParser.toTimezone(date, 'EST');
398+
expect(toWestDate.getTime() - toEastDate.getTime()).toEqual(1000 * 60 * 60 * 3);
399+
});
400+
401+
it('adjusts date: GMT-0500 - GMT+0500', function() {
402+
var date = new Date('2008-01-01T00:00:00.000Z');
403+
var toWestDate = dateParser.toTimezone(date, 'GMT-0500');
404+
var toEastDate = dateParser.toTimezone(date, 'GMT+0500');
405+
expect(toWestDate.getTime() - toEastDate.getTime()).toEqual(1000 * 60 * 60 * 10);
406+
});
407+
408+
it('adjusts date: -600 - +600', function() {
409+
var date = new Date('2008-01-01T00:00:00.000Z');
410+
var toWestDate = dateParser.toTimezone(date, '-600');
411+
var toEastDate = dateParser.toTimezone(date, '+600');
412+
expect(toWestDate.getTime() - toEastDate.getTime()).toEqual(1000 * 60 * 60 * 12);
413+
});
414+
415+
it('tolerates null date', function() {
416+
var date = null;
417+
var toNullDate = dateParser.toTimezone(date, '-600');
418+
expect(toNullDate).toEqual(date);
419+
});
420+
421+
it('tolerates null timezone', function() {
422+
var date = new Date('2008-01-01T00:00:00.000Z');
423+
var toNullTimezoneDate = dateParser.toTimezone(date, null);
424+
expect(toNullTimezoneDate).toEqual(date);
425+
});
426+
});
427+
428+
describe('fromTimezone', function() {
429+
it('adjusts date: PST - EST', function() {
430+
var date = new Date('2008-01-01T00:00:00.000Z');
431+
var fromWestDate = dateParser.fromTimezone(date, 'PST');
432+
var fromEastDate = dateParser.fromTimezone(date, 'EST');
433+
expect(fromWestDate.getTime() - fromEastDate.getTime()).toEqual(1000 * 60 * 60 * -3);
434+
});
435+
436+
it('adjusts date: GMT-0500 - GMT+0500', function() {
437+
var date = new Date('2008-01-01T00:00:00.000Z');
438+
var fromWestDate = dateParser.fromTimezone(date, 'GMT-0500');
439+
var fromEastDate = dateParser.fromTimezone(date, 'GMT+0500');
440+
expect(fromWestDate.getTime() - fromEastDate.getTime()).toEqual(1000 * 60 * 60 * -10);
441+
});
442+
443+
it('adjusts date: -600 - +600', function() {
444+
var date = new Date('2008-01-01T00:00:00.000Z');
445+
var fromWestDate = dateParser.fromTimezone(date, '-600');
446+
var fromEastDate = dateParser.fromTimezone(date, '+600');
447+
expect(fromWestDate.getTime() - fromEastDate.getTime()).toEqual(1000 * 60 * 60 * -12);
448+
});
449+
450+
it('tolerates null date', function() {
451+
var date = null;
452+
var toNullDate = dateParser.fromTimezone(date, '-600');
453+
expect(toNullDate).toEqual(date);
454+
});
455+
456+
it('tolerates null timezone', function() {
457+
var date = new Date('2008-01-01T00:00:00.000Z');
458+
var toNullTimezoneDate = dateParser.fromTimezone(date, null);
459+
expect(toNullTimezoneDate).toEqual(date);
460+
});
461+
});
462+
463+
describe('timezoneToOffset', function() {
464+
it('calculates minutes off from current timezone', function() {
465+
var offsetMinutesUtc = dateParser.timezoneToOffset('UTC');
466+
var offsetMinutesUtcPlus1 = dateParser.timezoneToOffset('GMT+0100');
467+
expect(offsetMinutesUtc - offsetMinutesUtcPlus1).toEqual(60);
468+
});
469+
});
470+
471+
describe('addDateMinutes', function() {
472+
it('adds minutes to a date', function() {
473+
var date = new Date('2008-01-01T00:00:00.000Z');
474+
var oneHourMore = dateParser.addDateMinutes(date, 60);
475+
expect(oneHourMore).toEqual(new Date('2008-01-01T01:00:00.000Z'));
476+
});
477+
});
478+
479+
describe('convertTimezoneToLocal', function() {
480+
it('adjusts date: PST - EST', function() {
481+
var date = new Date('2008-01-01T00:00:00.000Z');
482+
var toWestDate = dateParser.convertTimezoneToLocal(date, 'PST');
483+
var toEastDate = dateParser.convertTimezoneToLocal(date, 'EST');
484+
expect(toWestDate.getTime() - toEastDate.getTime()).toEqual(1000 * 60 * 60 * 3);
485+
});
486+
487+
it('adjusts date: GMT-0500 - GMT+0500', function() {
488+
var date = new Date('2008-01-01T00:00:00.000Z');
489+
var toWestDate = dateParser.convertTimezoneToLocal(date, 'GMT-0500');
490+
var toEastDate = dateParser.convertTimezoneToLocal(date, 'GMT+0500');
491+
expect(toWestDate.getTime() - toEastDate.getTime()).toEqual(1000 * 60 * 60 * 10);
492+
});
493+
494+
it('adjusts date: -600 - +600', function() {
495+
var date = new Date('2008-01-01T00:00:00.000Z');
496+
var toWestDate = dateParser.convertTimezoneToLocal(date, '-600');
497+
var toEastDate = dateParser.convertTimezoneToLocal(date, '+600');
498+
expect(toWestDate.getTime() - toEastDate.getTime()).toEqual(1000 * 60 * 60 * 12);
499+
});
500+
});
501+
});
390502
});

src/datepicker/datepicker.js

+36-18
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,15 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
1818
yearColumns: 5,
1919
minDate: null,
2020
maxDate: null,
21-
shortcutPropagation: false
21+
shortcutPropagation: false,
22+
ngModelOptions: {}
2223
})
2324

24-
.controller('UibDatepickerController', ['$scope', '$attrs', '$parse', '$interpolate', '$log', 'dateFilter', 'uibDatepickerConfig', '$datepickerSuppressError', function($scope, $attrs, $parse, $interpolate, $log, dateFilter, datepickerConfig, $datepickerSuppressError) {
25+
.controller('UibDatepickerController', ['$scope', '$attrs', '$parse', '$interpolate', '$log', 'dateFilter', 'uibDatepickerConfig', '$datepickerSuppressError', 'uibDateParser',
26+
function($scope, $attrs, $parse, $interpolate, $log, dateFilter, datepickerConfig, $datepickerSuppressError, dateParser) {
2527
var self = this,
26-
ngModelCtrl = { $setViewValue: angular.noop }; // nullModelCtrl;
28+
ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl;
29+
ngModelOptions = {};
2730

2831
// Modes chain
2932
this.modes = ['day', 'month', 'year'];
@@ -42,11 +45,11 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
4245
angular.forEach(['minDate', 'maxDate'], function(key) {
4346
if ($attrs[key]) {
4447
$scope.$parent.$watch($attrs[key], function(value) {
45-
self[key] = value ? angular.isDate(value) ? new Date(value) : new Date(dateFilter(value, 'medium')) : null;
48+
self[key] = value ? angular.isDate(value) ? dateParser.fromTimezone(new Date(value), ngModelOptions.timezone) : new Date(dateFilter(value, 'medium')) : null;
4649
self.refreshView();
4750
});
4851
} else {
49-
self[key] = datepickerConfig[key] ? new Date(datepickerConfig[key]) : null;
52+
self[key] = datepickerConfig[key] ? dateParser.fromTimezone(new Date(datepickerConfig[key]), ngModelOptions.timezone) : null;
5053
}
5154
});
5255

@@ -68,10 +71,10 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
6871
$scope.uniqueId = 'datepicker-' + $scope.$id + '-' + Math.floor(Math.random() * 10000);
6972

7073
if (angular.isDefined($attrs.initDate)) {
71-
this.activeDate = $scope.$parent.$eval($attrs.initDate) || new Date();
74+
this.activeDate = dateParser.fromTimezone($scope.$parent.$eval($attrs.initDate), ngModelOptions.timezone) || new Date();
7275
$scope.$parent.$watch($attrs.initDate, function(initDate) {
7376
if (initDate && (ngModelCtrl.$isEmpty(ngModelCtrl.$modelValue) || ngModelCtrl.$invalid)) {
74-
self.activeDate = initDate;
77+
self.activeDate = dateParser.fromTimezone(initDate, ngModelOptions.timezone);
7578
self.refreshView();
7679
}
7780
});
@@ -97,6 +100,7 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
97100

98101
this.init = function(ngModelCtrl_) {
99102
ngModelCtrl = ngModelCtrl_;
103+
ngModelOptions = ngModelCtrl_.$options || datepickerConfig.ngModelOptions;
100104

101105
if (ngModelCtrl.$modelValue) {
102106
this.activeDate = ngModelCtrl.$modelValue;
@@ -113,7 +117,7 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
113117
isValid = !isNaN(date);
114118

115119
if (isValid) {
116-
this.activeDate = date;
120+
this.activeDate = dateParser.fromTimezone(date, ngModelOptions.timezone);
117121
} else if (!$datepickerSuppressError) {
118122
$log.error('Datepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.');
119123
}
@@ -126,13 +130,15 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
126130
this._refreshView();
127131

128132
var date = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null;
133+
date = dateParser.fromTimezone(date, ngModelOptions.timezone);
129134
ngModelCtrl.$setValidity('dateDisabled', !date ||
130135
this.element && !this.isDisabled(date));
131136
}
132137
};
133138

134139
this.createDateObject = function(date, format) {
135140
var model = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null;
141+
model = dateParser.fromTimezone(model, ngModelOptions.timezone);
136142
return {
137143
date: date,
138144
label: dateFilter(date, format),
@@ -165,8 +171,9 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
165171

166172
$scope.select = function(date) {
167173
if ($scope.datepickerMode === self.minMode) {
168-
var dt = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : new Date(0, 0, 0, 0, 0, 0, 0);
174+
var dt = ngModelCtrl.$viewValue ? dateParser.fromTimezone(new Date(ngModelCtrl.$viewValue), ngModelOptions.timezone) : new Date(0, 0, 0, 0, 0, 0, 0);
169175
dt.setFullYear(date.getFullYear(), date.getMonth(), date.getDate());
176+
dt = dateParser.toTimezone(dt, ngModelOptions.timezone);
170177
ngModelCtrl.$setViewValue(dt);
171178
ngModelCtrl.$render();
172179
} else {
@@ -550,19 +557,20 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
550557
altInputFormats: []
551558
})
552559

553-
.controller('UibDatepickerPopupController', ['$scope', '$element', '$attrs', '$compile', '$parse', '$document', '$rootScope', '$uibPosition', 'dateFilter', 'uibDateParser', 'uibDatepickerPopupConfig', '$timeout',
554-
function(scope, element, attrs, $compile, $parse, $document, $rootScope, $position, dateFilter, dateParser, datepickerPopupConfig, $timeout) {
560+
.controller('UibDatepickerPopupController', ['$scope', '$element', '$attrs', '$compile', '$parse', '$document', '$rootScope', '$uibPosition', 'dateFilter', 'uibDateParser', 'uibDatepickerPopupConfig', '$timeout', 'uibDatepickerConfig',
561+
function(scope, element, attrs, $compile, $parse, $document, $rootScope, $position, dateFilter, dateParser, datepickerPopupConfig, $timeout, datepickerConfig) {
555562
var self = this;
556563
var cache = {},
557564
isHtml5DateInput = false;
558565
var dateFormat, closeOnDateSelection, appendToBody, onOpenFocus,
559566
datepickerPopupTemplateUrl, datepickerTemplateUrl, popupEl, datepickerEl,
560-
ngModel, $popup, altInputFormats;
567+
ngModel, ngModelOptions, $popup, altInputFormats;
561568

562569
scope.watchData = {};
563570

564571
this.init = function(_ngModel_) {
565572
ngModel = _ngModel_;
573+
ngModelOptions = _ngModel_.$options || datepickerConfig.ngModelOptions;
566574
closeOnDateSelection = angular.isDefined(attrs.closeOnDateSelection) ? scope.$parent.$eval(attrs.closeOnDateSelection) : datepickerPopupConfig.closeOnDateSelection;
567575
appendToBody = angular.isDefined(attrs.datepickerAppendToBody) ? scope.$parent.$eval(attrs.datepickerAppendToBody) : datepickerPopupConfig.appendToBody;
568576
onOpenFocus = angular.isDefined(attrs.onOpenFocus) ? scope.$parent.$eval(attrs.onOpenFocus) : datepickerPopupConfig.onOpenFocus;
@@ -602,8 +610,11 @@ function(scope, element, attrs, $compile, $parse, $document, $rootScope, $positi
602610

603611
// popup element used to display calendar
604612
popupEl = angular.element('<div uib-datepicker-popup-wrap><div uib-datepicker></div></div>');
613+
scope.ngModelOptions = angular.copy(ngModelOptions);
614+
scope.ngModelOptions.timezone = null;
605615
popupEl.attr({
606616
'ng-model': 'date',
617+
'ng-model-options': 'ngModelOptions',
607618
'ng-change': 'dateSelection(date)',
608619
'template-url': datepickerPopupTemplateUrl
609620
});
@@ -622,7 +633,7 @@ function(scope, element, attrs, $compile, $parse, $document, $rootScope, $positi
622633
if (attrs.datepickerOptions) {
623634
var options = scope.$parent.$eval(attrs.datepickerOptions);
624635
if (options && options.initDate) {
625-
scope.initDate = options.initDate;
636+
scope.initDate = dateParser.fromTimezone(options.initDate, ngModelOptions.timezone);
626637
datepickerEl.attr('init-date', 'initDate');
627638
delete options.initDate;
628639
}
@@ -636,9 +647,12 @@ function(scope, element, attrs, $compile, $parse, $document, $rootScope, $positi
636647
var getAttribute = $parse(attrs[key]);
637648
scope.$parent.$watch(getAttribute, function(value) {
638649
if (key === 'minDate' || key === 'maxDate') {
639-
cache[key] = angular.isDate(value) ? new Date(value) : new Date(dateFilter(value, 'medium'));
650+
cache[key] = angular.isDate(value) ? dateParser.fromTimezone(new Date(value), ngModelOptions.timezone) : new Date(dateFilter(value, 'medium'));
640651
}
641652
scope.watchData[key] = cache[key] || value;
653+
if (key === 'initDate') {
654+
scope.watchData[key] = dateParser.fromTimezone(new Date(value), ngModelOptions.timezone);
655+
}
642656
});
643657
datepickerEl.attr(cameltoDash(key), 'watchData.' + key);
644658

@@ -674,12 +688,16 @@ function(scope, element, attrs, $compile, $parse, $document, $rootScope, $positi
674688
ngModel.$validators.date = validator;
675689
ngModel.$parsers.unshift(parseDate);
676690
ngModel.$formatters.push(function(value) {
677-
scope.date = value;
678-
return ngModel.$isEmpty(value) ? value : dateFilter(value, dateFormat);
691+
if (ngModel.$isEmpty(value)) {
692+
scope.date = value;
693+
return value;
694+
}
695+
scope.date = dateParser.fromTimezone(value, ngModelOptions.timezone);
696+
return dateFilter(scope.date, dateFormat);
679697
});
680698
} else {
681699
ngModel.$formatters.push(function(value) {
682-
scope.date = value;
700+
scope.date = dateParser.fromTimezone(value, ngModelOptions.timezone);
683701
return value;
684702
});
685703
}
@@ -835,7 +853,7 @@ function(scope, element, attrs, $compile, $parse, $document, $rootScope, $positi
835853
if (angular.isString(viewValue)) {
836854
var date = parseDateString(viewValue);
837855
if (!isNaN(date)) {
838-
return date;
856+
return dateParser.toTimezone(date, ngModelOptions.timezone);
839857
}
840858
}
841859

src/datepicker/docs/readme.md

+4-3
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,10 @@ The datepicker has 3 modes:
100100
Number of columns displayed in year selection.
101101

102102
* `ng-model-options`
103-
_(Default: {})_ -
104-
allowInvalid support. [More on ngModelOptions](https://docs.angularjs.org/api/ng/directive/ngModelOptions).
105-
103+
_(Default: `{}`)_ -
104+
Supported properties:
105+
* allowInvalid
106+
* timezone
106107

107108
### uib-datepicker-popup settings ###
108109

0 commit comments

Comments
 (0)