Skip to content
This repository was archived by the owner on Sep 5, 2024. It is now read-only.

Commit 2a01746

Browse files
committed
feat(calendar): support specifying timezone in ng-model-options
- remove calls to `focusAfterAppend.focus()` from both monthBody and yearBody - as these were breaking initial scrolling in day mode and selection in month mode - fix cases where functions were being called with string or timestamp arguments - when they only accepted `Date` arguments - add event types when calling `$setViewValue()` - rename `CalendarCtrl.focus()` to `CalendarCtrl.focusDate()` to be more clear - and to avoid conflicts when analyzing code - fix a number of JSDoc issues with types - remove special initialization code from Calendar - it was working around #8585, which is now fixed Fixes #10431
1 parent 5fbabe7 commit 2a01746

10 files changed

+307
-201
lines changed

package-lock.json

+184-121
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/datepicker/js/calendar.js

+82-48
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
* @module material.components.datepicker
88
*
99
* @param {Date} ng-model The component's model. Should be a Date object.
10+
* @param {Object=} ng-model-options Allows tuning of the way in which `ng-model` is being
11+
* updated. Also allows for a timezone to be specified.
12+
* <a href="https://docs.angularjs.org/api/ng/directive/ngModelOptions#usage">Read more at the
13+
* ngModelOptions docs.</a>
1014
* @param {Date=} md-min-date Expression representing the minimum date.
1115
* @param {Date=} md-max-date Expression representing the maximum date.
1216
* @param {(function(Date): boolean)=} md-date-filter Function expecting a date and returning a
@@ -41,24 +45,16 @@
4145
// TODO(jelbourn): Previous month opacity is lowered when partially scrolled out of view.
4246
// TODO(jelbourn): Support md-calendar standalone on a page (as a tabstop w/ aria-live
4347
// announcement and key handling).
44-
// Read-only calendar (not just date-picker).
48+
// TODO Read-only calendar (not just date-picker).
4549

