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

Commit

Permalink
feat(datepicker): add timezone support
Browse files Browse the repository at this point in the history
- Add timezone support with ngModelOptions

Closes #5062
  • Loading branch information
davious authored and wesleycho committed Dec 16, 2015
1 parent 9019298 commit 09098f8
Show file tree
Hide file tree
Showing 5 changed files with 386 additions and 24 deletions.
32 changes: 32 additions & 0 deletions src/dateparser/dateparser.js
Original file line number Diff line number Diff line change
Expand Up @@ -330,4 +330,36 @@ angular.module('ui.bootstrap.dateparser', [])
function toInt(str) {
return parseInt(str, 10);
}

this.toTimezone = toTimezone;
this.fromTimezone = fromTimezone;
this.timezoneToOffset = timezoneToOffset;
this.addDateMinutes = addDateMinutes;
this.convertTimezoneToLocal = convertTimezoneToLocal;

function toTimezone(date, timezone) {
return date && timezone ? convertTimezoneToLocal(date, timezone) : date;
}

function fromTimezone(date, timezone) {
return date && timezone ? convertTimezoneToLocal(date, timezone, true) : date;
}

//https://github.com/angular/angular.js/blob/4daafd3dbe6a80d578f5a31df1bb99c77559543e/src/Angular.js#L1207
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 convertTimezoneToLocal(date, timezone, reverse) {
reverse = reverse ? -1 : 1;
var timezoneOffset = timezoneToOffset(timezone, date.getTimezoneOffset());
return addDateMinutes(date, reverse * (timezoneOffset - date.getTimezoneOffset()));
}
}]);
112 changes: 112 additions & 0 deletions src/dateparser/test/dateparser.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -387,4 +387,116 @@ describe('date parser', function() {

expect(dateParser.init).toHaveBeenCalled();
}));


describe('timezone functions', function() {
describe('toTimezone', function() {
it('adjusts date: PST - EST', function() {
var date = new Date('2008-01-01T00:00:00.000Z');
var toWestDate = dateParser.toTimezone(date, 'PST');
var toEastDate = dateParser.toTimezone(date, 'EST');
expect(toWestDate.getTime() - toEastDate.getTime()).toEqual(1000 * 60 * 60 * 3);
});

it('adjusts date: GMT-0500 - GMT+0500', function() {
var date = new Date('2008-01-01T00:00:00.000Z');
var toWestDate = dateParser.toTimezone(date, 'GMT-0500');
var toEastDate = dateParser.toTimezone(date, 'GMT+0500');
expect(toWestDate.getTime() - toEastDate.getTime()).toEqual(1000 * 60 * 60 * 10);
});

it('adjusts date: -600 - +600', function() {
var date = new Date('2008-01-01T00:00:00.000Z');
var toWestDate = dateParser.toTimezone(date, '-600');
var toEastDate = dateParser.toTimezone(date, '+600');
expect(toWestDate.getTime() - toEastDate.getTime()).toEqual(1000 * 60 * 60 * 12);
});

it('tolerates null date', function() {
var date = null;
var toNullDate = dateParser.toTimezone(date, '-600');
expect(toNullDate).toEqual(date);
});

it('tolerates null timezone', function() {
var date = new Date('2008-01-01T00:00:00.000Z');
var toNullTimezoneDate = dateParser.toTimezone(date, null);
expect(toNullTimezoneDate).toEqual(date);
});
});

describe('fromTimezone', function() {
it('adjusts date: PST - EST', function() {
var date = new Date('2008-01-01T00:00:00.000Z');
var fromWestDate = dateParser.fromTimezone(date, 'PST');
var fromEastDate = dateParser.fromTimezone(date, 'EST');
expect(fromWestDate.getTime() - fromEastDate.getTime()).toEqual(1000 * 60 * 60 * -3);
});

it('adjusts date: GMT-0500 - GMT+0500', function() {
var date = new Date('2008-01-01T00:00:00.000Z');
var fromWestDate = dateParser.fromTimezone(date, 'GMT-0500');
var fromEastDate = dateParser.fromTimezone(date, 'GMT+0500');
expect(fromWestDate.getTime() - fromEastDate.getTime()).toEqual(1000 * 60 * 60 * -10);
});

it('adjusts date: -600 - +600', function() {
var date = new Date('2008-01-01T00:00:00.000Z');
var fromWestDate = dateParser.fromTimezone(date, '-600');
var fromEastDate = dateParser.fromTimezone(date, '+600');
expect(fromWestDate.getTime() - fromEastDate.getTime()).toEqual(1000 * 60 * 60 * -12);
});

it('tolerates null date', function() {
var date = null;
var toNullDate = dateParser.fromTimezone(date, '-600');
expect(toNullDate).toEqual(date);
});

it('tolerates null timezone', function() {
var date = new Date('2008-01-01T00:00:00.000Z');
var toNullTimezoneDate = dateParser.fromTimezone(date, null);
expect(toNullTimezoneDate).toEqual(date);
});
});

describe('timezoneToOffset', function() {
it('calculates minutes off from current timezone', function() {
var offsetMinutesUtc = dateParser.timezoneToOffset('UTC');
var offsetMinutesUtcPlus1 = dateParser.timezoneToOffset('GMT+0100');
expect(offsetMinutesUtc - offsetMinutesUtcPlus1).toEqual(60);
});
});

describe('addDateMinutes', function() {
it('adds minutes to a date', function() {
var date = new Date('2008-01-01T00:00:00.000Z');
var oneHourMore = dateParser.addDateMinutes(date, 60);
expect(oneHourMore).toEqual(new Date('2008-01-01T01:00:00.000Z'));
});
});

describe('convertTimezoneToLocal', function() {
it('adjusts date: PST - EST', function() {
var date = new Date('2008-01-01T00:00:00.000Z');
var toWestDate = dateParser.convertTimezoneToLocal(date, 'PST');
var toEastDate = dateParser.convertTimezoneToLocal(date, 'EST');
expect(toWestDate.getTime() - toEastDate.getTime()).toEqual(1000 * 60 * 60 * 3);
});

it('adjusts date: GMT-0500 - GMT+0500', function() {
var date = new Date('2008-01-01T00:00:00.000Z');
var toWestDate = dateParser.convertTimezoneToLocal(date, 'GMT-0500');
var toEastDate = dateParser.convertTimezoneToLocal(date, 'GMT+0500');
expect(toWestDate.getTime() - toEastDate.getTime()).toEqual(1000 * 60 * 60 * 10);
});

it('adjusts date: -600 - +600', function() {
var date = new Date('2008-01-01T00:00:00.000Z');
var toWestDate = dateParser.convertTimezoneToLocal(date, '-600');
var toEastDate = dateParser.convertTimezoneToLocal(date, '+600');
expect(toWestDate.getTime() - toEastDate.getTime()).toEqual(1000 * 60 * 60 * 12);
});
});
});
});
54 changes: 36 additions & 18 deletions src/datepicker/datepicker.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,15 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
yearColumns: 5,
minDate: null,
maxDate: null,
shortcutPropagation: false
shortcutPropagation: false,
ngModelOptions: {}
})

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

