|
7 | 7 | * @module material.components.datepicker |
8 | 8 | * |
9 | 9 | * @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> |
10 | 14 | * @param {Date=} md-min-date Expression representing the minimum date. |
11 | 15 | * @param {Date=} md-max-date Expression representing the maximum date. |
12 | 16 | * @param {(function(Date): boolean)=} md-date-filter Function expecting a date and returning a |
|
41 | 45 | // TODO(jelbourn): Previous month opacity is lowered when partially scrolled out of view. |
42 | 46 | // TODO(jelbourn): Support md-calendar standalone on a page (as a tabstop w/ aria-live |
43 | 47 | // announcement and key handling). |
44 | | - // Read-only calendar (not just date-picker). |
| 48 | + // TODO Read-only calendar (not just date-picker). |
45 | 49 |
|
46 | | - function calendarDirective() { |
| 50 | + function calendarDirective(inputDirective) { |
47 | 51 | return { |
48 | 52 | 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">' + |
57 | 55 | '<md-calendar-year ng-switch-when="year"></md-calendar-year>' + |
58 | 56 | '<md-calendar-month ng-switch-default></md-calendar-month>' + |
59 | 57 | '</div>'; |
60 | | - |
61 | | - return template; |
62 | 58 | }, |
63 | 59 | scope: { |
64 | 60 | minDate: '=mdMinDate', |
|
77 | 73 | link: function(scope, element, attrs, controllers) { |
78 | 74 | var ngModelCtrl = controllers[0]; |
79 | 75 | var mdCalendarCtrl = controllers[1]; |
80 | | - mdCalendarCtrl.configureNgModel(ngModelCtrl); |
| 76 | + mdCalendarCtrl.configureNgModel(ngModelCtrl, inputDirective); |
81 | 77 | } |
82 | 78 | }; |
83 | 79 | } |
|
105 | 101 | * @ngInject @constructor |
106 | 102 | */ |
107 | 103 | function CalendarCtrl($element, $scope, $$mdDateUtil, $mdUtil, |
108 | | - $mdConstant, $mdTheming, $$rAF, $attrs, $mdDateLocale) { |
| 104 | + $mdConstant, $mdTheming, $$rAF, $attrs, $mdDateLocale, $filter) { |
109 | 105 |
|
110 | 106 | $mdTheming($element); |
111 | 107 |
|
112 | | - /** @final {!angular.JQLite} */ |
| 108 | + /** |
| 109 | + * @final |
| 110 | + * @type {!JQLite} |
| 111 | + */ |
113 | 112 | this.$element = $element; |
114 | 113 |
|
115 | | - /** @final {!angular.Scope} */ |
| 114 | + /** |
| 115 | + * @final |
| 116 | + * @type {!angular.Scope} |
| 117 | + */ |
116 | 118 | this.$scope = $scope; |
117 | 119 |
|
| 120 | + /** |
| 121 | + * @final |
| 122 | + * @type {!angular.$attrs} Current attributes object for the element |
| 123 | + */ |
| 124 | + this.$attrs = $attrs; |
| 125 | + |
118 | 126 | /** @final */ |
119 | 127 | this.dateUtil = $$mdDateUtil; |
120 | 128 |
|
|
130 | 138 | /** @final */ |
131 | 139 | this.$mdDateLocale = $mdDateLocale; |
132 | 140 |
|
133 | | - /** @final {Date} */ |
| 141 | + /** @final The built-in Angular date filter. */ |
| 142 | + this.ngDateFilter = $filter('date'); |
| 143 | + |
| 144 | + /** |
| 145 | + * @final |
| 146 | + * @type {Date} |
| 147 | + */ |
134 | 148 | this.today = this.dateUtil.createDateAtMidnight(); |
135 | 149 |
|
136 | | - /** @type {!angular.NgModelController} */ |
| 150 | + /** @type {!ngModel.NgModelController} */ |
137 | 151 | this.ngModelCtrl = null; |
138 | 152 |
|
139 | | - /** @type {String} Class applied to the selected date cell. */ |
| 153 | + /** @type {string} Class applied to the selected date cell. */ |
140 | 154 | this.SELECTED_DATE_CLASS = 'md-calendar-selected-date'; |
141 | 155 |
|
142 | | - /** @type {String} Class applied to the cell for today. */ |
| 156 | + /** @type {string} Class applied to the cell for today. */ |
143 | 157 | this.TODAY_CLASS = 'md-calendar-date-today'; |
144 | 158 |
|
145 | | - /** @type {String} Class applied to the focused cell. */ |
| 159 | + /** @type {string} Class applied to the focused cell. */ |
146 | 160 | this.FOCUSED_DATE_CLASS = 'md-focus'; |
147 | 161 |
|
148 | 162 | /** @final {number} Unique ID for this calendar instance. */ |
|
157 | 171 | */ |
158 | 172 | this.displayDate = null; |
159 | 173 |
|
| 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 | + |
160 | 180 | /** |
161 | 181 | * The selected date. Keep track of this separately from the ng-model value so that we |
162 | 182 | * can know, when the ng-model value changes, what the previous value was before it's updated |
|
180 | 200 | */ |
181 | 201 | this.lastRenderableDate = null; |
182 | 202 |
|
183 | | - /** |
184 | | - * Used to toggle initialize the root element in the next digest. |
185 | | - * @type {Boolean} |
186 | | - */ |
187 | | - this.isInitialized = false; |
188 | | - |
189 | 203 | /** |
190 | 204 | * Cache for the width of the element without a scrollbar. Used to hide the scrollbar later on |
191 | 205 | * and to avoid extra reflows when switching between views. |
|
233 | 247 | if (angular.version.major === 1 && angular.version.minor <= 4) { |
234 | 248 | this.$onInit(); |
235 | 249 | } |
236 | | - |
237 | 250 | } |
238 | 251 |
|
239 | 252 | /** |
240 | 253 | * 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. |
242 | 256 | */ |
243 | 257 | CalendarCtrl.prototype.$onInit = function() { |
244 | 258 | /** |
|
255 | 269 | this.mode = null; |
256 | 270 | } |
257 | 271 |
|
258 | | - var dateLocale = this.$mdDateLocale; |
259 | | - |
260 | | - if (this.minDate && this.minDate > dateLocale.firstRenderableDate) { |
| 272 | + if (this.minDate && this.minDate > this.$mdDateLocale.firstRenderableDate) { |
261 | 273 | this.firstRenderableDate = this.minDate; |
262 | 274 | } else { |
263 | | - this.firstRenderableDate = dateLocale.firstRenderableDate; |
| 275 | + this.firstRenderableDate = this.$mdDateLocale.firstRenderableDate; |
264 | 276 | } |
265 | 277 |
|
266 | | - if (this.maxDate && this.maxDate < dateLocale.lastRenderableDate) { |
| 278 | + if (this.maxDate && this.maxDate < this.$mdDateLocale.lastRenderableDate) { |
267 | 279 | this.lastRenderableDate = this.maxDate; |
268 | 280 | } else { |
269 | | - this.lastRenderableDate = dateLocale.lastRenderableDate; |
| 281 | + this.lastRenderableDate = this.$mdDateLocale.lastRenderableDate; |
270 | 282 | } |
271 | 283 | }; |
272 | 284 |
|
273 | 285 | /** |
274 | 286 | * 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. |
276 | 289 | */ |
277 | | - CalendarCtrl.prototype.configureNgModel = function(ngModelCtrl) { |
| 290 | + CalendarCtrl.prototype.configureNgModel = function(ngModelCtrl, inputDirective) { |
278 | 291 | var self = this; |
279 | | - |
280 | 292 | self.ngModelCtrl = ngModelCtrl; |
281 | 293 |
|
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]); |
285 | 306 |
|
286 | 307 | ngModelCtrl.$render = function() { |
287 | 308 | 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 | + } |
288 | 321 |
|
289 | 322 | // Notify the child scopes of any changes. |
290 | 323 | self.$scope.$broadcast('md-calendar-parent-changed', value); |
|
303 | 336 |
|
304 | 337 | /** |
305 | 338 | * 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 |
307 | 340 | */ |
308 | 341 | CalendarCtrl.prototype.setNgModelValue = function(date) { |
| 342 | + var timezone = this.$mdUtil.getModelOption(this.ngModelCtrl, 'timezone'); |
309 | 343 | var value = this.dateUtil.createDateAtMidnight(date); |
310 | | - this.focus(value); |
| 344 | + this.focusDate(value); |
311 | 345 | this.$scope.$emit('md-calendar-change', value); |
312 | | - this.ngModelCtrl.$setViewValue(value); |
| 346 | + this.ngModelCtrl.$setViewValue(this.ngDateFilter(value, 'yyyy-MM-dd', timezone), 'default'); |
313 | 347 | this.ngModelCtrl.$render(); |
314 | 348 | return value; |
315 | 349 | }; |
|
333 | 367 |
|
334 | 368 | /** |
335 | 369 | * 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. |
337 | 371 | */ |
338 | | - CalendarCtrl.prototype.focus = function(date) { |
| 372 | + CalendarCtrl.prototype.focusDate = function(date) { |
339 | 373 | if (this.dateUtil.isValidDate(date)) { |
340 | 374 | var previousFocus = this.$element[0].querySelector('.' + this.FOCUSED_DATE_CLASS); |
341 | 375 | if (previousFocus) { |
|
424 | 458 | this.$scope.$apply(function() { |
425 | 459 | // Capture escape and emit back up so that a wrapping component |
426 | 460 | // (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) { |
428 | 462 | self.$scope.$emit('md-calendar-close'); |
429 | 463 |
|
430 | | - if (event.which == self.keyCode.TAB) { |
| 464 | + if (event.which === self.keyCode.TAB) { |
431 | 465 | event.preventDefault(); |
432 | 466 | } |
433 | 467 |
|
|
0 commit comments