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 2 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()));
}
}]);
57 changes: 39 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 All @@ -571,6 +579,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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a particular reason for splitting this out?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a great question. Looking through the changes, the distinction seems superfluous. I forget why I made the distinction in the first place. Let me remove it.

ngModelOptions.timezone = null;
} else {
dateFormat = attrs.uibDatepickerPopup || datepickerPopupConfig.datepickerPopup;
attrs.$observe('uibDatepickerPopup', function(value, oldValue) {
Expand Down Expand Up @@ -598,8 +608,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 +631,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;
}
Expand All @@ -632,7 +645,11 @@ 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 || ngModelOptions.timezoneHtml5);
}
scope.watchData[key] = cache[key] || value;
});
Expand Down Expand Up @@ -670,12 +687,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;
});
}
Expand Down Expand Up @@ -830,7 +851,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