diff --git a/src/dateparser/dateparser.js b/src/dateparser/dateparser.js
index b60b7865d0..fd6670bca8 100644
--- a/src/dateparser/dateparser.js
+++ b/src/dateparser/dateparser.js
@@ -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()));
+ }
}]);
diff --git a/src/datepicker/datepicker.js b/src/datepicker/datepicker.js
index 0caa38ea63..5b609db0f5 100644
--- a/src/datepicker/datepicker.js
+++ b/src/datepicker/datepicker.js
@@ -17,12 +17,15 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
yearRange: 20,
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'];
@@ -41,11 +44,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 ? new Date(value) : null;
+ self[key] = value ? dateParser.fromTimezone(new Date(value), ngModelOptions.timezone) : 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;
}
});
@@ -67,10 +70,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();
}
});
@@ -96,6 +99,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();
@@ -108,7 +112,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.');
}
@@ -121,6 +125,7 @@ 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));
}
@@ -128,6 +133,7 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
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),
@@ -160,8 +166,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 {
@@ -543,19 +550,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;
@@ -568,6 +576,8 @@ function(scope, element, attrs, $compile, $parse, $document, $rootScope, $positi
if (datepickerPopupConfig.html5Types[attrs.type]) {
dateFormat = datepickerPopupConfig.html5Types[attrs.type];
isHtml5DateInput = true;
+ ngModelOptions.timezoneHtml5 = ngModelOptions.timezone;
+ ngModelOptions.timezone = null;
} else {
dateFormat = attrs.uibDatepickerPopup || datepickerPopupConfig.datepickerPopup;
attrs.$observe('uibDatepickerPopup', function(value, oldValue) {
@@ -595,8 +605,11 @@ function(scope, element, attrs, $compile, $parse, $document, $rootScope, $positi
// popup element used to display calendar
popupEl = angular.element('
');
+ 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
});
@@ -615,7 +628,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 || ngModelOptions.timezoneHtml5);
datepickerEl.attr('init-date', 'initDate');
delete options.initDate;
}
@@ -628,9 +641,12 @@ function(scope, element, attrs, $compile, $parse, $document, $rootScope, $positi
if (attrs[key]) {
var getAttribute = $parse(attrs[key]);
scope.$parent.$watch(getAttribute, function(value) {
- scope.watchData[key] = value;
if (key === 'minDate' || key === 'maxDate') {
- cache[key] = new Date(value);
+ cache[key] = dateParser.fromTimezone(new Date(value), ngModelOptions.timezone);
+ }
+ scope.watchData[key] = cache[key] || value;
+ if (key === 'initDate') {
+ scope.watchData[key] = dateParser.fromTimezone(new Date(value), ngModelOptions.timezone || ngModelOptions.timezoneHtml5);
}
});
datepickerEl.attr(cameltoDash(key), 'watchData.' + key);
@@ -667,12 +683,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.timezoneHtml5);
return value;
});
}
@@ -827,7 +847,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);
}
}
diff --git a/src/datepicker/docs/readme.md b/src/datepicker/docs/readme.md
index bfdc1312ff..f43a3a9c8a 100644
--- a/src/datepicker/docs/readme.md
+++ b/src/datepicker/docs/readme.md
@@ -100,6 +100,10 @@ The datepicker has 3 modes:
allowInvalid support. [More on ngModelOptions](https://docs.angularjs.org/api/ng/directive/ngModelOptions).
+* `ng-model-options`
+ _(Default: {})_ -
+ Timezone support. [More on ngModelOptions](https://docs.angularjs.org/api/ng/directive/ngModelOptions).
+
### uib-datepicker-popup settings ###
Options for the uib-datepicker must be passed as JSON using the `datepicker-options` attribute. This list is only for popup settings.
diff --git a/src/datepicker/test/datepicker.spec.js b/src/datepicker/test/datepicker.spec.js
index 57ae84b94d..565ad94d2a 100644
--- a/src/datepicker/test/datepicker.spec.js
+++ b/src/datepicker/test/datepicker.spec.js
@@ -110,6 +110,14 @@ describe('datepicker', function() {
});
}
+ function getSelectedElement(index) {
+ var buttons = getAllOptionsEl();
+ var el = $.grep(buttons, function(button, idx) {
+ return angular.element(button).hasClass('btn-info');
+ })[0];
+ return angular.element(el);
+ }
+
function triggerKeyDown(element, key, ctrl) {
var keyCodes = {
'enter': 13,
@@ -1292,6 +1300,70 @@ describe('datepicker', function() {
});
});
+ describe('datepickerConfig.ngModelOptions', function() {
+ describe('timezone', function() {
+ var originalConfig = {};
+ beforeEach(inject(function(uibDatepickerConfig) {
+ angular.extend(originalConfig, uibDatepickerConfig);
+ uibDatepickerConfig.ngModelOptions = { timezone: 'UTC' };
+ $rootScope.date = new Date('2005-11-07T00:00:00.000Z');
+ element = $compile('')($rootScope);
+ $rootScope.$digest();
+ }));
+
+ afterEach(inject(function(uibDatepickerConfig) {
+ // return it to the original state
+ angular.extend(uibDatepickerConfig, originalConfig);
+ }));
+
+ it('sets date to appropriate date', function() {
+ expectSelectedElement(8);
+ });
+
+ it('updates the input when a day is clicked', function() {
+ clickOption(9);
+ expect($rootScope.date).toEqual(new Date('2005-11-08T00:00:00.000Z'));
+ });
+
+ it('init date', function() {
+ $rootScope.initDate = new Date('2006-01-01T00:00:00.000Z');
+ $rootScope.date = null;
+ element = $compile('')($rootScope);
+ $rootScope.$digest();
+
+ expect(getTitle()).toEqual('January 2006');
+ });
+
+ it('min date', inject(function(uibDateParser) {
+ $rootScope.minDate = new Date('2010-09-31T00:00:00.000Z');
+ element = $compile('')($rootScope);
+ $rootScope.$digest();
+
+ expect(getSelectedElement().prop('disabled')).toBe(true);
+ }));
+ });
+ });
+
+ describe('uib-datepicker ng-model-options', function() {
+ describe('timezone', function() {
+ beforeEach(inject(function() {
+ $rootScope.date = new Date('2005-11-07T00:00:00.000Z');
+ $rootScope.ngModelOptions = { timezone: 'UTC'};
+ element = $compile('')($rootScope);
+ $rootScope.$digest();
+ }));
+
+ it('sets date to appropriate date', function() {
+ expectSelectedElement(8);
+ });
+
+ it('updates the input when a day is clicked', function() {
+ clickOption(9);
+ expect($rootScope.date).toEqual(new Date('2005-11-08T00:00:00.000Z'));
+ });
+ });
+ });
+
describe('setting datepickerPopupConfig', function() {
var originalConfig = {};
beforeEach(inject(function(uibDatepickerPopupConfig) {
@@ -2493,6 +2565,133 @@ describe('datepicker', function() {
});
});
+ describe('uibDatepickerConfig.ngModelOptions', function() {
+ describe('timezone pop-up', function() {
+ var inputEl, dropdownEl, $document, $sniffer, $timeout;
+
+ function assignElements(wrapElement) {
+ inputEl = wrapElement.find('input');
+ dropdownEl = wrapElement.find('ul');
+ element = dropdownEl.find('table');
+ }
+
+ beforeEach(inject(function(uibDatepickerConfig) {
+ uibDatepickerConfig.ngModelOptions = { timezone: 'UTC' };
+ $rootScope.date = new Date('2010-09-30T00:00:00.000Z');
+ $rootScope.isopen = true;
+ var wrapper = $compile('')($rootScope);
+ $rootScope.$digest();
+ assignElements(wrapper);
+ }));
+
+ afterEach(inject(function (uibDatepickerConfig) {
+ // return it to the original state
+ uibDatepickerConfig.ngModelOptions = {};
+ }));
+
+ it('interprets the date appropriately', function() {
+ expect(inputEl.val()).toBe('09/30/2010');
+ });
+
+ it('updates the input when a day is clicked', function() {
+ clickOption(17);
+ expect(inputEl.val()).toBe('09/15/2010');
+ expect($rootScope.date).toEqual(new Date('2010-09-15T00:00:00.000Z'));
+ });
+
+ it('shows the correct title', function() {
+ expect(getTitle()).toBe('September 2010');
+ });
+
+ it('init date', inject(function(uibDateParser) {
+ $rootScope.initDate = new Date('2006-01-01T00:00:00.000Z');
+ $rootScope.date = null;
+ var wrapper = $compile('')($rootScope);
+ $rootScope.$digest();
+ assignElements(wrapper);
+
+ expect(getTitle()).toBe('January 2006');
+ }));
+
+ it('min date', inject(function(uibDateParser) {
+ $rootScope.minDate = new Date('2010-09-31T00:00:00.000Z');
+ var wrapper = $compile('
')($rootScope);
+ $rootScope.$digest();
+ assignElements(wrapper);
+
+ expect(getSelectedElement().prop('disabled')).toBe(true);
+ }));
+ });
+ });
+
+ describe('ng-model-options', function() {
+ describe('timezone', function() {
+ var inputEl, dropdownEl, $document, $sniffer, $timeout;
+
+ function assignElements(wrapElement) {
+ inputEl = wrapElement.find('input');
+ dropdownEl = wrapElement.find('ul');
+ element = dropdownEl.find('table');
+ }
+
+ beforeEach(inject(function() {
+ $rootScope.date = new Date('2010-09-30T00:00:00.000Z');
+ $rootScope.ngModelOptions = { timezone: 'UTC' };
+ $rootScope.isopen = true;
+ var wrapper = $compile('
')($rootScope);
+ $rootScope.$digest();
+ assignElements(wrapper);
+ }));
+
+ it('interprets the date appropriately', function() {
+ expect(inputEl.val()).toBe('09/30/2010');
+ });
+
+ it('has `selected` only the correct day', function() {
+ expectSelectedElement(32);
+ });
+
+ it('updates the input when a day is clicked', function() {
+ clickOption(17);
+ expect(inputEl.val()).toBe('09/15/2010');
+ expect($rootScope.date).toEqual(new Date('2010-09-15T00:00:00.000Z'));
+ });
+ });
+
+ describe('timezone HTML5', function() {
+ var inputEl, dropdownEl, $document, $sniffer, $timeout;
+
+ function assignElements(wrapElement) {
+ inputEl = wrapElement.find('input');
+ dropdownEl = wrapElement.find('ul');
+ element = dropdownEl.find('table');
+ }
+
+ beforeEach(function() {
+ $rootScope.date = new Date('2010-09-30T00:00:00.000Z');
+ $rootScope.ngModelOptions = { timezone: 'UTC' };
+ $rootScope.isopen = true;
+ var wrapper = $compile('
')($rootScope);
+ $rootScope.$digest();
+ assignElements(wrapper);
+ });
+
+ it('interprets the date appropriately', function() {
+ expect(inputEl.val()).toBe('2010-09-30');
+ });
+
+ it('has `selected` only the correct day', function() {
+ expectSelectedElement(32);
+ });
+
+ it('updates the input when a day is clicked', function() {
+ clickOption(17);
+ expect(inputEl.val()).toBe('2010-09-15');
+ expect($rootScope.date).toEqual(new Date('2010-09-15T00:00:00.000Z'));
+ });
+ });
+ });
+
describe('with empty initial state', function() {
beforeEach(inject(function() {
$rootScope.date = null;