Skip to content

Commit cba7a61

Browse files
committed
feat(datepicker): ng-model-options: timezone wip
for angular-ui#4837
1 parent 763cfd9 commit cba7a61

File tree

3 files changed

+263
-25
lines changed

3 files changed

+263
-25
lines changed

Diff for: src/datepicker/datepicker.js

+85-25
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,15 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
1717
yearRange: 20,
1818
minDate: null,
1919
maxDate: null,
20-
shortcutPropagation: false
20+
shortcutPropagation: false,
21+
ngModelOptions: {}
2122
})
2223

23-
.controller('UibDatepickerController', ['$scope', '$attrs', '$parse', '$interpolate', '$log', 'dateFilter', 'uibDatepickerConfig', '$datepickerSuppressError', function($scope, $attrs, $parse, $interpolate, $log, dateFilter, datepickerConfig, $datepickerSuppressError) {
24+
.controller('UibDatepickerController', ['$scope', '$attrs', '$parse', '$interpolate', '$log', 'dateFilter', 'uibDatepickerConfig', '$datepickerSuppressError', 'uibDateUtils',
25+
function($scope, $attrs, $parse, $interpolate, $log, dateFilter, datepickerConfig, $datepickerSuppressError, dateUtils) {
2426
var self = this,
25-
ngModelCtrl = { $setViewValue: angular.noop }; // nullModelCtrl;
27+
ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl;
28+
ngModelOptions = {};
2629

2730
// Modes chain
2831
this.modes = ['day', 'month', 'year'];
@@ -41,11 +44,11 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
4144
angular.forEach(['minDate', 'maxDate'], function(key) {
4245
if ($attrs[key]) {
4346
$scope.$parent.$watch($attrs[key], function(value) {
44-
self[key] = value ? new Date(value) : null;
47+
self[key] = value ? dateUtils.fromTimezone(new Date(value), ngModelOptions.timezone) : null;
4548
self.refreshView();
4649
});
4750
} else {
48-
self[key] = datepickerConfig[key] ? new Date(datepickerConfig[key]) : null;
51+
self[key] = datepickerConfig[key] ? dateUtils.fromTimezone(new Date(datepickerConfig[key]), ngModelOptions.timezone) : null;
4952
}
5053
});
5154

@@ -70,7 +73,7 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
7073
this.activeDate = $scope.$parent.$eval($attrs.initDate) || new Date();
7174
$scope.$parent.$watch($attrs.initDate, function(initDate) {
7275
if (initDate && (ngModelCtrl.$isEmpty(ngModelCtrl.$modelValue) || ngModelCtrl.$invalid)) {
73-
self.activeDate = initDate;
76+
self.activeDate = dateUtils.fromTimezone(initDate, ngModelOptions.timezone);
7477
self.refreshView();
7578
}
7679
});
@@ -94,8 +97,9 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
9497
return false;
9598
};
9699

97-
this.init = function(ngModelCtrl_) {
100+
this.init = function(ngModelCtrl_, ngModelOptions_) {
98101
ngModelCtrl = ngModelCtrl_;
102+
ngModelOptions = ngModelOptions_ && ngModelOptions_.$options || datepickerConfig.ngModelOptions;
99103

100104
ngModelCtrl.$render = function() {
101105
self.render();
@@ -108,7 +112,7 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
108112
isValid = !isNaN(date);
109113

110114
if (isValid) {
111-
this.activeDate = date;
115+
this.activeDate = dateUtils.fromTimezone(date, ngModelOptions.timezone);
112116
} else if (!$datepickerSuppressError) {
113117
$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.');
114118
}
@@ -121,13 +125,15 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
121125
this._refreshView();
122126

123127
var date = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null;
128+
date = dateUtils.fromTimezone(date, ngModelOptions.timezone);
124129
ngModelCtrl.$setValidity('dateDisabled', !date ||
125130
this.element && !this.isDisabled(date));
126131
}
127132
};
128133

129134
this.createDateObject = function(date, format) {
130135
var model = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null;
136+
model = dateUtils.fromTimezone(model, ngModelOptions.timezone);
131137
return {
132138
date: date,
133139
label: dateFilter(date, format),
@@ -161,7 +167,9 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
161167
$scope.select = function(date) {
162168
if ($scope.datepickerMode === self.minMode) {
163169
var dt = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : new Date(0, 0, 0, 0, 0, 0, 0);
170+
dt = dateUtils.fromTimezone(dt, ngModelOptions.timezone);
164171
dt.setFullYear(date.getFullYear(), date.getMonth(), date.getDate());
172+
dt = dateUtils.toTimezone(dt, ngModelOptions.timezone);
165173
ngModelCtrl.$setViewValue(dt);
166174
ngModelCtrl.$render();
167175
} else {
@@ -461,13 +469,16 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
461469
customClass: '&',
462470
shortcutPropagation: '&?'
463471
},
464-
require: ['uibDatepicker', '^ngModel'],
472+
require: ['uibDatepicker', '^ngModel', '^?ngModelOptions'],
465473
controller: 'UibDatepickerController',
466474
controllerAs: 'datepicker',
467475
link: function(scope, element, attrs, ctrls) {
468-
var datepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1];
476+
var datepickerCtrl = ctrls[0],
477+
ngModelCtrl = ctrls[1],
478+
ngModelOptions = ctrls[2];
479+
469480

470-
datepickerCtrl.init(ngModelCtrl);
481+
datepickerCtrl.init(ngModelCtrl, ngModelOptions);
471482
}
472483
};
473484
})
@@ -543,19 +554,20 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
543554
altInputFormats: []
544555
})
545556

546-
.controller('UibDatepickerPopupController', ['$scope', '$element', '$attrs', '$compile', '$parse', '$document', '$rootScope', '$uibPosition', 'dateFilter', 'uibDateParser', 'uibDatepickerPopupConfig', '$timeout',
547-
function(scope, element, attrs, $compile, $parse, $document, $rootScope, $position, dateFilter, dateParser, datepickerPopupConfig, $timeout) {
557+
.controller('UibDatepickerPopupController', ['$scope', '$element', '$attrs', '$compile', '$parse', '$document', '$rootScope', '$uibPosition', 'dateFilter', 'uibDateParser', 'uibDatepickerPopupConfig', '$timeout', 'uibDateUtils', 'uibDatepickerConfig',
558+
function(scope, element, attrs, $compile, $parse, $document, $rootScope, $position, dateFilter, dateParser, datepickerPopupConfig, $timeout, dateUtils, datepickerConfig) {
548559
var self = this;
549560
var cache = {},
550561
isHtml5DateInput = false;
551562
var dateFormat, closeOnDateSelection, appendToBody, onOpenFocus,
552563
datepickerPopupTemplateUrl, datepickerTemplateUrl, popupEl, datepickerEl,
553-
ngModel, $popup, altInputFormats;
564+
ngModel, ngModelOptions, $popup, altInputFormats;
554565

555566
scope.watchData = {};
556567

557-
this.init = function(_ngModel_) {
568+
this.init = function(_ngModel_, _ngModelOptions_) {
558569
ngModel = _ngModel_;
570+
ngModelOptions = _ngModelOptions_ && _ngModelOptions_.$options || datepickerConfig.ngModelOptions;
559571
closeOnDateSelection = angular.isDefined(attrs.closeOnDateSelection) ? scope.$parent.$eval(attrs.closeOnDateSelection) : datepickerPopupConfig.closeOnDateSelection;
560572
appendToBody = angular.isDefined(attrs.datepickerAppendToBody) ? scope.$parent.$eval(attrs.datepickerAppendToBody) : datepickerPopupConfig.appendToBody;
561573
onOpenFocus = angular.isDefined(attrs.onOpenFocus) ? scope.$parent.$eval(attrs.onOpenFocus) : datepickerPopupConfig.onOpenFocus;
@@ -605,6 +617,9 @@ function(scope, element, attrs, $compile, $parse, $document, $rootScope, $positi
605617
datepickerEl = angular.element(popupEl.children()[0]);
606618
datepickerEl.attr('template-url', datepickerTemplateUrl);
607619

620+
scope.ngModelOptions = { $options: angular.extend({ timezone: null }, ngModelOptions) };
621+
datepickerEl.attr({'ng-model-options': 'ngModelOptions'});
622+
608623
if (isHtml5DateInput) {
609624
if (attrs.type === 'month') {
610625
datepickerEl.attr('datepicker-mode', '"month"');
@@ -615,7 +630,7 @@ function(scope, element, attrs, $compile, $parse, $document, $rootScope, $positi
615630
if (attrs.datepickerOptions) {
616631
var options = scope.$parent.$eval(attrs.datepickerOptions);
617632
if (options && options.initDate) {
618-
scope.initDate = options.initDate;
633+
scope.initDate = dateUtils.fromTimezone(options.initDate, ngModelOptions.timezone);
619634
datepickerEl.attr('init-date', 'initDate');
620635
delete options.initDate;
621636
}
@@ -628,9 +643,12 @@ function(scope, element, attrs, $compile, $parse, $document, $rootScope, $positi
628643
if (attrs[key]) {
629644
var getAttribute = $parse(attrs[key]);
630645
scope.$parent.$watch(getAttribute, function(value) {
631-
scope.watchData[key] = value;
632646
if (key === 'minDate' || key === 'maxDate') {
633-
cache[key] = new Date(value);
647+
cache[key] = dateUtils.fromTimezone(new Date(value), ngModelOptions.timezone);
648+
}
649+
scope.watchData[key] = cache[key] || value;
650+
if (key === 'initDate') {
651+
scope.watchData[key] = dateUtils.fromTimezone(new Date(value), ngModelOptions.timezone);
634652
}
635653
});
636654
datepickerEl.attr(cameltoDash(key), 'watchData.' + key);
@@ -667,8 +685,13 @@ function(scope, element, attrs, $compile, $parse, $document, $rootScope, $positi
667685
ngModel.$validators.date = validator;
668686
ngModel.$parsers.unshift(parseDate);
669687
ngModel.$formatters.push(function(value) {
670-
scope.date = value;
671-
return ngModel.$isEmpty(value) ? value : dateFilter(value, dateFormat);
688+
if (ngModel.$isEmpty(value)) {
689+
scope.date = value;
690+
return value;
691+
}
692+
var dt = dateUtils.fromTimezone(value, ngModelOptions.timezone);
693+
scope.date = dt;
694+
return dateFilter(dt, dateFormat);
672695
});
673696
} else {
674697
ngModel.$formatters.push(function(value) {
@@ -825,7 +848,7 @@ function(scope, element, attrs, $compile, $parse, $document, $rootScope, $positi
825848
return undefined;
826849
}
827850

828-
return date;
851+
return dateUtils.toTimezone(date, ngModelOptions.timezone);
829852
}
830853

831854
return undefined;
@@ -903,7 +926,7 @@ function(scope, element, attrs, $compile, $parse, $document, $rootScope, $positi
903926

904927
.directive('uibDatepickerPopup', function() {
905928
return {
906-
require: ['ngModel', 'uibDatepickerPopup'],
929+
require: ['uibDatepickerPopup', 'ngModel', '^?ngModelOptions'],
907930
controller: 'UibDatepickerPopupController',
908931
scope: {
909932
isOpen: '=?',
@@ -914,10 +937,11 @@ function(scope, element, attrs, $compile, $parse, $document, $rootScope, $positi
914937
customClass: '&'
915938
},
916939
link: function(scope, element, attrs, ctrls) {
917-
var ngModel = ctrls[0],
918-
ctrl = ctrls[1];
940+
var ctrl = ctrls[0],
941+
ngModel = ctrls[1],
942+
ngModelOptions = ctrls[2];
919943

920-
ctrl.init(ngModel);
944+
ctrl.init(ngModel, ngModelOptions);
921945
}
922946
};
923947
})
@@ -930,4 +954,40 @@ function(scope, element, attrs, $compile, $parse, $document, $rootScope, $positi
930954
return attrs.templateUrl || 'uib/template/datepicker/popup.html';
931955
}
932956
};
957+
})
958+
959+
.service('uibDateUtils', function() {
960+
return {
961+
toTimezone: toTimezone,
962+
fromTimezone: fromTimezone,
963+
timezoneToOffset: timezoneToOffset,
964+
addDateMinutes: addDateMinutes,
965+
convertTimezoneToLocal: convertTimezoneToLocal
966+
};
967+
968+
function toTimezone(date, timezone) {
969+
return timezone ? convertTimezoneToLocal(date, timezone) : date;
970+
}
971+
972+
function fromTimezone(date, timezone) {
973+
return timezone ? convertTimezoneToLocal(date, timezone, true) : date;
974+
}
975+
976+
//https://github.com/angular/angular.js/blob/4daafd3dbe6a80d578f5a31df1bb99c77559543e/src/Angular.js#L1207
977+
function timezoneToOffset(timezone, fallback) {
978+
var requestedTimezoneOffset = Date.parse('Jan 01, 1970 00:00:00 ' + timezone) / 60000;
979+
return isNaN(requestedTimezoneOffset) ? fallback : requestedTimezoneOffset;
980+
}
981+
982+
function addDateMinutes(date, minutes) {
983+
date = new Date(date.getTime());
984+
date.setMinutes(date.getMinutes() + minutes);
985+
return date;
986+
}
987+
988+
function convertTimezoneToLocal(date, timezone, reverse) {
989+
reverse = reverse ? -1 : 1;
990+
var timezoneOffset = timezoneToOffset(timezone, date.getTimezoneOffset());
991+
return addDateMinutes(date, reverse * (timezoneOffset - date.getTimezoneOffset()));
992+
}
933993
});

Diff for: src/datepicker/docs/readme.md

+4
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ The datepicker has 3 modes:
9595
_(Default: `20`)_ -
9696
Number of years displayed in year selection.
9797

98+
* `ng-model-options`
99+
_(Default: {})_ -
100+
Timezone support. [More on ngModelOptions](https://docs.angularjs.org/api/ng/directive/ngModelOptions).
101+
98102
### uib-datepicker-popup settings ###
99103

100104
Options for the uib-datepicker must be passed as JSON using the `datepicker-options` attribute. This list is only for popup settings.

0 commit comments

Comments
 (0)