46-
function calendarDirective() {
50+
function calendarDirective(inputDirective) {
4751
return {
4852
template: function(tElement, tAttr) {
49-
// TODO(crisbeto): This is a workaround that allows the calendar to work, without
50-
// a datepicker, until issue #8585 gets resolved. It can safely be removed
51-
// afterwards. This ensures that the virtual repeater scrolls to the proper place on load by
52-
// deferring the execution until the next digest. It's necessary only if the calendar is used
53-
// without a datepicker, otherwise it's already wrapped in an ngIf.
54-
var extraAttrs = tAttr.hasOwnProperty('ngIf') ? '' : 'ng-if="calendarCtrl.isInitialized"';
55-
var template = '' +
56-
'<div ng-switch="calendarCtrl.currentView" ' + extraAttrs + '>' +
53+
return '' +
54+
'<div ng-switch="calendarCtrl.currentView">' +
5755
'<md-calendar-year ng-switch-when="year"></md-calendar-year>' +
5856
'<md-calendar-month ng-switch-default></md-calendar-month>' +
5957
'</div>';
60-
61-
return template;
6258
},
6359
scope: {
6460
minDate: '=mdMinDate',
@@ -77,7 +73,7 @@
7773
link: function(scope, element, attrs, controllers) {
7874
var ngModelCtrl = controllers[0];
7975
var mdCalendarCtrl = controllers[1];
80-
mdCalendarCtrl.configureNgModel(ngModelCtrl);
76+
mdCalendarCtrl.configureNgModel(ngModelCtrl, inputDirective);
8177
}
8278
};
8379
}
@@ -105,16 +101,28 @@
105101
* @ngInject @constructor
106102
*/
107103
function CalendarCtrl($element, $scope, $$mdDateUtil, $mdUtil,
108-
$mdConstant, $mdTheming, $$rAF, $attrs, $mdDateLocale) {
104+
$mdConstant, $mdTheming, $$rAF, $attrs, $mdDateLocale, $filter) {
109105

110106
$mdTheming($element);
111107

112-
/** @final {!angular.JQLite} */
108+
/**
109+
* @final
110+
* @type {!JQLite}
111+
*/
113112
this.$element = $element;
114113

115-
/** @final {!angular.Scope} */
114+
/**
115+
* @final
116+
* @type {!angular.Scope}
117+
*/
116118
this.$scope = $scope;
117119

120+
/**
121+
* @final
122+
* @type {!angular.$attrs} Current attributes object for the element
123+
*/
124+
this.$attrs = $attrs;
125+
118126
/** @final */
119127
this.dateUtil = $$mdDateUtil;
120128

@@ -130,19 +138,25 @@
130138
/** @final */
131139
this.$mdDateLocale = $mdDateLocale;
132140

133-
/** @final {Date} */
141+
/** @final The built-in Angular date filter. */
142+
this.ngDateFilter = $filter('date');
143+
144+
/**
145+
* @final
146+
* @type {Date}
147+
*/
134148
this.today = this.dateUtil.createDateAtMidnight();
135149

136-
/** @type {!angular.NgModelController} */
150+
/** @type {!ngModel.NgModelController} */
137151
this.ngModelCtrl = null;
138152

139-
/** @type {String} Class applied to the selected date cell. */
153+
/** @type {string} Class applied to the selected date cell. */
140154
this.SELECTED_DATE_CLASS = 'md-calendar-selected-date';
141155

142-
/** @type {String} Class applied to the cell for today. */
156+
/** @type {string} Class applied to the cell for today. */
143157
this.TODAY_CLASS = 'md-calendar-date-today';
144158

145-
/** @type {String} Class applied to the focused cell. */
159+
/** @type {string} Class applied to the focused cell. */
146160
this.FOCUSED_DATE_CLASS = 'md-focus';
147161

148162
/** @final {number} Unique ID for this calendar instance. */
@@ -157,6 +171,12 @@
157171
*/
158172
this.displayDate = null;
159173

174+
/**
175+
* Allows restricting the calendar to only allow selecting a month or a day.
176+
* @type {'month'|'day'|null}
177+
*/
178+
this.mode = null;
179+
160180
/**
161181
* The selected date. Keep track of this separately from the ng-model value so that we
162182
* can know, when the ng-model value changes, what the previous value was before it's updated
@@ -180,12 +200,6 @@
180200
*/
181201
this.lastRenderableDate = null;
182202

183-
/**
184-
* Used to toggle initialize the root element in the next digest.
185-
* @type {Boolean}
186-
*/
187-
this.isInitialized = false;
188-
189203
/**
190204
* Cache for the width of the element without a scrollbar. Used to hide the scrollbar later on
191205
* and to avoid extra reflows when switching between views.
@@ -233,12 +247,12 @@
233247
if (angular.version.major === 1 && angular.version.minor <= 4) {
234248
this.$onInit();
235249
}
236-
237250
}
238251

239252
/**
240253
* AngularJS Lifecycle hook for newer AngularJS versions.
241-
* Bindings are not guaranteed to have been assigned in the controller, but they are in the $onInit hook.
254+
* Bindings are not guaranteed to have been assigned in the controller, but they are in the
255+
* $onInit hook.
242256
*/
243257
CalendarCtrl.prototype.$onInit = function() {
244258
/**
@@ -255,36 +269,55 @@
255269
this.mode = null;
256270
}
257271

258-
var dateLocale = this.$mdDateLocale;
259-
260-
if (this.minDate && this.minDate > dateLocale.firstRenderableDate) {
272+
if (this.minDate && this.minDate > this.$mdDateLocale.firstRenderableDate) {
261273
this.firstRenderableDate = this.minDate;
262274
} else {
263-
this.firstRenderableDate = dateLocale.firstRenderableDate;
275+
this.firstRenderableDate = this.$mdDateLocale.firstRenderableDate;
264276
}
265277

266-
if (this.maxDate && this.maxDate < dateLocale.lastRenderableDate) {
278+
if (this.maxDate && this.maxDate < this.$mdDateLocale.lastRenderableDate) {
267279
this.lastRenderableDate = this.maxDate;
268280
} else {
269-
this.lastRenderableDate = dateLocale.lastRenderableDate;
281+
this.lastRenderableDate = this.$mdDateLocale.lastRenderableDate;
270282
}
271283
};
272284

273285
/**
274286
* Sets up the controller's reference to ngModelController.
275-
* @param {!angular.NgModelController} ngModelCtrl
287+
* @param {!ngModel.NgModelController} ngModelCtrl Instance of the ngModel controller.
288+
* @param {Object} inputDirective Config for Angular's `input` directive.
276289
*/
277-
CalendarCtrl.prototype.configureNgModel = function(ngModelCtrl) {
290+
CalendarCtrl.prototype.configureNgModel = function(ngModelCtrl, inputDirective) {
278291
var self = this;
279-
280292
self.ngModelCtrl = ngModelCtrl;
281293

282-
self.$mdUtil.nextTick(function() {
283-
self.isInitialized = true;
284-
});
294+
// The component needs to be [type="date"] in order to be picked up by AngularJS.
295+
this.$attrs.$set('type', 'date');
296+
297+
// Invoke the `input` directive link function, adding a stub for the element.
298+
// This allows us to re-use AngularJS' logic for setting the timezone via ng-model-options.
299+
// It works by calling the link function directly which then adds the proper `$parsers` and
300+
// `$formatters` to the NgModelController.
301+
inputDirective[0].link.pre(this.$scope, {
302+
on: angular.noop,
303+
val: angular.noop,
304+
0: {}
305+
}, this.$attrs, [ngModelCtrl]);
285306

286307
ngModelCtrl.$render = function() {
287308
var value = this.$viewValue;
309+
var parsedValue, convertedValue;
310+
311+
// In the case where a conversion is needed, the $viewValue here will be a string like
312+
// "2020-05-10" instead of a Date object.
313+
if (!self.dateUtil.isValidDate(value)) {
314+
parsedValue = self.$mdDateLocale.parseDate(this.$viewValue);
315+
convertedValue =
316+
new Date(parsedValue.getTime() + 60000 * parsedValue.getTimezoneOffset());
317+
if (self.dateUtil.isValidDate(convertedValue)) {
318+
value = convertedValue;
319+
}
320+
}
288321

289322
// Notify the child scopes of any changes.
290323
self.$scope.$broadcast('md-calendar-parent-changed', value);
@@ -303,13 +336,14 @@
303336

304337
/**
305338
* Sets the ng-model value for the calendar and emits a change event.
306-
* @param {Date} date
339+
* @param {Date} date new value for the calendar
307340
*/
308341
CalendarCtrl.prototype.setNgModelValue = function(date) {
342+
var timezone = this.$mdUtil.getModelOption(this.ngModelCtrl, 'timezone');
309343
var value = this.dateUtil.createDateAtMidnight(date);
310-
this.focus(value);
344+
this.focusDate(value);
311345
this.$scope.$emit('md-calendar-change', value);
312-
this.ngModelCtrl.$setViewValue(value);
346+
this.ngModelCtrl.$setViewValue(this.ngDateFilter(value, 'yyyy-MM-dd', timezone), 'default');
313347
this.ngModelCtrl.$render();
314348
return value;
315349
};
@@ -333,9 +367,9 @@
333367

334368
/**
335369
* Focus the cell corresponding to the given date.
336-
* @param {Date} date The date to be focused.
370+
* @param {Date=} date The date to be focused.
337371
*/
338-
CalendarCtrl.prototype.focus = function(date) {
372+
CalendarCtrl.prototype.focusDate = function(date) {
339373
if (this.dateUtil.isValidDate(date)) {
340374
var previousFocus = this.$element[0].querySelector('.' + this.FOCUSED_DATE_CLASS);
341375
if (previousFocus) {
@@ -424,10 +458,10 @@
424458
this.$scope.$apply(function() {
425459
// Capture escape and emit back up so that a wrapping component
426460
// (such as a date-picker) can decide to close.
427-
if (event.which == self.keyCode.ESCAPE || event.which == self.keyCode.TAB) {
461+
if (event.which === self.keyCode.ESCAPE || event.which === self.keyCode.TAB) {
428462
self.$scope.$emit('md-calendar-close');
429463

430-
if (event.which == self.keyCode.TAB) {
464+
if (event.which === self.keyCode.TAB) {
431465
event.preventDefault();
432466
}
433467

src/components/datepicker/js/calendarMonth.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@
9898
this.cellClickHandler = function() {
9999
var timestamp = $$mdDateUtil.getTimestampFromNode(this);
100100
self.$scope.$apply(function() {
101-
self.calendarCtrl.setNgModelValue(timestamp);
101+
self.calendarCtrl.setNgModelValue(self.dateLocale.parseDate(timestamp));
102102
});
103103
};
104104

@@ -145,7 +145,7 @@
145145

146146
/**
147147
* Gets the "index" of the currently selected date as it would be in the virtual-repeat.
148-
* @returns {number}
148+
* @returns {number} the "index" of the currently selected date
149149
*/
150150
CalendarMonthCtrl.prototype.getSelectedMonthIndex = function() {
151151
var calendarCtrl = this.calendarCtrl;
@@ -268,7 +268,7 @@
268268
date = this.dateUtil.clampDate(date, calendarCtrl.minDate, calendarCtrl.maxDate);
269269

270270
this.changeDisplayDate(date).then(function() {
271-
calendarCtrl.focus(date);
271+
calendarCtrl.focusDate(date);
272272
});
273273
}
274274
}

src/components/datepicker/js/calendarMonthBody.js

-1
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,6 @@
8686

8787
if (this.focusAfterAppend) {
8888
this.focusAfterAppend.classList.add(this.calendarCtrl.FOCUSED_DATE_CLASS);
89-
this.focusAfterAppend.focus();
9089
this.focusAfterAppend = null;
9190
}
9291
};

src/components/datepicker/js/calendarYear.js

+3-4
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,7 @@
4747
* Controller for the mdCalendar component.
4848
* @ngInject @constructor
4949
*/
50-
function CalendarYearCtrl($element, $scope, $animate, $q,
51-
$$mdDateUtil, $mdUtil) {
50+
function CalendarYearCtrl($element, $scope, $animate, $q, $$mdDateUtil, $mdUtil) {
5251

5352
/** @final {!angular.JQLite} */
5453
this.$element = $element;
@@ -197,7 +196,7 @@
197196
date = dateUtil.getFirstDateOfMonth(self.dateUtil.clampDate(date, min, max));
198197

199198
self.changeDate(date).then(function() {
200-
calendarCtrl.focus(date);
199+
calendarCtrl.focusDate(date);
201200
});
202201
}
203202
}
@@ -228,7 +227,7 @@
228227

229228
if (calendarCtrl.mode) {
230229
this.$mdUtil.nextTick(function() {
231-
calendarCtrl.setNgModelValue(timestamp);
230+
calendarCtrl.setNgModelValue(calendarCtrl.$mdDateLocale.parseDate(timestamp));
232231
});
233232
} else {
234233
calendarCtrl.setCurrentView('month', timestamp);

src/components/datepicker/js/calendarYearBody.js

+1-2
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,6 @@
7676

7777
if (this.focusAfterAppend) {
7878
this.focusAfterAppend.classList.add(this.calendarCtrl.FOCUSED_DATE_CLASS);
79-
this.focusAfterAppend.focus();
8079
this.focusAfterAppend = null;
8180
}
8281
};
@@ -163,7 +162,7 @@
163162
var firstRow = document.createElement('tr');
164163
var labelCell = document.createElement('td');
165164
labelCell.className = 'md-calendar-month-label';
166-
labelCell.textContent = year;
165+
labelCell.textContent = String(year);
167166
firstRow.appendChild(labelCell);
168167

169168
for (i = 0; i < 6; i++) {

src/components/datepicker/js/dateLocaleProvider.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@
183183
* Factory function that returns an instance of the dateLocale service.
184184
* @ngInject
185185
* @param $locale
186+
* @param $filter
186187
* @returns {DateLocale}
187188
*/
188189
DateLocaleProvider.prototype.$get = function($locale, $filter) {
@@ -214,7 +215,7 @@
214215

215216
/**
216217
* Default string-to-date parsing function.
217-
* @param {string} dateString
218+
* @param {string|number} dateString
218219
* @returns {!Date}
219220
*/
220221
function defaultParseDate(dateString) {

src/components/datepicker/js/dateUtil.js

+9-9
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@
7979
}
8080

8181
/**
82-
* Gets whether two dates are the same day (not not necesarily the same time).
82+
* Gets whether two dates are the same day (not not necessarily the same time).
8383
* @param {Date} d1
8484
* @param {Date} d2
8585
* @returns {boolean}
@@ -207,19 +207,19 @@
207207

208208
/**
209209
* Creates a date with the time set to midnight.
210-
* Drop-in replacement for two forms of the Date constructor:
211-
* 1. No argument for Date representing now.
212-
* 2. Single-argument value representing number of seconds since Unix Epoch
213-
* or a Date object.
214-
* @param {number|Date=} opt_value
210+
* Drop-in replacement for two forms of the Date constructor via opt_value.
211+
* @param {number|Date=} opt_value Leave undefined for a Date representing now. Or use a
212+
* single value representing the number of seconds since the Unix Epoch or a Date object.
215213
* @return {Date} New date with time set to midnight.
216214
*/
217215
function createDateAtMidnight(opt_value) {
218216
var date;
219-
if (angular.isUndefined(opt_value)) {
220-
date = new Date();
221-
} else {
217+
if (angular.isDate(opt_value)) {
218+
date = opt_value;
219+
} else if (angular.isNumber(opt_value)) {
222220
date = new Date(opt_value);
221+
} else {
222+
date = new Date();
223223
}
224224
setDateTimeToMidnight(date);
225225
return date;

0 commit comments

Comments
 (0)