// Modes chain
this.modes = ['day', 'month', 'year'];
Expand All @@ -42,11 +45,11 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
angular.forEach(['minDate', 'maxDate'], function(key) {
if ($attrs[key]) {
$scope.$parent.$watch($attrs[key], function(value) {
self[key] = value ? angular.isDate(value) ? new Date(value) : new Date(dateFilter(value, 'medium')) : null;
self[key] = value ? angular.isDate(value) ? dateParser.fromTimezone(new Date(value), ngModelOptions.timezone) : new Date(dateFilter(value, 'medium')) : null;
self.refreshView();
});
} else {
self[key] = datepickerConfig[key] ? new Date(datepickerConfig[key]) : null;
self[key] = datepickerConfig[key] ? dateParser.fromTimezone(new Date(datepickerConfig[key]), ngModelOptions.timezone) : null;
}
});

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

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

this.init = function(ngModelCtrl_) {
ngModelCtrl = ngModelCtrl_;
ngModelOptions = ngModelCtrl_.$options || datepickerConfig.ngModelOptions;

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

if (isValid) {
this.activeDate = date;
this.activeDate = dateParser.fromTimezone(date, ngModelOptions.timezone);
} else if (!$datepickerSuppressError) {
$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.');
}
Expand All @@ -126,13 +130,15 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
this._refreshView();

var date = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null;
date = dateParser.fromTimezone(date, ngModelOptions.timezone);
ngModelCtrl.$setValidity('dateDisabled', !date ||
this.element && !this.isDisabled(date));
}
};

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

$scope.select = function(date) {
if ($scope.datepickerMode === self.minMode) {
var dt = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : new Date(0, 0, 0, 0, 0, 0, 0);
var dt = ngModelCtrl.$viewValue ? dateParser.fromTimezone(new Date(ngModelCtrl.$viewValue), ngModelOptions.timezone) : new Date(0, 0, 0, 0, 0, 0, 0);
dt.setFullYear(date.getFullYear(), date.getMonth(), date.getDate());
dt = dateParser.toTimezone(dt, ngModelOptions.timezone);
ngModelCtrl.$setViewValue(dt);
ngModelCtrl.$render();
} else {
Expand Down Expand Up @@ -550,19 +557,20 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
altInputFormats: []
})

