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

feat(datepicker): ng-model-options: timezone #5062

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;

ngModelCtrl.$render = function() {
self.render();
Expand All @@ -109,7 +113,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 @@ -122,13 +126,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 @@ -161,8 +167,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 @@ -546,19 +553,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 @@ -598,8 +606,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 @@ -618,7 +629,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 @@ -632,9 +643,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 @@ -670,12 +684,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 @@ -830,7 +848,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