|
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