.controller('UibDatepickerPopupController', ['$scope', '$element', '$attrs', '$compile', '$parse', '$document', '$rootScope', '$uibPosition', 'dateFilter', 'uibDateParser', 'uibDatepickerPopupConfig', '$timeout',
function(scope, element, attrs, $compile, $parse, $document, $rootScope, $position, dateFilter, dateParser, datepickerPopupConfig, $timeout) {
.controller('UibDatepickerPopupController', ['$scope', '$element', '$attrs', '$compile', '$parse', '$document', '$rootScope', '$uibPosition', 'dateFilter', 'uibDateParser', 'uibDatepickerPopupConfig', '$timeout', 'uibDatepickerConfig',
function(scope, element, attrs, $compile, $parse, $document, $rootScope, $position, dateFilter, dateParser, datepickerPopupConfig, $timeout, datepickerConfig) {
var self = this;
var cache = {},
isHtml5DateInput = false;
var dateFormat, closeOnDateSelection, appendToBody, onOpenFocus,
datepickerPopupTemplateUrl, datepickerTemplateUrl, popupEl, datepickerEl,
ngModel, $popup, altInputFormats;
ngModel, ngModelOptions, $popup, altInputFormats;

scope.watchData = {};

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

// popup element used to display calendar
popupEl = angular.element('<div uib-datepicker-popup-wrap><div uib-datepicker></div></div>');
scope.ngModelOptions = angular.copy(ngModelOptions);
scope.ngModelOptions.timezone = null;
popupEl.attr({
'ng-model': 'date',
'ng-model-options': 'ngModelOptions',
'ng-change': 'dateSelection(date)',
'template-url': datepickerPopupTemplateUrl
});
Expand All @@ -622,7 +633,7 @@ function(scope, element, attrs, $compile, $parse, $document, $rootScope, $positi
if (attrs.datepickerOptions) {
var options = scope.$parent.$eval(attrs.datepickerOptions);
if (options && options.initDate) {
scope.initDate = options.initDate;
scope.initDate = dateParser.fromTimezone(options.initDate, ngModelOptions.timezone);
datepickerEl.attr('init-date', 'initDate');
delete options.initDate;
}
Expand All @@ -636,9 +647,12 @@ function(scope, element, attrs, $compile, $parse, $document, $rootScope, $positi
var getAttribute = $parse(attrs[key]);
scope.$parent.$watch(getAttribute, function(value) {
if (key === 'minDate' || key === 'maxDate') {
cache[key] = angular.isDate(value) ? new Date(value) : new Date(dateFilter(value, 'medium'));
cache[key] = angular.isDate(value) ? dateParser.fromTimezone(new Date(value), ngModelOptions.timezone) : new Date(dateFilter(value, 'medium'));
}
scope.watchData[key] = cache[key] || value;
if (key === 'initDate') {
scope.watchData[key] = dateParser.fromTimezone(new Date(value), ngModelOptions.timezone);
}
});
datepickerEl.attr(cameltoDash(key), 'watchData.' + key);

Expand Down Expand Up @@ -674,12 +688,16 @@ function(scope, element, attrs, $compile, $parse, $document, $rootScope, $positi
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 (ngModel.$isEmpty(value)) {
scope.date = value;
return value;
}
scope.date = dateParser.fromTimezone(value, ngModelOptions.timezone);
return dateFilter(scope.date, dateFormat);
});
} else {
ngModel.$formatters.push(function(value) {
scope.date = value;
scope.date = dateParser.fromTimezone(value, ngModelOptions.timezone);
return value;
});
}
Expand Down Expand Up @@ -835,7 +853,7 @@ function(scope, element, attrs, $compile, $parse, $document, $rootScope, $positi
if (angular.isString(viewValue)) {
var date = parseDateString(viewValue);
if (!isNaN(date)) {
return date;
return dateParser.toTimezone(date, ngModelOptions.timezone);
}
}

Expand Down
7 changes: 4 additions & 3 deletions src/datepicker/docs/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,10 @@ The datepicker has 3 modes:
Number of columns displayed in year selection.

* `ng-model-options`
_(Default: {})_ -
allowInvalid support. [More on ngModelOptions](https://docs.angularjs.org/api/ng/directive/ngModelOptions).

_(Default: `{}`)_ -
Supported properties:
* allowInvalid
* timezone

### uib-datepicker-popup settings ###

Expand Down
Loading

0 comments on commit 09098f8

Please sign in to comment.