source: trip-planner-front/node_modules/angular-material/modules/js/datepicker/datepicker.js@ 6a3a178

Last change on this file since 6a3a178 was 6a3a178, checked in by Ema <ema_spirova@…>, 3 years ago

initial commit

  • Property mode set to 100644
File size: 121.7 KB
Line 
1/*!
2 * AngularJS Material Design
3 * https://github.com/angular/material
4 * @license MIT
5 * v1.2.3
6 */
7(function( window, angular, undefined ){
8"use strict";
9
10/**
11 * @ngdoc module
12 * @name material.components.datepicker
13 * @description Module for the datepicker component.
14 */
15
16angular.module('material.components.datepicker', [
17 'material.core',
18 'material.components.icon',
19 'material.components.virtualRepeat'
20]);
21
22(function() {
23 'use strict';
24
25 /**
26 * @ngdoc directive
27 * @name mdCalendar
28 * @module material.components.datepicker
29 *
30 * @param {Date} ng-model The component's model. Should be a Date object.
31 * @param {Object=} ng-model-options Allows tuning of the way in which `ng-model` is being
32 * updated. Also allows for a timezone to be specified.
33 * <a href="https://docs.angularjs.org/api/ng/directive/ngModelOptions#usage">Read more at the
34 * ngModelOptions docs.</a>
35 * @param {Date=} md-min-date Expression representing the minimum date.
36 * @param {Date=} md-max-date Expression representing the maximum date.
37 * @param {(function(Date): boolean)=} md-date-filter Function expecting a date and returning a
38 * boolean whether it can be selected in "day" mode or not.
39 * @param {(function(Date): boolean)=} md-month-filter Function expecting a date and returning a
40 * boolean whether it can be selected in "month" mode or not.
41 * @param {String=} md-current-view Current view of the calendar. Can be either "month" or "year".
42 * @param {String=} md-mode Restricts the user to only selecting a value from a particular view.
43 * This option can be used if the user is only supposed to choose from a certain date type
44 * (e.g. only selecting the month). Can be either "month" or "day". **Note** that this will
45 * overwrite the `md-current-view` value.
46 *
47 * @description
48 * `<md-calendar>` is a component that renders a calendar that can be used to select a date.
49 * It is a part of the `<md-datepicker>` pane, however it can also be used on it's own.
50 *
51 * @usage
52 *
53 * <hljs lang="html">
54 * <md-calendar ng-model="birthday"></md-calendar>
55 * </hljs>
56 */
57 CalendarCtrl['$inject'] = ["$element", "$scope", "$$mdDateUtil", "$mdUtil", "$mdConstant", "$mdTheming", "$$rAF", "$attrs", "$mdDateLocale", "$filter", "$document"];
58 calendarDirective['$inject'] = ["inputDirective"];
59 angular.module('material.components.datepicker')
60 .directive('mdCalendar', calendarDirective);
61
62 // TODO(jelbourn): Mac Cmd + left / right == Home / End
63 // TODO(jelbourn): Refactor month element creation to use cloneNode (performance).
64 // TODO(jelbourn): Define virtual scrolling constants (compactness) users can override.
65 // TODO(jelbourn): Animated month transition on ng-model change (virtual-repeat)
66 // TODO(jelbourn): Scroll snapping (virtual repeat)
67 // TODO(jelbourn): Remove superfluous row from short months (virtual-repeat)
68 // TODO(jelbourn): Month headers stick to top when scrolling.
69 // TODO(jelbourn): Previous month opacity is lowered when partially scrolled out of view.
70 // TODO(jelbourn): Support md-calendar standalone on a page (as a tabstop w/ aria-live
71 // announcement and key handling).
72 // TODO Read-only calendar (not just date-picker).
73
74 function calendarDirective(inputDirective) {
75 return {
76 template: function(tElement, tAttr) {
77 // This allows the calendar to work, without a datepicker. This ensures that the virtual
78 // repeater scrolls to the proper place on load by deferring the execution until the next
79 // digest. It's necessary only if the calendar is used without a datepicker, otherwise it's
80 // already wrapped in an ngIf.
81 var extraAttrs = tAttr.hasOwnProperty('ngIf') ? '' : 'ng-if="calendarCtrl.isInitialized"';
82 return '' +
83 '<div ng-switch="calendarCtrl.currentView" ' + extraAttrs + '>' +
84 '<md-calendar-year ng-switch-when="year"></md-calendar-year>' +
85 '<md-calendar-month ng-switch-default></md-calendar-month>' +
86 '</div>';
87 },
88 scope: {
89 minDate: '=mdMinDate',
90 maxDate: '=mdMaxDate',
91 dateFilter: '=mdDateFilter',
92 monthFilter: '=mdMonthFilter',
93
94 // These need to be prefixed, because Angular resets
95 // any changes to the value due to bindToController.
96 _mode: '@mdMode',
97 _currentView: '@mdCurrentView'
98 },
99 require: ['ngModel', 'mdCalendar'],
100 controller: CalendarCtrl,
101 controllerAs: 'calendarCtrl',
102 bindToController: true,
103 link: function(scope, element, attrs, controllers) {
104 var ngModelCtrl = controllers[0];
105 var mdCalendarCtrl = controllers[1];
106 mdCalendarCtrl.configureNgModel(ngModelCtrl, inputDirective);
107 }
108 };
109 }
110
111 /**
112 * Occasionally the hideVerticalScrollbar method might read an element's
113 * width as 0, because it hasn't been laid out yet. This value will be used
114 * as a fallback, in order to prevent scenarios where the element's width
115 * would otherwise have been set to 0. This value is the "usual" width of a
116 * calendar within a floating calendar pane.
117 */
118 var FALLBACK_WIDTH = 340;
119
120 /** Next identifier for calendar instance. */
121 var nextUniqueId = 0;
122
123 /** Maps the `md-mode` values to their corresponding calendar views. */
124 var MODE_MAP = {
125 day: 'month',
126 month: 'year'
127 };
128
129 /**
130 * Controller for the mdCalendar component.
131 * ngInject @constructor
132 */
133 function CalendarCtrl($element, $scope, $$mdDateUtil, $mdUtil, $mdConstant, $mdTheming, $$rAF,
134 $attrs, $mdDateLocale, $filter, $document) {
135 $mdTheming($element);
136
137 /**
138 * @final
139 * @type {!JQLite}
140 */
141 this.$element = $element;
142
143 /**
144 * @final
145 * @type {!angular.Scope}
146 */
147 this.$scope = $scope;
148
149 /**
150 * @final
151 * @type {!angular.$attrs} Current attributes object for the element
152 */
153 this.$attrs = $attrs;
154
155 /** @final */
156 this.dateUtil = $$mdDateUtil;
157
158 /** @final */
159 this.$mdUtil = $mdUtil;
160
161 /** @final */
162 this.keyCode = $mdConstant.KEY_CODE;
163
164 /** @final */
165 this.$$rAF = $$rAF;
166
167 /** @final */
168 this.$mdDateLocale = $mdDateLocale;
169
170 /** @final The built-in Angular date filter. */
171 this.ngDateFilter = $filter('date');
172
173 /**
174 * @final
175 * @type {Date}
176 */
177 this.today = this.dateUtil.createDateAtMidnight();
178
179 /** @type {!ngModel.NgModelController} */
180 this.ngModelCtrl = undefined;
181
182 /** @type {string} Class applied to the selected date cell. */
183 this.SELECTED_DATE_CLASS = 'md-calendar-selected-date';
184
185 /** @type {string} Class applied to the cell for today. */
186 this.TODAY_CLASS = 'md-calendar-date-today';
187
188 /** @type {string} Class applied to the focused cell. */
189 this.FOCUSED_DATE_CLASS = 'md-focus';
190
191 /**
192 * @final
193 * @type {number} Unique ID for this calendar instance.
194 */
195 this.id = nextUniqueId++;
196
197 /**
198 * The date that is currently focused or showing in the calendar. This will initially be set
199 * to the ng-model value if set, otherwise to today. It will be updated as the user navigates
200 * to other months. The cell corresponding to the displayDate does not necessarily always have
201 * focus in the document (such as for cases when the user is scrolling the calendar).
202 * @type {Date}
203 */
204 this.displayDate = null;
205
206 /**
207 * Allows restricting the calendar to only allow selecting a month or a day.
208 * @type {'month'|'day'|null}
209 */
210 this.mode = null;
211
212 /**
213 * The selected date. Keep track of this separately from the ng-model value so that we
214 * can know, when the ng-model value changes, what the previous value was before it's updated
215 * in the component's UI.
216 *
217 * @type {Date}
218 */
219 this.selectedDate = null;
220
221 /**
222 * The first date that can be rendered by the calendar. The default is taken
223 * from the mdDateLocale provider and is limited by the mdMinDate.
224 * @type {Date}
225 */
226 this.firstRenderableDate = null;
227
228 /**
229 * The last date that can be rendered by the calendar. The default comes
230 * from the mdDateLocale provider and is limited by the maxDate.
231 * @type {Date}
232 */
233 this.lastRenderableDate = null;
234
235 /**
236 * Used to toggle initialize the root element in the next digest.
237 * @type {boolean}
238 */
239 this.isInitialized = false;
240
241 /**
242 * Cache for the width of the element without a scrollbar. Used to hide the scrollbar later on
243 * and to avoid extra reflows when switching between views.
244 * @type {number}
245 */
246 this.width = 0;
247
248 /**
249 * Caches the width of the scrollbar in order to be used when hiding it and to avoid extra reflows.
250 * @type {number}
251 */
252 this.scrollbarWidth = 0;
253
254 /**
255 * @type {boolean} set to true if the calendar is being used "standalone" (outside of a
256 * md-datepicker).
257 */
258 this.standaloneMode = false;
259
260 // Unless the user specifies so, the calendar should not be a tab stop.
261 // This is necessary because ngAria might add a tabindex to anything with an ng-model
262 // (based on whether or not the user has turned that particular feature on/off).
263 if (!$attrs.tabindex) {
264 $element.attr('tabindex', '-1');
265 }
266
267 var boundKeyHandler = angular.bind(this, this.handleKeyEvent);
268
269 // If use the md-calendar directly in the body without datepicker,
270 // handleKeyEvent will disable other inputs on the page.
271 // So only apply the handleKeyEvent on the body when the md-calendar inside datepicker,
272 // otherwise apply on the calendar element only.
273
274 var handleKeyElement;
275 if ($element.parent().hasClass('md-datepicker-calendar')) {
276 handleKeyElement = angular.element($document[0].body);
277 } else {
278 this.standaloneMode = true;
279 handleKeyElement = $element;
280 }
281
282 // Bind the keydown handler to the body, in order to handle cases where the focused
283 // element gets removed from the DOM and stops propagating click events.
284 handleKeyElement.on('keydown', boundKeyHandler);
285
286 $scope.$on('$destroy', function() {
287 handleKeyElement.off('keydown', boundKeyHandler);
288 });
289
290 // For AngularJS 1.4 and older, where there are no lifecycle hooks but bindings are pre-assigned,
291 // manually call the $onInit hook.
292 if (angular.version.major === 1 && angular.version.minor <= 4) {
293 this.$onInit();
294 }
295 }
296
297 /**
298 * AngularJS Lifecycle hook for newer AngularJS versions.
299 * Bindings are not guaranteed to have been assigned in the controller, but they are in the
300 * $onInit hook.
301 */
302 CalendarCtrl.prototype.$onInit = function() {
303 /**
304 * The currently visible calendar view. Note the prefix on the scope value,
305 * which is necessary, because the datepicker seems to reset the real one value if the
306 * calendar is open, but the `currentView` on the datepicker's scope is empty.
307 * @type {String}
308 */
309 if (this._mode && MODE_MAP.hasOwnProperty(this._mode)) {
310 this.currentView = MODE_MAP[this._mode];
311 this.mode = this._mode;
312 } else {
313 this.currentView = this._currentView || 'month';
314 this.mode = null;
315 }
316
317 if (this.minDate && this.minDate > this.$mdDateLocale.firstRenderableDate) {
318 this.firstRenderableDate = this.minDate;
319 } else {
320 this.firstRenderableDate = this.$mdDateLocale.firstRenderableDate;
321 }
322
323 if (this.maxDate && this.maxDate < this.$mdDateLocale.lastRenderableDate) {
324 this.lastRenderableDate = this.maxDate;
325 } else {
326 this.lastRenderableDate = this.$mdDateLocale.lastRenderableDate;
327 }
328 };
329
330 /**
331 * Sets up the controller's reference to ngModelController.
332 * @param {!ngModel.NgModelController} ngModelCtrl Instance of the ngModel controller.
333 * @param {Object} inputDirective Config for AngularJS's `input` directive.
334 */
335 CalendarCtrl.prototype.configureNgModel = function(ngModelCtrl, inputDirective) {
336 var self = this;
337 self.ngModelCtrl = ngModelCtrl;
338
339 // The component needs to be [type="date"] in order to be picked up by AngularJS.
340 this.$attrs.$set('type', 'date');
341
342 // Invoke the `input` directive link function, adding a stub for the element.
343 // This allows us to re-use AngularJS' logic for setting the timezone via ng-model-options.
344 // It works by calling the link function directly which then adds the proper `$parsers` and
345 // `$formatters` to the NgModelController.
346 inputDirective[0].link.pre(this.$scope, {
347 on: angular.noop,
348 val: angular.noop,
349 0: {}
350 }, this.$attrs, [ngModelCtrl]);
351
352 ngModelCtrl.$render = function() {
353 var value = this.$viewValue, convertedDate;
354
355 // In the case where a conversion is needed, the $viewValue here will be a string like
356 // "2020-05-10" instead of a Date object.
357 if (!self.dateUtil.isValidDate(value)) {
358 convertedDate = self.dateUtil.removeLocalTzAndReparseDate(new Date(value));
359 if (self.dateUtil.isValidDate(convertedDate)) {
360 value = convertedDate;
361 }
362 }
363
364 // Notify the child scopes of any changes.
365 self.$scope.$broadcast('md-calendar-parent-changed', value);
366
367 // Set up the selectedDate if it hasn't been already.
368 if (!self.selectedDate) {
369 self.selectedDate = value;
370 }
371
372 // Also set up the displayDate.
373 if (!self.displayDate) {
374 self.displayDate = self.selectedDate || self.today;
375 }
376 };
377
378 self.$mdUtil.nextTick(function() {
379 self.isInitialized = true;
380 });
381 };
382
383 /**
384 * Sets the ng-model value for the calendar and emits a change event.
385 * @param {Date} date new value for the calendar
386 */
387 CalendarCtrl.prototype.setNgModelValue = function(date) {
388 var timezone = this.$mdUtil.getModelOption(this.ngModelCtrl, 'timezone');
389 var value = this.dateUtil.createDateAtMidnight(date);
390 this.focusDate(value);
391 this.$scope.$emit('md-calendar-change', value);
392 // Using the timezone when the offset is negative (GMT+X) causes the previous day to be
393 // selected here. This check avoids that.
394 if (timezone == null || value.getTimezoneOffset() < 0) {
395 this.ngModelCtrl.$setViewValue(this.ngDateFilter(value, 'yyyy-MM-dd'), 'default');
396 } else {
397 this.ngModelCtrl.$setViewValue(this.ngDateFilter(value, 'yyyy-MM-dd', timezone), 'default');
398 }
399 this.ngModelCtrl.$render();
400 return value;
401 };
402
403 /**
404 * Sets the current view that should be visible in the calendar
405 * @param {string} newView View name to be set.
406 * @param {number|Date} time Date object or a timestamp for the new display date.
407 */
408 CalendarCtrl.prototype.setCurrentView = function(newView, time) {
409 var self = this;
410
411 self.$mdUtil.nextTick(function() {
412 self.currentView = newView;
413
414 if (time) {
415 self.displayDate = angular.isDate(time) ? time : new Date(time);
416 }
417 });
418 };
419
420 /**
421 * Focus the cell corresponding to the given date.
422 * @param {Date=} date The date to be focused.
423 */
424 CalendarCtrl.prototype.focusDate = function(date) {
425 if (this.dateUtil.isValidDate(date)) {
426 var previousFocus = this.$element[0].querySelector('.' + this.FOCUSED_DATE_CLASS);
427 if (previousFocus) {
428 previousFocus.classList.remove(this.FOCUSED_DATE_CLASS);
429 }
430
431 var cellId = this.getDateId(date, this.currentView);
432 var cell = document.getElementById(cellId);
433 if (cell) {
434 cell.classList.add(this.FOCUSED_DATE_CLASS);
435 cell.focus();
436 this.displayDate = date;
437 }
438 } else {
439 var rootElement = this.$element[0].querySelector('[ng-switch]');
440
441 if (rootElement) {
442 rootElement.focus();
443 }
444 }
445 };
446
447 /**
448 * Highlights a date cell on the calendar and changes the selected date.
449 * @param {Date=} date Date to be marked as selected.
450 */
451 CalendarCtrl.prototype.changeSelectedDate = function(date) {
452 var selectedDateClass = this.SELECTED_DATE_CLASS;
453 var prevDateCell = this.$element[0].querySelector('.' + selectedDateClass);
454
455 // Remove the selected class from the previously selected date, if any.
456 if (prevDateCell) {
457 prevDateCell.classList.remove(selectedDateClass);
458 prevDateCell.setAttribute('aria-selected', 'false');
459 }
460
461 // Apply the select class to the new selected date if it is set.
462 if (date) {
463 var dateCell = document.getElementById(this.getDateId(date, this.currentView));
464 if (dateCell) {
465 dateCell.classList.add(selectedDateClass);
466 dateCell.setAttribute('aria-selected', 'true');
467 }
468 }
469
470 this.selectedDate = date;
471 };
472
473 /**
474 * Normalizes the key event into an action name. The action will be broadcast
475 * to the child controllers.
476 * @param {KeyboardEvent} event
477 * @returns {string} The action that should be taken, or null if the key
478 * does not match a calendar shortcut.
479 */
480 CalendarCtrl.prototype.getActionFromKeyEvent = function(event) {
481 var keyCode = this.keyCode;
482
483 switch (event.which) {
484 case keyCode.ENTER: return 'select';
485
486 case keyCode.RIGHT_ARROW: return 'move-right';
487 case keyCode.LEFT_ARROW: return 'move-left';
488
489 case keyCode.DOWN_ARROW: return event.metaKey ? 'move-page-down' : 'move-row-down';
490 case keyCode.UP_ARROW: return event.metaKey ? 'move-page-up' : 'move-row-up';
491
492 case keyCode.PAGE_DOWN: return 'move-page-down';
493 case keyCode.PAGE_UP: return 'move-page-up';
494
495 case keyCode.HOME: return 'start';
496 case keyCode.END: return 'end';
497
498 default: return null;
499 }
500 };
501
502 /**
503 * Handles a key event in the calendar with the appropriate action.
504 * The action will either
505 * - select the focused date
506 * - navigate to focus a new date
507 * - emit a md-calendar-close event if in a md-datepicker panel
508 * - emit a md-calendar-parent-action
509 * - delegate to normal tab order if the TAB key is pressed in standalone mode
510 * @param {KeyboardEvent} event
511 */
512 CalendarCtrl.prototype.handleKeyEvent = function(event) {
513 var self = this;
514
515 this.$scope.$apply(function() {
516 // Capture escape and emit back up so that a wrapping component
517 // (such as a date-picker) can decide to close.
518 if (event.which === self.keyCode.ESCAPE ||
519 (event.which === self.keyCode.TAB && !self.standaloneMode)) {
520 self.$scope.$emit('md-calendar-close');
521
522 if (event.which === self.keyCode.TAB) {
523 event.preventDefault();
524 }
525
526 return;
527 } else if (event.which === self.keyCode.TAB && self.standaloneMode) {
528 // delegate to the normal tab order if the TAB key is pressed in standalone mode
529 return;
530 }
531
532 // Broadcast the action that any child controllers should take.
533 var action = self.getActionFromKeyEvent(event);
534 if (action) {
535 event.preventDefault();
536 event.stopPropagation();
537 self.$scope.$broadcast('md-calendar-parent-action', action);
538 }
539 });
540 };
541
542 /**
543 * Hides the vertical scrollbar on the calendar scroller of a child controller by
544 * setting the width on the calendar scroller and the `overflow: hidden` wrapper
545 * around the scroller, and then setting a padding-right on the scroller equal
546 * to the width of the browser's scrollbar.
547 *
548 * This will cause a reflow.
549 *
550 * @param {object} childCtrl The child controller whose scrollbar should be hidden.
551 */
552 CalendarCtrl.prototype.hideVerticalScrollbar = function(childCtrl) {
553 var self = this;
554 var element = childCtrl.$element[0];
555 var scrollMask = element.querySelector('.md-calendar-scroll-mask');
556
557 if (self.width > 0) {
558 setWidth();
559 } else {
560 self.$$rAF(function() {
561 var scroller = childCtrl.calendarScroller;
562
563 self.scrollbarWidth = scroller.offsetWidth - scroller.clientWidth;
564 self.width = element.querySelector('table').offsetWidth;
565 setWidth();
566 });
567 }
568
569 function setWidth() {
570 var width = self.width || FALLBACK_WIDTH;
571 var scrollbarWidth = self.scrollbarWidth;
572 var scroller = childCtrl.calendarScroller;
573
574 scrollMask.style.width = width + 'px';
575 scroller.style.width = (width + scrollbarWidth) + 'px';
576 scroller.style.paddingRight = scrollbarWidth + 'px';
577 }
578 };
579
580 /**
581 * Gets an identifier for a date unique to the calendar instance for internal
582 * purposes. Not to be displayed.
583 * @param {Date} date The date for which the id is being generated
584 * @param {string} namespace Namespace for the id. (month, year etc.)
585 * @returns {string}
586 */
587 CalendarCtrl.prototype.getDateId = function(date, namespace) {
588 if (!namespace) {
589 throw new Error('A namespace for the date id has to be specified.');
590 }
591
592 return [
593 'md',
594 this.id,
595 namespace,
596 date.getFullYear(),
597 date.getMonth(),
598 date.getDate()
599 ].join('-');
600 };
601
602 /**
603 * Util to trigger an extra digest on a parent scope, in order to to ensure that
604 * any child virtual repeaters have updated. This is necessary, because the virtual
605 * repeater doesn't update the $index the first time around since the content isn't
606 * in place yet. The case, in which this is an issue, is when the repeater has less
607 * than a page of content (e.g. a month or year view has a min or max date).
608 */
609 CalendarCtrl.prototype.updateVirtualRepeat = function() {
610 var scope = this.$scope;
611 var virtualRepeatResizeListener = scope.$on('$md-resize-enable', function() {
612 if (!scope.$$phase) {
613 scope.$apply();
614 }
615
616 virtualRepeatResizeListener();
617 });
618 };
619})();
620
621(function() {
622 'use strict';
623
624 CalendarMonthCtrl['$inject'] = ["$element", "$scope", "$animate", "$q", "$$mdDateUtil", "$mdDateLocale"];
625 angular.module('material.components.datepicker')
626 .directive('mdCalendarMonth', calendarDirective);
627
628 /**
629 * Height of one calendar month tbody. This must be made known to the virtual-repeat and is
630 * subsequently used for scrolling to specific months.
631 */
632 var TBODY_HEIGHT = 265;
633
634 /**
635 * Height of a calendar month with a single row. This is needed to calculate the offset for
636 * rendering an extra month in virtual-repeat that only contains one row.
637 */
638 var TBODY_SINGLE_ROW_HEIGHT = 45;
639
640 /** Private directive that represents a list of months inside the calendar. */
641 function calendarDirective() {
642 return {
643 template:
644 '<table aria-hidden="true" class="md-calendar-day-header"><thead></thead></table>' +
645 '<div class="md-calendar-scroll-mask">' +
646 '<md-virtual-repeat-container class="md-calendar-scroll-container" ' +
647 'md-offset-size="' + (TBODY_SINGLE_ROW_HEIGHT - TBODY_HEIGHT) + '">' +
648 '<table role="grid" tabindex="0" class="md-calendar" aria-readonly="true">' +
649 '<tbody ' +
650 'md-calendar-month-body ' +
651 'role="rowgroup" ' +
652 'md-virtual-repeat="i in monthCtrl.items" ' +
653 'md-month-offset="$index" ' +
654 'class="md-calendar-month" ' +
655 'md-start-index="monthCtrl.getSelectedMonthIndex()" ' +
656 'md-item-size="' + TBODY_HEIGHT + '">' +
657
658 // The <tr> ensures that the <tbody> will always have the
659 // proper height, even if it's empty. If it's content is
660 // compiled, the <tr> will be overwritten.
661 '<tr aria-hidden="true" md-force-height="\'' + TBODY_HEIGHT + 'px\'"></tr>' +
662 '</tbody>' +
663 '</table>' +
664 '</md-virtual-repeat-container>' +
665 '</div>',
666 require: ['^^mdCalendar', 'mdCalendarMonth'],
667 controller: CalendarMonthCtrl,
668 controllerAs: 'monthCtrl',
669 bindToController: true,
670 link: function(scope, element, attrs, controllers) {
671 var calendarCtrl = controllers[0];
672 var monthCtrl = controllers[1];
673 monthCtrl.initialize(calendarCtrl);
674 }
675 };
676 }
677
678 /**
679 * Controller for the calendar month component.
680 * ngInject @constructor
681 */
682 function CalendarMonthCtrl($element, $scope, $animate, $q,
683 $$mdDateUtil, $mdDateLocale) {
684
685 /** @final {!angular.JQLite} */
686 this.$element = $element;
687
688 /** @final {!angular.Scope} */
689 this.$scope = $scope;
690
691 /** @final {!angular.$animate} */
692 this.$animate = $animate;
693
694 /** @final {!angular.$q} */
695 this.$q = $q;
696
697 /** @final */
698 this.dateUtil = $$mdDateUtil;
699
700 /** @final */
701 this.dateLocale = $mdDateLocale;
702
703 /** @final {HTMLElement} */
704 this.calendarScroller = $element[0].querySelector('.md-virtual-repeat-scroller');
705
706 /** @type {boolean} */
707 this.isInitialized = false;
708
709 /** @type {boolean} */
710 this.isMonthTransitionInProgress = false;
711
712 var self = this;
713
714 /**
715 * Handles a click event on a date cell.
716 * Created here so that every cell can use the same function instance.
717 * @this {HTMLTableCellElement} The cell that was clicked.
718 */
719 this.cellClickHandler = function() {
720 var timestamp = $$mdDateUtil.getTimestampFromNode(this);
721 self.$scope.$apply(function() {
722 // The timestamp has to be converted to a valid date.
723 self.calendarCtrl.setNgModelValue(new Date(timestamp));
724 });
725 };
726
727 /**
728 * Handles click events on the month headers. Switches
729 * the calendar to the year view.
730 * @this {HTMLTableCellElement} The cell that was clicked.
731 */
732 this.headerClickHandler = function() {
733 self.calendarCtrl.setCurrentView('year', $$mdDateUtil.getTimestampFromNode(this));
734 };
735 }
736
737 /** Initialization **/
738
739 /**
740 * Initialize the controller by saving a reference to the calendar and
741 * setting up the object that will be iterated by the virtual repeater.
742 */
743 CalendarMonthCtrl.prototype.initialize = function(calendarCtrl) {
744 /**
745 * Dummy array-like object for virtual-repeat to iterate over. The length is the total
746 * number of months that can be viewed. We add 2 months: one to include the current month
747 * and one for the last dummy month.
748 *
749 * This is shorter than ideal because of a (potential) Firefox bug
750 * https://bugzilla.mozilla.org/show_bug.cgi?id=1181658.
751 */
752
753 this.items = {
754 length: this.dateUtil.getMonthDistance(
755 calendarCtrl.firstRenderableDate,
756 calendarCtrl.lastRenderableDate
757 ) + 2
758 };
759
760 this.calendarCtrl = calendarCtrl;
761 this.attachScopeListeners();
762 calendarCtrl.updateVirtualRepeat();
763
764 // Fire the initial render, since we might have missed it the first time it fired.
765 calendarCtrl.ngModelCtrl && calendarCtrl.ngModelCtrl.$render();
766 };
767
768 /**
769 * Gets the "index" of the currently selected date as it would be in the virtual-repeat.
770 * @returns {number} the "index" of the currently selected date
771 */
772 CalendarMonthCtrl.prototype.getSelectedMonthIndex = function() {
773 var calendarCtrl = this.calendarCtrl;
774
775 return this.dateUtil.getMonthDistance(
776 calendarCtrl.firstRenderableDate,
777 calendarCtrl.displayDate || calendarCtrl.selectedDate || calendarCtrl.today
778 );
779 };
780
781 /**
782 * Change the date that is being shown in the calendar. If the given date is in a different
783 * month, the displayed month will be transitioned.
784 * @param {Date} date
785 */
786 CalendarMonthCtrl.prototype.changeDisplayDate = function(date) {
787 // Initialization is deferred until this function is called because we want to reflect
788 // the starting value of ngModel.
789 if (!this.isInitialized) {
790 this.buildWeekHeader();
791 this.calendarCtrl.hideVerticalScrollbar(this);
792 this.isInitialized = true;
793 return this.$q.when();
794 }
795
796 // If trying to show an invalid date or a transition is in progress, do nothing.
797 if (!this.dateUtil.isValidDate(date) || this.isMonthTransitionInProgress) {
798 return this.$q.when();
799 }
800
801 this.isMonthTransitionInProgress = true;
802 var animationPromise = this.animateDateChange(date);
803
804 this.calendarCtrl.displayDate = date;
805
806 var self = this;
807 animationPromise.then(function() {
808 self.isMonthTransitionInProgress = false;
809 });
810
811 return animationPromise;
812 };
813
814 /**
815 * Animates the transition from the calendar's current month to the given month.
816 * @param {Date} date
817 * @returns {angular.$q.Promise} The animation promise.
818 */
819 CalendarMonthCtrl.prototype.animateDateChange = function(date) {
820 if (this.dateUtil.isValidDate(date)) {
821 var monthDistance = this.dateUtil.getMonthDistance(this.calendarCtrl.firstRenderableDate, date);
822 this.calendarScroller.scrollTop = monthDistance * TBODY_HEIGHT;
823 }
824
825 return this.$q.when();
826 };
827
828 /**
829 * Builds and appends a day-of-the-week header to the calendar.
830 * This should only need to be called once during initialization.
831 */
832 CalendarMonthCtrl.prototype.buildWeekHeader = function() {
833 var firstDayOfWeek = this.dateLocale.firstDayOfWeek;
834 var shortDays = this.dateLocale.shortDays;
835
836 var row = document.createElement('tr');
837 for (var i = 0; i < 7; i++) {
838 var th = document.createElement('th');
839 th.textContent = shortDays[(i + firstDayOfWeek) % 7];
840 row.appendChild(th);
841 }
842
843 this.$element.find('thead').append(row);
844 };
845
846 /**
847 * Attaches listeners for the scope events that are broadcast by the calendar.
848 */
849 CalendarMonthCtrl.prototype.attachScopeListeners = function() {
850 var self = this;
851
852 self.$scope.$on('md-calendar-parent-changed', function(event, value) {
853 self.calendarCtrl.changeSelectedDate(value);
854 self.changeDisplayDate(value);
855 });
856
857 self.$scope.$on('md-calendar-parent-action', angular.bind(this, this.handleKeyEvent));
858 };
859
860 /**
861 * Handles the month-specific keyboard interactions.
862 * @param {Object} event Scope event object passed by the calendar.
863 * @param {String} action Action, corresponding to the key that was pressed.
864 */
865 CalendarMonthCtrl.prototype.handleKeyEvent = function(event, action) {
866 var calendarCtrl = this.calendarCtrl;
867 var displayDate = calendarCtrl.displayDate;
868
869 if (action === 'select') {
870 calendarCtrl.setNgModelValue(displayDate);
871 } else {
872 var date = null;
873 var dateUtil = this.dateUtil;
874
875 switch (action) {
876 case 'move-right': date = dateUtil.incrementDays(displayDate, 1); break;
877 case 'move-left': date = dateUtil.incrementDays(displayDate, -1); break;
878
879 case 'move-page-down': date = dateUtil.incrementMonths(displayDate, 1); break;
880 case 'move-page-up': date = dateUtil.incrementMonths(displayDate, -1); break;
881
882 case 'move-row-down': date = dateUtil.incrementDays(displayDate, 7); break;
883 case 'move-row-up': date = dateUtil.incrementDays(displayDate, -7); break;
884
885 case 'start': date = dateUtil.getFirstDateOfMonth(displayDate); break;
886 case 'end': date = dateUtil.getLastDateOfMonth(displayDate); break;
887 }
888
889 if (date) {
890 date = this.dateUtil.clampDate(date, calendarCtrl.minDate, calendarCtrl.maxDate);
891
892 this.changeDisplayDate(date).then(function() {
893 calendarCtrl.focusDate(date);
894 });
895 }
896 }
897 };
898})();
899
900(function() {
901 'use strict';
902
903 mdCalendarMonthBodyDirective['$inject'] = ["$compile", "$$mdSvgRegistry"];
904 CalendarMonthBodyCtrl['$inject'] = ["$element", "$$mdDateUtil", "$mdDateLocale"];
905 angular.module('material.components.datepicker')
906 .directive('mdCalendarMonthBody', mdCalendarMonthBodyDirective);
907
908 /**
909 * Private directive consumed by md-calendar-month. Having this directive lets the calender use
910 * md-virtual-repeat and also cleanly separates the month DOM construction functions from
911 * the rest of the calendar controller logic.
912 * ngInject
913 */
914 function mdCalendarMonthBodyDirective($compile, $$mdSvgRegistry) {
915 var ARROW_ICON = $compile('<md-icon md-svg-src="' +
916 $$mdSvgRegistry.mdTabsArrow + '"></md-icon>')({})[0];
917
918 return {
919 require: ['^^mdCalendar', '^^mdCalendarMonth', 'mdCalendarMonthBody'],
920 scope: { offset: '=mdMonthOffset' },
921 controller: CalendarMonthBodyCtrl,
922 controllerAs: 'mdMonthBodyCtrl',
923 bindToController: true,
924 link: function(scope, element, attrs, controllers) {
925 var calendarCtrl = controllers[0];
926 var monthCtrl = controllers[1];
927 var monthBodyCtrl = controllers[2];
928
929 monthBodyCtrl.calendarCtrl = calendarCtrl;
930 monthBodyCtrl.monthCtrl = monthCtrl;
931 monthBodyCtrl.arrowIcon = ARROW_ICON.cloneNode(true);
932
933 // The virtual-repeat re-uses the same DOM elements, so there are only a limited number
934 // of repeated items that are linked, and then those elements have their bindings updated.
935 // Since the months are not generated by bindings, we simply regenerate the entire thing
936 // when the binding (offset) changes.
937 scope.$watch(function() { return monthBodyCtrl.offset; }, function(offset) {
938 if (angular.isNumber(offset)) {
939 monthBodyCtrl.generateContent();
940 }
941 });
942 }
943 };
944 }
945
946 /**
947 * Controller for a single calendar month.
948 * ngInject @constructor
949 */
950 function CalendarMonthBodyCtrl($element, $$mdDateUtil, $mdDateLocale) {
951 /**
952 * @final
953 * @type {!JQLite}
954 */
955 this.$element = $element;
956
957 /** @final */
958 this.dateUtil = $$mdDateUtil;
959
960 /** @final */
961 this.dateLocale = $mdDateLocale;
962
963 /** @type {Object} Reference to the month view. */
964 this.monthCtrl = null;
965
966 /** @type {Object} Reference to the calendar. */
967 this.calendarCtrl = null;
968
969 /**
970 * Number of months from the start of the month "items" that the currently rendered month
971 * occurs. Set via angular data binding.
972 * @type {number|null}
973 */
974 this.offset = null;
975
976 /**
977 * Date cell to focus after appending the month to the document.
978 * @type {HTMLElement}
979 */
980 this.focusAfterAppend = null;
981 }
982
983 /** Generate and append the content for this month to the directive element. */
984 CalendarMonthBodyCtrl.prototype.generateContent = function() {
985 var date = this.dateUtil.incrementMonths(this.calendarCtrl.firstRenderableDate, this.offset);
986
987 this.$element
988 .empty()
989 .append(this.buildCalendarForMonth(date));
990
991 if (this.focusAfterAppend) {
992 this.focusAfterAppend.classList.add(this.calendarCtrl.FOCUSED_DATE_CLASS);
993 this.focusAfterAppend = null;
994 }
995 };
996
997 /**
998 * Creates a single cell to contain a date in the calendar with all appropriate
999 * attributes and classes added. If a date is given, the cell content will be set
1000 * based on the date.
1001 * @param {Date=} opt_date
1002 * @returns {HTMLElement}
1003 */
1004 CalendarMonthBodyCtrl.prototype.buildDateCell = function(opt_date) {
1005 var monthCtrl = this.monthCtrl;
1006 var calendarCtrl = this.calendarCtrl;
1007
1008 // TODO(jelbourn): cloneNode is likely a faster way of doing this.
1009 var cell = document.createElement('td');
1010 cell.tabIndex = -1;
1011 cell.classList.add('md-calendar-date');
1012 cell.setAttribute('role', 'gridcell');
1013
1014 if (opt_date) {
1015 cell.setAttribute('tabindex', '-1');
1016 cell.setAttribute('aria-label', this.dateLocale.longDateFormatter(opt_date));
1017 cell.id = calendarCtrl.getDateId(opt_date, 'month');
1018
1019 // Use `data-timestamp` attribute because IE10 does not support the `dataset` property.
1020 cell.setAttribute('data-timestamp', opt_date.getTime());
1021
1022 // TODO(jelourn): Doing these comparisons for class addition during generation might be slow.
1023 // It may be better to finish the construction and then query the node and add the class.
1024 if (this.dateUtil.isSameDay(opt_date, calendarCtrl.today)) {
1025 cell.classList.add(calendarCtrl.TODAY_CLASS);
1026 }
1027
1028 if (this.dateUtil.isValidDate(calendarCtrl.selectedDate) &&
1029 this.dateUtil.isSameDay(opt_date, calendarCtrl.selectedDate)) {
1030 cell.classList.add(calendarCtrl.SELECTED_DATE_CLASS);
1031 cell.setAttribute('aria-selected', 'true');
1032 }
1033
1034 var cellText = this.dateLocale.dates[opt_date.getDate()];
1035
1036 if (this.isDateEnabled(opt_date)) {
1037 // Add a indicator for select, hover, and focus states.
1038 var selectionIndicator = document.createElement('span');
1039 selectionIndicator.classList.add('md-calendar-date-selection-indicator');
1040 selectionIndicator.textContent = cellText;
1041 cell.appendChild(selectionIndicator);
1042 cell.addEventListener('click', monthCtrl.cellClickHandler);
1043
1044 if (calendarCtrl.displayDate && this.dateUtil.isSameDay(opt_date, calendarCtrl.displayDate)) {
1045 this.focusAfterAppend = cell;
1046 }
1047 } else {
1048 cell.classList.add('md-calendar-date-disabled');
1049 cell.textContent = cellText;
1050 }
1051 }
1052
1053 return cell;
1054 };
1055
1056 /**
1057 * Check whether date is in range and enabled
1058 * @param {Date=} opt_date
1059 * @return {boolean} Whether the date is enabled.
1060 */
1061 CalendarMonthBodyCtrl.prototype.isDateEnabled = function(opt_date) {
1062 return this.dateUtil.isDateWithinRange(opt_date,
1063 this.calendarCtrl.minDate, this.calendarCtrl.maxDate) &&
1064 (!angular.isFunction(this.calendarCtrl.dateFilter)
1065 || this.calendarCtrl.dateFilter(opt_date));
1066 };
1067
1068 /**
1069 * Builds a `tr` element for the calendar grid.
1070 * @param rowNumber The week number within the month.
1071 * @returns {HTMLElement}
1072 */
1073 CalendarMonthBodyCtrl.prototype.buildDateRow = function(rowNumber) {
1074 var row = document.createElement('tr');
1075 row.setAttribute('role', 'row');
1076
1077 // Because of an NVDA bug (with Firefox), the row needs an aria-label in order
1078 // to prevent the entire row being read aloud when the user moves between rows.
1079 // See http://community.nvda-project.org/ticket/4643.
1080 row.setAttribute('aria-label', this.dateLocale.weekNumberFormatter(rowNumber));
1081
1082 return row;
1083 };
1084
1085 /**
1086 * Builds the <tbody> content for the given date's month.
1087 * @param {Date=} opt_dateInMonth
1088 * @returns {DocumentFragment} A document fragment containing the <tr> elements.
1089 */
1090 CalendarMonthBodyCtrl.prototype.buildCalendarForMonth = function(opt_dateInMonth) {
1091 var date = this.dateUtil.isValidDate(opt_dateInMonth) ? opt_dateInMonth : new Date();
1092
1093 var firstDayOfMonth = this.dateUtil.getFirstDateOfMonth(date);
1094 var firstDayOfTheWeek = this.getLocaleDay_(firstDayOfMonth);
1095 var numberOfDaysInMonth = this.dateUtil.getNumberOfDaysInMonth(date);
1096
1097 // Store rows for the month in a document fragment so that we can append them all at once.
1098 var monthBody = document.createDocumentFragment();
1099
1100 var rowNumber = 1;
1101 var row = this.buildDateRow(rowNumber);
1102 monthBody.appendChild(row);
1103
1104 // If this is the final month in the list of items, only the first week should render,
1105 // so we should return immediately after the first row is complete and has been
1106 // attached to the body.
1107 var isFinalMonth = this.offset === this.monthCtrl.items.length - 1;
1108
1109 // Add a label for the month. If the month starts on a Sun/Mon/Tues, the month label
1110 // goes on a row above the first of the month. Otherwise, the month label takes up the first
1111 // two cells of the first row.
1112 var blankCellOffset = 0;
1113 var monthLabelCell = document.createElement('td');
1114 var monthLabelCellContent = document.createElement('span');
1115 var calendarCtrl = this.calendarCtrl;
1116
1117 monthLabelCellContent.textContent = this.dateLocale.monthHeaderFormatter(date);
1118 monthLabelCell.appendChild(monthLabelCellContent);
1119 monthLabelCell.classList.add('md-calendar-month-label');
1120 // If the entire month is after the max date, render the label as a disabled state.
1121 if (calendarCtrl.maxDate && firstDayOfMonth > calendarCtrl.maxDate) {
1122 monthLabelCell.classList.add('md-calendar-month-label-disabled');
1123 // If the user isn't supposed to be able to change views, render the
1124 // label as usual, but disable the clicking functionality.
1125 } else if (!calendarCtrl.mode) {
1126 monthLabelCell.addEventListener('click', this.monthCtrl.headerClickHandler);
1127 monthLabelCell.setAttribute('data-timestamp', firstDayOfMonth.getTime());
1128 monthLabelCell.setAttribute('aria-label', this.dateLocale.monthFormatter(date));
1129 monthLabelCell.classList.add('md-calendar-label-clickable');
1130 monthLabelCell.appendChild(this.arrowIcon.cloneNode(true));
1131 }
1132
1133 if (firstDayOfTheWeek <= 2) {
1134 monthLabelCell.setAttribute('colspan', '7');
1135
1136 var monthLabelRow = this.buildDateRow();
1137 monthLabelRow.appendChild(monthLabelCell);
1138 monthBody.insertBefore(monthLabelRow, row);
1139
1140 if (isFinalMonth) {
1141 return monthBody;
1142 }
1143 } else {
1144 blankCellOffset = 3;
1145 monthLabelCell.setAttribute('colspan', '3');
1146 row.appendChild(monthLabelCell);
1147 }
1148
1149 // Add a blank cell for each day of the week that occurs before the first of the month.
1150 // For example, if the first day of the month is a Tuesday, add blank cells for Sun and Mon.
1151 // The blankCellOffset is needed in cases where the first N cells are used by the month label.
1152 for (var i = blankCellOffset; i < firstDayOfTheWeek; i++) {
1153 row.appendChild(this.buildDateCell());
1154 }
1155
1156 // Add a cell for each day of the month, keeping track of the day of the week so that
1157 // we know when to start a new row.
1158 var dayOfWeek = firstDayOfTheWeek;
1159 var iterationDate = firstDayOfMonth;
1160 for (var d = 1; d <= numberOfDaysInMonth; d++) {
1161 // If we've reached the end of the week, start a new row.
1162 if (dayOfWeek === 7) {
1163 // We've finished the first row, so we're done if this is the final month.
1164 if (isFinalMonth) {
1165 return monthBody;
1166 }
1167 dayOfWeek = 0;
1168 rowNumber++;
1169 row = this.buildDateRow(rowNumber);
1170 monthBody.appendChild(row);
1171 }
1172
1173 iterationDate.setDate(d);
1174 var cell = this.buildDateCell(iterationDate);
1175 row.appendChild(cell);
1176
1177 dayOfWeek++;
1178 }
1179
1180 // Ensure that the last row of the month has 7 cells.
1181 while (row.childNodes.length < 7) {
1182 row.appendChild(this.buildDateCell());
1183 }
1184
1185 // Ensure that all months have 6 rows. This is necessary for now because the virtual-repeat
1186 // requires that all items have exactly the same height.
1187 while (monthBody.childNodes.length < 6) {
1188 var whitespaceRow = this.buildDateRow();
1189 for (var j = 0; j < 7; j++) {
1190 whitespaceRow.appendChild(this.buildDateCell());
1191 }
1192 monthBody.appendChild(whitespaceRow);
1193 }
1194
1195 return monthBody;
1196 };
1197
1198 /**
1199 * Gets the day-of-the-week index for a date for the current locale.
1200 * @private
1201 * @param {Date} date
1202 * @returns {number} The column index of the date in the calendar.
1203 */
1204 CalendarMonthBodyCtrl.prototype.getLocaleDay_ = function(date) {
1205 return (date.getDay() + (7 - this.dateLocale.firstDayOfWeek)) % 7;
1206 };
1207})();
1208
1209(function() {
1210 'use strict';
1211
1212 CalendarYearCtrl['$inject'] = ["$element", "$scope", "$animate", "$q", "$$mdDateUtil", "$mdUtil"];
1213 angular.module('material.components.datepicker')
1214 .directive('mdCalendarYear', calendarDirective);
1215
1216 /**
1217 * Height of one calendar year tbody. This must be made known to the virtual-repeat and is
1218 * subsequently used for scrolling to specific years.
1219 */
1220 var TBODY_HEIGHT = 88;
1221
1222 /** Private component, representing a list of years in the calendar. */
1223 function calendarDirective() {
1224 return {
1225 template:
1226 '<div class="md-calendar-scroll-mask">' +
1227 '<md-virtual-repeat-container class="md-calendar-scroll-container">' +
1228 '<table role="grid" tabindex="0" class="md-calendar" aria-readonly="true">' +
1229 '<tbody ' +
1230 'md-calendar-year-body ' +
1231 'role="rowgroup" ' +
1232 'md-virtual-repeat="i in yearCtrl.items" ' +
1233 'md-year-offset="$index" class="md-calendar-year" ' +
1234 'md-start-index="yearCtrl.getFocusedYearIndex()" ' +
1235 'md-item-size="' + TBODY_HEIGHT + '">' +
1236 // The <tr> ensures that the <tbody> will have the proper
1237 // height, even though it may be empty.
1238 '<tr aria-hidden="true" md-force-height="\'' + TBODY_HEIGHT + 'px\'"></tr>' +
1239 '</tbody>' +
1240 '</table>' +
1241 '</md-virtual-repeat-container>' +
1242 '</div>',
1243 require: ['^^mdCalendar', 'mdCalendarYear'],
1244 controller: CalendarYearCtrl,
1245 controllerAs: 'yearCtrl',
1246 bindToController: true,
1247 link: function(scope, element, attrs, controllers) {
1248 var calendarCtrl = controllers[0];
1249 var yearCtrl = controllers[1];
1250 yearCtrl.initialize(calendarCtrl);
1251 }
1252 };
1253 }
1254
1255 /**
1256 * Controller for the mdCalendar component.
1257 * ngInject @constructor
1258 */
1259 function CalendarYearCtrl($element, $scope, $animate, $q, $$mdDateUtil, $mdUtil) {
1260
1261 /** @final {!angular.JQLite} */
1262 this.$element = $element;
1263
1264 /** @final {!angular.Scope} */
1265 this.$scope = $scope;
1266
1267 /** @final {!angular.$animate} */
1268 this.$animate = $animate;
1269
1270 /** @final {!angular.$q} */
1271 this.$q = $q;
1272
1273 /** @final */
1274 this.dateUtil = $$mdDateUtil;
1275
1276 /** @final {HTMLElement} */
1277 this.calendarScroller = $element[0].querySelector('.md-virtual-repeat-scroller');
1278
1279 /** @type {boolean} */
1280 this.isInitialized = false;
1281
1282 /** @type {boolean} */
1283 this.isMonthTransitionInProgress = false;
1284
1285 /** @final */
1286 this.$mdUtil = $mdUtil;
1287
1288 var self = this;
1289
1290 /**
1291 * Handles a click event on a date cell.
1292 * Created here so that every cell can use the same function instance.
1293 * @this {HTMLTableCellElement} The cell that was clicked.
1294 */
1295 this.cellClickHandler = function() {
1296 self.onTimestampSelected($$mdDateUtil.getTimestampFromNode(this));
1297 };
1298 }
1299
1300 /**
1301 * Initialize the controller by saving a reference to the calendar and
1302 * setting up the object that will be iterated by the virtual repeater.
1303 */
1304 CalendarYearCtrl.prototype.initialize = function(calendarCtrl) {
1305 /**
1306 * Dummy array-like object for virtual-repeat to iterate over. The length is the total
1307 * number of years that can be viewed. We add 1 extra in order to include the current year.
1308 */
1309
1310 this.items = {
1311 length: this.dateUtil.getYearDistance(
1312 calendarCtrl.firstRenderableDate,
1313 calendarCtrl.lastRenderableDate
1314 ) + 1
1315 };
1316
1317 this.calendarCtrl = calendarCtrl;
1318 this.attachScopeListeners();
1319 calendarCtrl.updateVirtualRepeat();
1320
1321 // Fire the initial render, since we might have missed it the first time it fired.
1322 calendarCtrl.ngModelCtrl && calendarCtrl.ngModelCtrl.$render();
1323 };
1324
1325 /**
1326 * Gets the "index" of the currently selected date as it would be in the virtual-repeat.
1327 * @returns {number}
1328 */
1329 CalendarYearCtrl.prototype.getFocusedYearIndex = function() {
1330 var calendarCtrl = this.calendarCtrl;
1331
1332 return this.dateUtil.getYearDistance(
1333 calendarCtrl.firstRenderableDate,
1334 calendarCtrl.displayDate || calendarCtrl.selectedDate || calendarCtrl.today
1335 );
1336 };
1337
1338 /**
1339 * Change the date that is highlighted in the calendar.
1340 * @param {Date} date
1341 */
1342 CalendarYearCtrl.prototype.changeDate = function(date) {
1343 // Initialization is deferred until this function is called because we want to reflect
1344 // the starting value of ngModel.
1345 if (!this.isInitialized) {
1346 this.calendarCtrl.hideVerticalScrollbar(this);
1347 this.isInitialized = true;
1348 return this.$q.when();
1349 } else if (this.dateUtil.isValidDate(date) && !this.isMonthTransitionInProgress) {
1350 var self = this;
1351 var animationPromise = this.animateDateChange(date);
1352
1353 self.isMonthTransitionInProgress = true;
1354 self.calendarCtrl.displayDate = date;
1355
1356 return animationPromise.then(function() {
1357 self.isMonthTransitionInProgress = false;
1358 });
1359 }
1360 };
1361
1362 /**
1363 * Animates the transition from the calendar's current month to the given month.
1364 * @param {Date} date
1365 * @returns {angular.$q.Promise} The animation promise.
1366 */
1367 CalendarYearCtrl.prototype.animateDateChange = function(date) {
1368 if (this.dateUtil.isValidDate(date)) {
1369 var monthDistance = this.dateUtil.getYearDistance(this.calendarCtrl.firstRenderableDate, date);
1370 this.calendarScroller.scrollTop = monthDistance * TBODY_HEIGHT;
1371 }
1372
1373 return this.$q.when();
1374 };
1375
1376 /**
1377 * Handles the year-view-specific keyboard interactions.
1378 * @param {Object} event Scope event object passed by the calendar.
1379 * @param {String} action Action, corresponding to the key that was pressed.
1380 */
1381 CalendarYearCtrl.prototype.handleKeyEvent = function(event, action) {
1382 var self = this;
1383 var calendarCtrl = self.calendarCtrl;
1384 var displayDate = calendarCtrl.displayDate;
1385
1386 if (action === 'select') {
1387 self.changeDate(displayDate).then(function() {
1388 self.onTimestampSelected(displayDate);
1389 });
1390 } else {
1391 var date = null;
1392 var dateUtil = self.dateUtil;
1393
1394 switch (action) {
1395 case 'move-right': date = dateUtil.incrementMonths(displayDate, 1); break;
1396 case 'move-left': date = dateUtil.incrementMonths(displayDate, -1); break;
1397
1398 case 'move-row-down': date = dateUtil.incrementMonths(displayDate, 6); break;
1399 case 'move-row-up': date = dateUtil.incrementMonths(displayDate, -6); break;
1400 }
1401
1402 if (date) {
1403 var min = calendarCtrl.minDate ? dateUtil.getFirstDateOfMonth(calendarCtrl.minDate) : null;
1404 var max = calendarCtrl.maxDate ? dateUtil.getFirstDateOfMonth(calendarCtrl.maxDate) : null;
1405 date = dateUtil.getFirstDateOfMonth(self.dateUtil.clampDate(date, min, max));
1406
1407 self.changeDate(date).then(function() {
1408 calendarCtrl.focusDate(date);
1409 });
1410 }
1411 }
1412 };
1413
1414 /**
1415 * Attaches listeners for the scope events that are broadcast by the calendar.
1416 */
1417 CalendarYearCtrl.prototype.attachScopeListeners = function() {
1418 var self = this;
1419
1420 self.$scope.$on('md-calendar-parent-changed', function(event, value) {
1421 self.calendarCtrl.changeSelectedDate(value ? self.dateUtil.getFirstDateOfMonth(value) : value);
1422 self.changeDate(value);
1423 });
1424
1425 self.$scope.$on('md-calendar-parent-action', angular.bind(self, self.handleKeyEvent));
1426 };
1427
1428 /**
1429 * Handles the behavior when a date is selected. Depending on the `mode`
1430 * of the calendar, this can either switch back to the calendar view or
1431 * set the model value.
1432 * @param {number} timestamp The selected timestamp.
1433 */
1434 CalendarYearCtrl.prototype.onTimestampSelected = function(timestamp) {
1435 var calendarCtrl = this.calendarCtrl;
1436
1437 if (calendarCtrl.mode) {
1438 this.$mdUtil.nextTick(function() {
1439 // The timestamp has to be converted to a valid date.
1440 calendarCtrl.setNgModelValue(new Date(timestamp));
1441 });
1442 } else {
1443 calendarCtrl.setCurrentView('month', timestamp);
1444 }
1445 };
1446})();
1447
1448(function() {
1449 'use strict';
1450
1451 CalendarYearBodyCtrl['$inject'] = ["$element", "$$mdDateUtil", "$mdDateLocale"];
1452 angular.module('material.components.datepicker')
1453 .directive('mdCalendarYearBody', mdCalendarYearDirective);
1454
1455 /**
1456 * Private component, consumed by the md-calendar-year, which separates the DOM construction logic
1457 * and allows for the year view to use md-virtual-repeat.
1458 */
1459 function mdCalendarYearDirective() {
1460 return {
1461 require: ['^^mdCalendar', '^^mdCalendarYear', 'mdCalendarYearBody'],
1462 scope: { offset: '=mdYearOffset' },
1463 controller: CalendarYearBodyCtrl,
1464 controllerAs: 'mdYearBodyCtrl',
1465 bindToController: true,
1466 link: function(scope, element, attrs, controllers) {
1467 var calendarCtrl = controllers[0];
1468 var yearCtrl = controllers[1];
1469 var yearBodyCtrl = controllers[2];
1470
1471 yearBodyCtrl.calendarCtrl = calendarCtrl;
1472 yearBodyCtrl.yearCtrl = yearCtrl;
1473
1474 scope.$watch(function() { return yearBodyCtrl.offset; }, function(offset) {
1475 if (angular.isNumber(offset)) {
1476 yearBodyCtrl.generateContent();
1477 }
1478 });
1479 }
1480 };
1481 }
1482
1483 /**
1484 * Controller for a single year.
1485 * ngInject @constructor
1486 */
1487 function CalendarYearBodyCtrl($element, $$mdDateUtil, $mdDateLocale) {
1488 /**
1489 * @final
1490 * @type {!JQLite}
1491 */
1492 this.$element = $element;
1493
1494 /** @final */
1495 this.dateUtil = $$mdDateUtil;
1496
1497 /** @final */
1498 this.dateLocale = $mdDateLocale;
1499
1500 /** @type {Object} Reference to the calendar. */
1501 this.calendarCtrl = null;
1502
1503 /** @type {Object} Reference to the year view. */
1504 this.yearCtrl = null;
1505
1506 /**
1507 * Number of months from the start of the month "items" that the currently rendered month
1508 * occurs. Set via angular data binding.
1509 * @type {number|null}
1510 */
1511 this.offset = null;
1512
1513 /**
1514 * Date cell to focus after appending the month to the document.
1515 * @type {HTMLElement}
1516 */
1517 this.focusAfterAppend = null;
1518 }
1519
1520 /** Generate and append the content for this year to the directive element. */
1521 CalendarYearBodyCtrl.prototype.generateContent = function() {
1522 var date = this.dateUtil.incrementYears(this.calendarCtrl.firstRenderableDate, this.offset);
1523
1524 this.$element
1525 .empty()
1526 .append(this.buildCalendarForYear(date));
1527
1528 if (this.focusAfterAppend) {
1529 this.focusAfterAppend.classList.add(this.calendarCtrl.FOCUSED_DATE_CLASS);
1530 this.focusAfterAppend = null;
1531 }
1532 };
1533
1534 /**
1535 * Creates a single cell to contain a year in the calendar.
1536 * @param {number} year Four-digit year.
1537 * @param {number} month Zero-indexed month.
1538 * @returns {HTMLElement}
1539 */
1540 CalendarYearBodyCtrl.prototype.buildMonthCell = function(year, month) {
1541 var calendarCtrl = this.calendarCtrl;
1542 var yearCtrl = this.yearCtrl;
1543 var cell = this.buildBlankCell();
1544
1545 // Represent this month/year as a date.
1546 var firstOfMonth = new Date(year, month, 1);
1547 cell.setAttribute('aria-label', this.dateLocale.monthFormatter(firstOfMonth));
1548 cell.id = calendarCtrl.getDateId(firstOfMonth, 'year');
1549
1550 // Use `data-timestamp` attribute because IE10 does not support the `dataset` property.
1551 cell.setAttribute('data-timestamp', String(firstOfMonth.getTime()));
1552
1553 if (this.dateUtil.isSameMonthAndYear(firstOfMonth, calendarCtrl.today)) {
1554 cell.classList.add(calendarCtrl.TODAY_CLASS);
1555 }
1556
1557 if (this.dateUtil.isValidDate(calendarCtrl.selectedDate) &&
1558 this.dateUtil.isSameMonthAndYear(firstOfMonth, calendarCtrl.selectedDate)) {
1559 cell.classList.add(calendarCtrl.SELECTED_DATE_CLASS);
1560 cell.setAttribute('aria-selected', 'true');
1561 }
1562
1563 var cellText = this.dateLocale.shortMonths[month];
1564
1565 if (this.dateUtil.isMonthWithinRange(
1566 firstOfMonth, calendarCtrl.minDate, calendarCtrl.maxDate) &&
1567 (!angular.isFunction(calendarCtrl.monthFilter) ||
1568 calendarCtrl.monthFilter(firstOfMonth))) {
1569 var selectionIndicator = document.createElement('span');
1570 selectionIndicator.classList.add('md-calendar-date-selection-indicator');
1571 selectionIndicator.textContent = cellText;
1572 cell.appendChild(selectionIndicator);
1573 cell.addEventListener('click', yearCtrl.cellClickHandler);
1574
1575 if (calendarCtrl.displayDate &&
1576 this.dateUtil.isSameMonthAndYear(firstOfMonth, calendarCtrl.displayDate)) {
1577 this.focusAfterAppend = cell;
1578 }
1579 } else {
1580 cell.classList.add('md-calendar-date-disabled');
1581 cell.textContent = cellText;
1582 }
1583
1584 return cell;
1585 };
1586
1587 /**
1588 * Builds a blank cell.
1589 * @return {HTMLElement}
1590 */
1591 CalendarYearBodyCtrl.prototype.buildBlankCell = function() {
1592 var cell = document.createElement('td');
1593 cell.tabIndex = -1;
1594 cell.classList.add('md-calendar-date');
1595 cell.setAttribute('role', 'gridcell');
1596
1597 cell.setAttribute('tabindex', '-1');
1598 return cell;
1599 };
1600
1601 /**
1602 * Builds the <tbody> content for the given year.
1603 * @param {Date} date Date for which the content should be built.
1604 * @returns {DocumentFragment} A document fragment containing the months within the year.
1605 */
1606 CalendarYearBodyCtrl.prototype.buildCalendarForYear = function(date) {
1607 // Store rows for the month in a document fragment so that we can append them all at once.
1608 var year = date.getFullYear();
1609 var yearBody = document.createDocumentFragment();
1610
1611 var monthCell, i;
1612 // First row contains label and Jan-Jun.
1613 var firstRow = document.createElement('tr');
1614 var labelCell = document.createElement('td');
1615 labelCell.className = 'md-calendar-month-label';
1616 labelCell.textContent = String(year);
1617 firstRow.appendChild(labelCell);
1618
1619 for (i = 0; i < 6; i++) {
1620 firstRow.appendChild(this.buildMonthCell(year, i));
1621 }
1622 yearBody.appendChild(firstRow);
1623
1624 // Second row contains a blank cell and Jul-Dec.
1625 var secondRow = document.createElement('tr');
1626 secondRow.appendChild(this.buildBlankCell());
1627 for (i = 6; i < 12; i++) {
1628 secondRow.appendChild(this.buildMonthCell(year, i));
1629 }
1630 yearBody.appendChild(secondRow);
1631
1632 return yearBody;
1633 };
1634})();
1635
1636(function() {
1637 'use strict';
1638
1639 /**
1640 * @ngdoc service
1641 * @name $mdDateLocaleProvider
1642 * @module material.components.datepicker
1643 *
1644 * @description
1645 * The `$mdDateLocaleProvider` is the provider that creates the `$mdDateLocale` service.
1646 * This provider that allows the user to specify messages, formatters, and parsers for date
1647 * internationalization. The `$mdDateLocale` service itself is consumed by AngularJS Material
1648 * components that deal with dates
1649 * (i.e. <a ng-href="api/directive/mdDatepicker">mdDatepicker</a>).
1650 *
1651 * @property {Array<string>} months Array of month names (in order).
1652 * @property {Array<string>} shortMonths Array of abbreviated month names.
1653 * @property {Array<string>} days Array of the days of the week (in order).
1654 * @property {Array<string>} shortDays Array of abbreviated days of the week.
1655 * @property {Array<string>} dates Array of dates of the month. Only necessary for locales
1656 * using a numeral system other than [1, 2, 3...].
1657 * @property {Array<string>} firstDayOfWeek The first day of the week. Sunday = 0, Monday = 1,
1658 * etc.
1659 * @property {function(string): Date} parseDate Function that converts a date string to a Date
1660 * object (the date portion).
1661 * @property {function(Date, string): string} formatDate Function to format a date object to a
1662 * string. The datepicker directive also provides the time zone, if it was specified.
1663 * @property {function(Date): string} monthHeaderFormatter Function that returns the label for
1664 * a month given a date.
1665 * @property {function(Date): string} monthFormatter Function that returns the full name of a month
1666 * for a given date.
1667 * @property {function(number): string} weekNumberFormatter Function that returns a label for
1668 * a week given the week number.
1669 * @property {function(Date): string} longDateFormatter Function that formats a date into a long
1670 * `aria-label` that is read by the screen reader when the focused date changes.
1671 * @property {string} msgCalendar Translation of the label "Calendar" for the current locale.
1672 * @property {string} msgOpenCalendar Translation of the button label "Open calendar" for the
1673 * current locale.
1674 * @property {Date} firstRenderableDate The date from which the datepicker calendar will begin
1675 * rendering. Note that this will be ignored if a minimum date is set.
1676 * Defaults to January 1st 1880.
1677 * @property {Date} lastRenderableDate The last date that will be rendered by the datepicker
1678 * calendar. Note that this will be ignored if a maximum date is set.
1679 * Defaults to January 1st 2130.
1680 * @property {function(string): boolean} isDateComplete Function to determine whether a string
1681 * makes sense to be parsed to a `Date` object. Returns `true` if the date appears to be complete
1682 * and parsing should occur. By default, this checks for 3 groups of text or numbers separated
1683 * by delimiters. This means that by default, date strings must include a month, day, and year
1684 * to be parsed and for the model to be updated.
1685 *
1686 * @usage
1687 * <hljs lang="js">
1688 * myAppModule.config(function($mdDateLocaleProvider) {
1689 *
1690 * // Example of a French localization.
1691 * $mdDateLocaleProvider.months = ['janvier', 'février', 'mars', ...];
1692 * $mdDateLocaleProvider.shortMonths = ['janv', 'févr', 'mars', ...];
1693 * $mdDateLocaleProvider.days = ['dimanche', 'lundi', 'mardi', ...];
1694 * $mdDateLocaleProvider.shortDays = ['Di', 'Lu', 'Ma', ...];
1695 *
1696 * // Can change week display to start on Monday.
1697 * $mdDateLocaleProvider.firstDayOfWeek = 1;
1698 *
1699 * // Optional.
1700 * $mdDateLocaleProvider.dates = [1, 2, 3, 4, 5, 6, ...];
1701 *
1702 * // Example uses moment.js to parse and format dates.
1703 * $mdDateLocaleProvider.parseDate = function(dateString) {
1704 * var m = moment(dateString, 'L', true);
1705 * return m.isValid() ? m.toDate() : new Date(NaN);
1706 * };
1707 *
1708 * $mdDateLocaleProvider.formatDate = function(date) {
1709 * var m = moment(date);
1710 * return m.isValid() ? m.format('L') : '';
1711 * };
1712 *
1713 * // Allow only a day and month to be specified.
1714 * // This is required if using the 'M/D' format with moment.js.
1715 * $mdDateLocaleProvider.isDateComplete = function(dateString) {
1716 * dateString = dateString.trim();
1717 *
1718 * // Look for two chunks of content (either numbers or text) separated by delimiters.
1719 * var re = /^(([a-zA-Z]{3,}|[0-9]{1,4})([ .,]+|[/-]))([a-zA-Z]{3,}|[0-9]{1,4})/;
1720 * return re.test(dateString);
1721 * };
1722 *
1723 * $mdDateLocaleProvider.monthHeaderFormatter = function(date) {
1724 * return myShortMonths[date.getMonth()] + ' ' + date.getFullYear();
1725 * };
1726 *
1727 * // In addition to date display, date components also need localized messages
1728 * // for aria-labels for screen-reader users.
1729 *
1730 * $mdDateLocaleProvider.weekNumberFormatter = function(weekNumber) {
1731 * return 'Semaine ' + weekNumber;
1732 * };
1733 *
1734 * $mdDateLocaleProvider.msgCalendar = 'Calendrier';
1735 * $mdDateLocaleProvider.msgOpenCalendar = 'Ouvrir le calendrier';
1736 *
1737 * // You can also set when your calendar begins and ends.
1738 * $mdDateLocaleProvider.firstRenderableDate = new Date(1776, 6, 4);
1739 * $mdDateLocaleProvider.lastRenderableDate = new Date(2012, 11, 21);
1740 * });
1741 * </hljs>
1742 *
1743 */
1744 angular.module('material.components.datepicker').config(["$provide", function($provide) {
1745 // TODO(jelbourn): Assert provided values are correctly formatted. Need assertions.
1746
1747 /** @constructor */
1748 function DateLocaleProvider() {
1749 /** Array of full month names. E.g., ['January', 'February', ...] */
1750 this.months = null;
1751
1752 /** Array of abbreviated month names. E.g., ['Jan', 'Feb', ...] */
1753 this.shortMonths = null;
1754
1755 /** Array of full day of the week names. E.g., ['Monday', 'Tuesday', ...] */
1756 this.days = null;
1757
1758 /** Array of abbreviated dat of the week names. E.g., ['M', 'T', ...] */
1759 this.shortDays = null;
1760
1761 /** Array of dates of a month (1 - 31). Characters might be different in some locales. */
1762 this.dates = null;
1763
1764 /** Index of the first day of the week. 0 = Sunday, 1 = Monday, etc. */
1765 this.firstDayOfWeek = 0;
1766
1767 /**
1768 * Function that converts the date portion of a Date to a string.
1769 * @type {(function(Date): string)}
1770 */
1771 this.formatDate = null;
1772
1773 /**
1774 * Function that converts a date string to a Date object (the date portion)
1775 * @type {function(string): Date}
1776 */
1777 this.parseDate = null;
1778
1779 /**
1780 * Function that formats a Date into a month header string.
1781 * @type {function(Date): string}
1782 */
1783 this.monthHeaderFormatter = null;
1784
1785 /**
1786 * Function that formats a week number into a label for the week.
1787 * @type {function(number): string}
1788 */
1789 this.weekNumberFormatter = null;
1790
1791 /**
1792 * Function that formats a date into a long aria-label that is read
1793 * when the focused date changes.
1794 * @type {function(Date): string}
1795 */
1796 this.longDateFormatter = null;
1797
1798 /**
1799 * Function to determine whether a string makes sense to be
1800 * parsed to a Date object.
1801 * @type {function(string): boolean}
1802 */
1803 this.isDateComplete = null;
1804
1805 /**
1806 * ARIA label for the calendar "dialog" used in the datepicker.
1807 * @type {string}
1808 */
1809 this.msgCalendar = '';
1810
1811 /**
1812 * ARIA label for the datepicker's "Open calendar" buttons.
1813 * @type {string}
1814 */
1815 this.msgOpenCalendar = '';
1816 }
1817
1818 /**
1819 * Factory function that returns an instance of the dateLocale service.
1820 * ngInject
1821 * @param $locale
1822 * @param $filter
1823 * @returns {DateLocale}
1824 */
1825 DateLocaleProvider.prototype.$get = function($locale, $filter) {
1826 /**
1827 * Default date-to-string formatting function.
1828 * @param {!Date} date
1829 * @param {string=} timezone
1830 * @returns {string}
1831 */
1832 function defaultFormatDate(date, timezone) {
1833 if (!date) {
1834 return '';
1835 }
1836
1837 // All of the dates created through ng-material *should* be set to midnight.
1838 // If we encounter a date where the localeTime shows at 11pm instead of midnight,
1839 // we have run into an issue with DST where we need to increment the hour by one:
1840 // var d = new Date(1992, 9, 8, 0, 0, 0);
1841 // d.toLocaleString(); // == "10/7/1992, 11:00:00 PM"
1842 var localeTime = date.toLocaleTimeString();
1843 var formatDate = date;
1844 if (date.getHours() === 0 &&
1845 (localeTime.indexOf('11:') !== -1 || localeTime.indexOf('23:') !== -1)) {
1846 formatDate = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 1, 0, 0);
1847 }
1848
1849 return $filter('date')(formatDate, 'M/d/yyyy', timezone);
1850 }
1851
1852 /**
1853 * Default string-to-date parsing function.
1854 * @param {string|number} dateString
1855 * @returns {!Date}
1856 */
1857 function defaultParseDate(dateString) {
1858 return new Date(dateString);
1859 }
1860
1861 /**
1862 * Default function to determine whether a string makes sense to be
1863 * parsed to a Date object.
1864 *
1865 * This is very permissive and is just a basic check to ensure that
1866 * things like single integers aren't able to be parsed into dates.
1867 * @param {string} dateString
1868 * @returns {boolean}
1869 */
1870 function defaultIsDateComplete(dateString) {
1871 dateString = dateString.trim();
1872
1873 // Looks for three chunks of content (either numbers or text) separated
1874 // by delimiters.
1875 var re = /^(([a-zA-Z]{3,}|[0-9]{1,4})([ .,]+|[/-])){2}([a-zA-Z]{3,}|[0-9]{1,4})$/;
1876 return re.test(dateString);
1877 }
1878
1879 /**
1880 * Default date-to-string formatter to get a month header.
1881 * @param {!Date} date
1882 * @returns {string}
1883 */
1884 function defaultMonthHeaderFormatter(date) {
1885 return service.shortMonths[date.getMonth()] + ' ' + date.getFullYear();
1886 }
1887
1888 /**
1889 * Default formatter for a month.
1890 * @param {!Date} date
1891 * @returns {string}
1892 */
1893 function defaultMonthFormatter(date) {
1894 return service.months[date.getMonth()] + ' ' + date.getFullYear();
1895 }
1896
1897 /**
1898 * Default week number formatter.
1899 * @param number
1900 * @returns {string}
1901 */
1902 function defaultWeekNumberFormatter(number) {
1903 return 'Week ' + number;
1904 }
1905
1906 /**
1907 * Default formatter for date cell aria-labels.
1908 * @param {!Date} date
1909 * @returns {string}
1910 */
1911 function defaultLongDateFormatter(date) {
1912 // Example: 'Thursday June 18 2015'
1913 return [
1914 service.days[date.getDay()],
1915 service.months[date.getMonth()],
1916 service.dates[date.getDate()],
1917 date.getFullYear()
1918 ].join(' ');
1919 }
1920
1921 // The default "short" day strings are the first character of each day,
1922 // e.g., "Monday" => "M".
1923 var defaultShortDays = $locale.DATETIME_FORMATS.SHORTDAY.map(function(day) {
1924 return day.substring(0, 1);
1925 });
1926
1927 // The default dates are simply the numbers 1 through 31.
1928 var defaultDates = Array(32);
1929 for (var i = 1; i <= 31; i++) {
1930 defaultDates[i] = i;
1931 }
1932
1933 // Default ARIA messages are in English (US).
1934 var defaultMsgCalendar = 'Calendar';
1935 var defaultMsgOpenCalendar = 'Open calendar';
1936
1937 // Default start/end dates that are rendered in the calendar.
1938 var defaultFirstRenderableDate = new Date(1880, 0, 1);
1939 var defaultLastRendereableDate = new Date(defaultFirstRenderableDate.getFullYear() + 250, 0, 1);
1940
1941 var service = {
1942 months: this.months || $locale.DATETIME_FORMATS.MONTH,
1943 shortMonths: this.shortMonths || $locale.DATETIME_FORMATS.SHORTMONTH,
1944 days: this.days || $locale.DATETIME_FORMATS.DAY,
1945 shortDays: this.shortDays || defaultShortDays,
1946 dates: this.dates || defaultDates,
1947 firstDayOfWeek: this.firstDayOfWeek || 0,
1948 formatDate: this.formatDate || defaultFormatDate,
1949 parseDate: this.parseDate || defaultParseDate,
1950 isDateComplete: this.isDateComplete || defaultIsDateComplete,
1951 monthHeaderFormatter: this.monthHeaderFormatter || defaultMonthHeaderFormatter,
1952 monthFormatter: this.monthFormatter || defaultMonthFormatter,
1953 weekNumberFormatter: this.weekNumberFormatter || defaultWeekNumberFormatter,
1954 longDateFormatter: this.longDateFormatter || defaultLongDateFormatter,
1955 msgCalendar: this.msgCalendar || defaultMsgCalendar,
1956 msgOpenCalendar: this.msgOpenCalendar || defaultMsgOpenCalendar,
1957 firstRenderableDate: this.firstRenderableDate || defaultFirstRenderableDate,
1958 lastRenderableDate: this.lastRenderableDate || defaultLastRendereableDate
1959 };
1960
1961 return service;
1962 };
1963 DateLocaleProvider.prototype.$get['$inject'] = ["$locale", "$filter"];
1964
1965 $provide.provider('$mdDateLocale', new DateLocaleProvider());
1966 }]);
1967})();
1968
1969(function() {
1970 'use strict';
1971
1972 /**
1973 * Utility for performing date calculations to facilitate operation of the calendar and
1974 * datepicker.
1975 */
1976 angular.module('material.components.datepicker').factory('$$mdDateUtil', ["$mdDateLocale", function($mdDateLocale) {
1977 return {
1978 getFirstDateOfMonth: getFirstDateOfMonth,
1979 getNumberOfDaysInMonth: getNumberOfDaysInMonth,
1980 getDateInNextMonth: getDateInNextMonth,
1981 getDateInPreviousMonth: getDateInPreviousMonth,
1982 isInNextMonth: isInNextMonth,
1983 isInPreviousMonth: isInPreviousMonth,
1984 getDateMidpoint: getDateMidpoint,
1985 isSameMonthAndYear: isSameMonthAndYear,
1986 getWeekOfMonth: getWeekOfMonth,
1987 incrementDays: incrementDays,
1988 incrementMonths: incrementMonths,
1989 getLastDateOfMonth: getLastDateOfMonth,
1990 isSameDay: isSameDay,
1991 getMonthDistance: getMonthDistance,
1992 isValidDate: isValidDate,
1993 setDateTimeToMidnight: setDateTimeToMidnight,
1994 createDateAtMidnight: createDateAtMidnight,
1995 isDateWithinRange: isDateWithinRange,
1996 incrementYears: incrementYears,
1997 getYearDistance: getYearDistance,
1998 clampDate: clampDate,
1999 getTimestampFromNode: getTimestampFromNode,
2000 isMonthWithinRange: isMonthWithinRange,
2001 removeLocalTzAndReparseDate: removeLocalTzAndReparseDate
2002 };
2003
2004 /**
2005 * Gets the first day of the month for the given date's month.
2006 * @param {Date} date
2007 * @returns {Date}
2008 */
2009 function getFirstDateOfMonth(date) {
2010 return new Date(date.getFullYear(), date.getMonth(), 1);
2011 }
2012
2013 /**
2014 * Gets the number of days in the month for the given date's month.
2015 * @param date
2016 * @returns {number}
2017 */
2018 function getNumberOfDaysInMonth(date) {
2019 return new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate();
2020 }
2021
2022 /**
2023 * Get an arbitrary date in the month after the given date's month.
2024 * @param date
2025 * @returns {Date}
2026 */
2027 function getDateInNextMonth(date) {
2028 return new Date(date.getFullYear(), date.getMonth() + 1, 1);
2029 }
2030
2031 /**
2032 * Get an arbitrary date in the month before the given date's month.
2033 * @param date
2034 * @returns {Date}
2035 */
2036 function getDateInPreviousMonth(date) {
2037 return new Date(date.getFullYear(), date.getMonth() - 1, 1);
2038 }
2039
2040 /**
2041 * Gets whether two dates have the same month and year.
2042 * @param {Date} d1
2043 * @param {Date} d2
2044 * @returns {boolean}
2045 */
2046 function isSameMonthAndYear(d1, d2) {
2047 return d1.getFullYear() === d2.getFullYear() && d1.getMonth() === d2.getMonth();
2048 }
2049
2050 /**
2051 * Gets whether two dates are the same day (not not necessarily the same time).
2052 * @param {Date} d1
2053 * @param {Date} d2
2054 * @returns {boolean}
2055 */
2056 function isSameDay(d1, d2) {
2057 return d1.getDate() == d2.getDate() && isSameMonthAndYear(d1, d2);
2058 }
2059
2060 /**
2061 * Gets whether a date is in the month immediately after some date.
2062 * @param {Date} startDate The date from which to compare.
2063 * @param {Date} endDate The date to check.
2064 * @returns {boolean}
2065 */
2066 function isInNextMonth(startDate, endDate) {
2067 var nextMonth = getDateInNextMonth(startDate);
2068 return isSameMonthAndYear(nextMonth, endDate);
2069 }
2070
2071 /**
2072 * Gets whether a date is in the month immediately before some date.
2073 * @param {Date} startDate The date from which to compare.
2074 * @param {Date} endDate The date to check.
2075 * @returns {boolean}
2076 */
2077 function isInPreviousMonth(startDate, endDate) {
2078 var previousMonth = getDateInPreviousMonth(startDate);
2079 return isSameMonthAndYear(endDate, previousMonth);
2080 }
2081
2082 /**
2083 * Gets the midpoint between two dates.
2084 * @param {Date} d1
2085 * @param {Date} d2
2086 * @returns {Date}
2087 */
2088 function getDateMidpoint(d1, d2) {
2089 return createDateAtMidnight((d1.getTime() + d2.getTime()) / 2);
2090 }
2091
2092 /**
2093 * Gets the week of the month that a given date occurs in.
2094 * @param {Date} date
2095 * @returns {number} Index of the week of the month (zero-based).
2096 */
2097 function getWeekOfMonth(date) {
2098 var firstDayOfMonth = getFirstDateOfMonth(date);
2099 return Math.floor((firstDayOfMonth.getDay() + date.getDate() - 1) / 7);
2100 }
2101
2102 /**
2103 * Gets a new date incremented by the given number of days. Number of days can be negative.
2104 * @param {Date} date
2105 * @param {number} numberOfDays
2106 * @returns {Date}
2107 */
2108 function incrementDays(date, numberOfDays) {
2109 return new Date(date.getFullYear(), date.getMonth(), date.getDate() + numberOfDays);
2110 }
2111
2112 /**
2113 * Gets a new date incremented by the given number of months. Number of months can be negative.
2114 * If the date of the given month does not match the target month, the date will be set to the
2115 * last day of the month.
2116 * @param {Date} date
2117 * @param {number} numberOfMonths
2118 * @returns {Date}
2119 */
2120 function incrementMonths(date, numberOfMonths) {
2121 // If the same date in the target month does not actually exist, the Date object will
2122 // automatically advance *another* month by the number of missing days.
2123 // For example, if you try to go from Jan. 30 to Feb. 30, you'll end up on March 2.
2124 // So, we check if the month overflowed and go to the last day of the target month instead.
2125 var dateInTargetMonth = new Date(date.getFullYear(), date.getMonth() + numberOfMonths, 1);
2126 var numberOfDaysInMonth = getNumberOfDaysInMonth(dateInTargetMonth);
2127 if (numberOfDaysInMonth < date.getDate()) {
2128 dateInTargetMonth.setDate(numberOfDaysInMonth);
2129 } else {
2130 dateInTargetMonth.setDate(date.getDate());
2131 }
2132
2133 return dateInTargetMonth;
2134 }
2135
2136 /**
2137 * Get the integer distance between two months. This *only* considers the month and year
2138 * portion of the Date instances.
2139 *
2140 * @param {Date} start
2141 * @param {Date} end
2142 * @returns {number} Number of months between `start` and `end`. If `end` is before `start`
2143 * chronologically, this number will be negative.
2144 */
2145 function getMonthDistance(start, end) {
2146 return (12 * (end.getFullYear() - start.getFullYear())) + (end.getMonth() - start.getMonth());
2147 }
2148
2149 /**
2150 * Gets the last day of the month for the given date.
2151 * @param {Date} date
2152 * @returns {Date}
2153 */
2154 function getLastDateOfMonth(date) {
2155 return new Date(date.getFullYear(), date.getMonth(), getNumberOfDaysInMonth(date));
2156 }
2157
2158 /**
2159 * Checks whether a date is valid.
2160 * @param {Date} date
2161 * @return {boolean} Whether the date is a valid Date.
2162 */
2163 function isValidDate(date) {
2164 return date && date.getTime && !isNaN(date.getTime());
2165 }
2166
2167 /**
2168 * Sets a date's time to midnight.
2169 * @param {Date} date
2170 */
2171 function setDateTimeToMidnight(date) {
2172 if (isValidDate(date)) {
2173 date.setHours(0, 0, 0, 0);
2174 }
2175 }
2176
2177 /**
2178 * Creates a date with the time set to midnight.
2179 * Drop-in replacement for two forms of the Date constructor via opt_value.
2180 * @param {number|Date=} opt_value Leave undefined for a Date representing now. Or use a
2181 * single value representing the number of seconds since the Unix Epoch or a Date object.
2182 * @return {Date} New date with time set to midnight.
2183 */
2184 function createDateAtMidnight(opt_value) {
2185 var date;
2186 if (angular.isDate(opt_value)) {
2187 date = opt_value;
2188 } else if (angular.isNumber(opt_value)) {
2189 date = new Date(opt_value);
2190 } else {
2191 date = new Date();
2192 }
2193 setDateTimeToMidnight(date);
2194 return date;
2195 }
2196
2197 /**
2198 * Checks if a date is within a min and max range, ignoring the time component.
2199 * If minDate or maxDate are not dates, they are ignored.
2200 * @param {Date} date
2201 * @param {Date} minDate
2202 * @param {Date} maxDate
2203 */
2204 function isDateWithinRange(date, minDate, maxDate) {
2205 var dateAtMidnight = createDateAtMidnight(date);
2206 var minDateAtMidnight = isValidDate(minDate) ? createDateAtMidnight(minDate) : null;
2207 var maxDateAtMidnight = isValidDate(maxDate) ? createDateAtMidnight(maxDate) : null;
2208 return (!minDateAtMidnight || minDateAtMidnight <= dateAtMidnight) &&
2209 (!maxDateAtMidnight || maxDateAtMidnight >= dateAtMidnight);
2210 }
2211
2212 /**
2213 * Gets a new date incremented by the given number of years. Number of years can be negative.
2214 * See `incrementMonths` for notes on overflow for specific dates.
2215 * @param {Date} date
2216 * @param {number} numberOfYears
2217 * @returns {Date}
2218 */
2219 function incrementYears(date, numberOfYears) {
2220 return incrementMonths(date, numberOfYears * 12);
2221 }
2222
2223 /**
2224 * Get the integer distance between two years. This *only* considers the year portion of the
2225 * Date instances.
2226 *
2227 * @param {Date} start
2228 * @param {Date} end
2229 * @returns {number} Number of months between `start` and `end`. If `end` is before `start`
2230 * chronologically, this number will be negative.
2231 */
2232 function getYearDistance(start, end) {
2233 return end.getFullYear() - start.getFullYear();
2234 }
2235
2236 /**
2237 * Clamps a date between a minimum and a maximum date.
2238 * @param {Date} date Date to be clamped
2239 * @param {Date=} minDate Minimum date
2240 * @param {Date=} maxDate Maximum date
2241 * @return {Date}
2242 */
2243 function clampDate(date, minDate, maxDate) {
2244 var boundDate = date;
2245 if (minDate && date < minDate) {
2246 boundDate = new Date(minDate.getTime());
2247 }
2248 if (maxDate && date > maxDate) {
2249 boundDate = new Date(maxDate.getTime());
2250 }
2251 return boundDate;
2252 }
2253
2254 /**
2255 * Extracts and parses the timestamp from a DOM node.
2256 * @param {HTMLElement} node Node from which the timestamp will be extracted.
2257 * @return {number} Time since epoch.
2258 */
2259 function getTimestampFromNode(node) {
2260 if (node && node.hasAttribute('data-timestamp')) {
2261 return Number(node.getAttribute('data-timestamp'));
2262 }
2263 }
2264
2265 /**
2266 * Checks if a month is within a min and max range, ignoring the date and time components.
2267 * If minDate or maxDate are not dates, they are ignored.
2268 * @param {Date} date
2269 * @param {Date} minDate
2270 * @param {Date} maxDate
2271 */
2272 function isMonthWithinRange(date, minDate, maxDate) {
2273 var month = date.getMonth();
2274 var year = date.getFullYear();
2275
2276 return (!minDate || minDate.getFullYear() < year || minDate.getMonth() <= month) &&
2277 (!maxDate || maxDate.getFullYear() > year || maxDate.getMonth() >= month);
2278 }
2279
2280 /**
2281 * @param {Date} value date in local timezone
2282 * @return {Date} date with local timezone offset removed
2283 */
2284 function removeLocalTzAndReparseDate(value) {
2285 var dateValue, formattedDate;
2286 // Remove the local timezone offset before calling formatDate.
2287 dateValue = new Date(value.getTime() + 60000 * value.getTimezoneOffset());
2288 formattedDate = $mdDateLocale.formatDate(dateValue);
2289 // parseDate only works with a date formatted by formatDate when using Moment validation.
2290 return $mdDateLocale.parseDate(formattedDate);
2291 }
2292 }]);
2293})();
2294
2295(function() {
2296 'use strict';
2297
2298 // TODO(jelbourn): forward more attributes to the internal input (required, autofocus, etc.)
2299 // TODO(jelbourn): something better for mobile (calendar panel takes up entire screen?)
2300 // TODO(jelbourn): input behavior (masking? auto-complete?)
2301
2302 DatePickerCtrl['$inject'] = ["$scope", "$element", "$attrs", "$window", "$mdConstant", "$mdTheming", "$mdUtil", "$mdDateLocale", "$$mdDateUtil", "$$rAF", "$filter", "$timeout"];
2303 datePickerDirective['$inject'] = ["$$mdSvgRegistry", "$mdUtil", "$mdAria", "inputDirective"];
2304 angular.module('material.components.datepicker')
2305 .directive('mdDatepicker', datePickerDirective);
2306
2307 /**
2308 * @ngdoc directive
2309 * @name mdDatepicker
2310 * @module material.components.datepicker
2311 *
2312 * @param {Date} ng-model The component's model. Expects either a JavaScript Date object or a
2313 * value that can be parsed into one (e.g. a ISO 8601 string).
2314 * @param {Object=} ng-model-options Allows tuning of the way in which `ng-model` is being
2315 * updated. Also allows for a timezone to be specified.
2316 * <a href="https://docs.angularjs.org/api/ng/directive/ngModelOptions#usage">
2317 * Read more at the ngModelOptions docs.</a>
2318 * @param {expression=} ng-change Expression evaluated when the model value changes.
2319 * @param {expression=} ng-focus Expression evaluated when the input is focused or the calendar
2320 * is opened.
2321 * @param {expression=} ng-blur Expression evaluated when focus is removed from the input or the
2322 * calendar is closed.
2323 * @param {boolean=} ng-disabled Whether the datepicker is disabled.
2324 * @param {boolean=} ng-required Whether a value is required for the datepicker.
2325 * @param {Date=} md-min-date Expression representing a min date (inclusive).
2326 * @param {Date=} md-max-date Expression representing a max date (inclusive).
2327 * @param {(function(Date): boolean)=} md-date-filter Function expecting a date and returning a
2328 * boolean whether it can be selected in "day" mode or not. Returning false will also trigger a
2329 * `filtered` model validation error.
2330 * @param {(function(Date): boolean)=} md-month-filter Function expecting a date and returning a
2331 * boolean whether it can be selected in "month" mode or not. Returning false will also trigger a
2332 * `filtered` model validation error.
2333 * @param {string=} md-placeholder The date input placeholder value.
2334 * @param {string=} md-open-on-focus When present, the calendar will be opened when the input
2335 * is focused.
2336 * @param {Boolean=} md-is-open Expression that can be used to open the datepicker's calendar
2337 * on-demand.
2338 * @param {string=} md-current-view Default open view of the calendar pane. Can be either
2339 * "month" or "year".
2340 * @param {string=} md-mode Restricts the user to only selecting a value from a particular view.
2341 * This option can be used if the user is only supposed to choose from a certain date type
2342 * (e.g. only selecting the month).
2343 * Can be either "month" or "day". **Note** that this will overwrite the `md-current-view` value.
2344 * @param {string=} md-hide-icons Determines which datepicker icons should be hidden. Note that
2345 * this may cause the datepicker to not align properly with other components.
2346 * **Use at your own risk.** Possible values are:
2347 * * `"all"` - Hides all icons.
2348 * * `"calendar"` - Only hides the calendar icon.
2349 * * `"triangle"` - Only hides the triangle icon.
2350 * @param {Object=} md-date-locale Allows for the values from the `$mdDateLocaleProvider` to be
2351 * overwritten on a per-element basis (e.g. `msgOpenCalendar` can be overwritten with
2352 * `md-date-locale="{ msgOpenCalendar: 'Open a special calendar' }"`).
2353 * @param {string=} input-aria-describedby A space-separated list of element IDs. This should
2354 * contain the IDs of any elements that describe this datepicker. Screen readers will read the
2355 * content of these elements at the end of announcing that the datepicker has been selected
2356 * and describing its current state. The descriptive elements do not need to be visible on the
2357 * page.
2358 * @param {string=} input-aria-labelledby A space-separated list of element IDs. The ideal use
2359 * case is that this would contain the ID of a `<label>` element should be associated with this
2360 * datepicker. This is necessary when using `md-datepicker` inside of an `md-input-container`
2361 * with a `<label>`.<br><br>
2362 * For `<label id="start-date">Start Date</label>`, you would set this to
2363 * `input-aria-labelledby="start-date"`.
2364 *
2365 * @description
2366 * `<md-datepicker>` is a component used to select a single date.
2367 * For information on how to configure internationalization for the date picker,
2368 * see <a ng-href="api/service/$mdDateLocaleProvider">$mdDateLocaleProvider</a>.
2369 *
2370 * This component supports
2371 * [ngMessages](https://docs.angularjs.org/api/ngMessages/directive/ngMessages).
2372 * Supported attributes are:
2373 * * `required`: whether a required date is not set.
2374 * * `mindate`: whether the selected date is before the minimum allowed date.
2375 * * `maxdate`: whether the selected date is after the maximum allowed date.
2376 * * `debounceInterval`: ms to delay input processing (since last debounce reset);
2377 * default value 500ms
2378 *
2379 * @usage
2380 * <hljs lang="html">
2381 * <md-datepicker ng-model="birthday"></md-datepicker>
2382 * </hljs>
2383 *
2384 */
2385
2386 function datePickerDirective($$mdSvgRegistry, $mdUtil, $mdAria, inputDirective) {
2387 return {
2388 template: function(tElement, tAttrs) {
2389 // Buttons are not in the tab order because users can open the calendar via keyboard
2390 // interaction on the text input, and multiple tab stops for one component (picker)
2391 // may be confusing.
2392 var hiddenIcons = tAttrs.mdHideIcons;
2393 var inputAriaDescribedby = tAttrs.inputAriaDescribedby;
2394 var inputAriaLabelledby = tAttrs.inputAriaLabelledby;
2395 var ariaLabelValue = tAttrs.ariaLabel || tAttrs.mdPlaceholder;
2396 var ngModelOptions = tAttrs.ngModelOptions;
2397
2398 var calendarButton = (hiddenIcons === 'all' || hiddenIcons === 'calendar') ? '' :
2399 '<md-button class="md-datepicker-button md-icon-button" type="button" ' +
2400 'tabindex="-1" aria-hidden="true" ' +
2401 'ng-click="ctrl.openCalendarPane($event)">' +
2402 '<md-icon class="md-datepicker-calendar-icon" aria-label="md-calendar" ' +
2403 'md-svg-src="' + $$mdSvgRegistry.mdCalendar + '"></md-icon>' +
2404 '</md-button>';
2405
2406 var triangleButton = '';
2407
2408 if (hiddenIcons !== 'all' && hiddenIcons !== 'triangle') {
2409 triangleButton = '' +
2410 '<md-button type="button" md-no-ink ' +
2411 'class="md-datepicker-triangle-button md-icon-button" ' +
2412 'ng-click="ctrl.openCalendarPane($event)" ' +
2413 'aria-label="{{::ctrl.locale.msgOpenCalendar}}">' +
2414 '<div class="md-datepicker-expand-triangle"></div>' +
2415 '</md-button>';
2416
2417 tElement.addClass(HAS_TRIANGLE_ICON_CLASS);
2418 }
2419
2420 return calendarButton +
2421 '<div class="md-datepicker-input-container" ng-class="{\'md-datepicker-focused\': ctrl.isFocused}">' +
2422 '<input ' +
2423 (ariaLabelValue ? 'aria-label="' + ariaLabelValue + '" ' : '') +
2424 (inputAriaDescribedby ? 'aria-describedby="' + inputAriaDescribedby + '" ' : '') +
2425 (inputAriaLabelledby ? 'aria-labelledby="' + inputAriaLabelledby + '" ' : '') +
2426 'class="md-datepicker-input" ' +
2427 'aria-haspopup="dialog" ' +
2428 'ng-focus="ctrl.setFocused(true)" ' +
2429 'ng-blur="ctrl.setFocused(false)"> ' +
2430 triangleButton +
2431 '</div>' +
2432
2433 // This pane will be detached from here and re-attached to the document body.
2434 '<div class="md-datepicker-calendar-pane md-whiteframe-z1" id="{{::ctrl.calendarPaneId}}">' +
2435 '<div class="md-datepicker-input-mask">' +
2436 '<div class="md-datepicker-input-mask-opaque"></div>' +
2437 '</div>' +
2438 '<div class="md-datepicker-calendar">' +
2439 '<md-calendar role="dialog" aria-label="{{::ctrl.locale.msgCalendar}}" ' +
2440 'md-current-view="{{::ctrl.currentView}}" ' +
2441 'md-mode="{{::ctrl.mode}}" ' +
2442 'md-min-date="ctrl.minDate" ' +
2443 'md-max-date="ctrl.maxDate" ' +
2444 'md-date-filter="ctrl.dateFilter" ' +
2445 'md-month-filter="ctrl.monthFilter" ' +
2446 (ngModelOptions ? 'ng-model-options="' + ngModelOptions + '" ' : '') +
2447 'ng-model="ctrl.date" ng-if="ctrl.isCalendarOpen">' +
2448 '</md-calendar>' +
2449 '</div>' +
2450 '</div>';
2451 },
2452 require: ['ngModel', 'mdDatepicker', '?^mdInputContainer', '?^form'],
2453 scope: {
2454 minDate: '=mdMinDate',
2455 maxDate: '=mdMaxDate',
2456 placeholder: '@mdPlaceholder',
2457 currentView: '@mdCurrentView',
2458 mode: '@mdMode',
2459 dateFilter: '=mdDateFilter',
2460 monthFilter: '=mdMonthFilter',
2461 isOpen: '=?mdIsOpen',
2462 debounceInterval: '=mdDebounceInterval',
2463 dateLocale: '=mdDateLocale'
2464 },
2465 controller: DatePickerCtrl,
2466 controllerAs: 'ctrl',
2467 bindToController: true,
2468 link: function(scope, element, attr, controllers) {
2469 var ngModelCtrl = controllers[0];
2470 var mdDatePickerCtrl = controllers[1];
2471 var mdInputContainer = controllers[2];
2472 var parentForm = controllers[3];
2473 var mdNoAsterisk = $mdUtil.parseAttributeBoolean(attr.mdNoAsterisk);
2474
2475 mdDatePickerCtrl.configureNgModel(ngModelCtrl, mdInputContainer, inputDirective);
2476
2477 if (mdInputContainer) {
2478 // We need to move the spacer after the datepicker itself,
2479 // because md-input-container adds it after the
2480 // md-datepicker-input by default. The spacer gets wrapped in a
2481 // div, because it floats and gets aligned next to the datepicker.
2482 // There are easier ways of working around this with CSS (making the
2483 // datepicker 100% wide, change the `display` etc.), however they
2484 // break the alignment with any other form controls.
2485 var spacer = element[0].querySelector('.md-errors-spacer');
2486
2487 if (spacer) {
2488 element.after(angular.element('<div>').append(spacer));
2489 }
2490
2491 mdInputContainer.setHasPlaceholder(attr.mdPlaceholder);
2492 mdInputContainer.input = element;
2493 mdInputContainer.element
2494 .addClass(INPUT_CONTAINER_CLASS)
2495 .toggleClass(HAS_CALENDAR_ICON_CLASS,
2496 attr.mdHideIcons !== 'calendar' && attr.mdHideIcons !== 'all');
2497
2498 if (!mdInputContainer.label) {
2499 $mdAria.expect(element, 'aria-label', attr.mdPlaceholder);
2500 } else if (!mdNoAsterisk) {
2501 attr.$observe('required', function(value) {
2502 mdInputContainer.label.toggleClass('md-required', !!value);
2503 });
2504 }
2505
2506 scope.$watch(mdInputContainer.isErrorGetter || function() {
2507 return ngModelCtrl.$invalid && (ngModelCtrl.$touched ||
2508 (parentForm && parentForm.$submitted));
2509 }, mdInputContainer.setInvalid);
2510 } else if (parentForm) {
2511 // If invalid, highlights the input when the parent form is submitted.
2512 var parentSubmittedWatcher = scope.$watch(function() {
2513 return parentForm.$submitted;
2514 }, function(isSubmitted) {
2515 if (isSubmitted) {
2516 mdDatePickerCtrl.updateErrorState();
2517 parentSubmittedWatcher();
2518 }
2519 });
2520 }
2521 }
2522 };
2523 }
2524
2525 /** Additional offset for the input's `size` attribute, which is updated based on its content. */
2526 var EXTRA_INPUT_SIZE = 3;
2527
2528 /** Class applied to the container if the date is invalid. */
2529 var INVALID_CLASS = 'md-datepicker-invalid';
2530
2531 /** Class applied to the datepicker when it's open. */
2532 var OPEN_CLASS = 'md-datepicker-open';
2533
2534 /** Class applied to the md-input-container, if a datepicker is placed inside it */
2535 var INPUT_CONTAINER_CLASS = '_md-datepicker-floating-label';
2536
2537 /** Class to be applied when the calendar icon is enabled. */
2538 var HAS_CALENDAR_ICON_CLASS = '_md-datepicker-has-calendar-icon';
2539
2540 /** Class to be applied when the triangle icon is enabled. */
2541 var HAS_TRIANGLE_ICON_CLASS = '_md-datepicker-has-triangle-icon';
2542
2543 /** Default time in ms to debounce input event by. */
2544 var DEFAULT_DEBOUNCE_INTERVAL = 500;
2545
2546 /**
2547 * Height of the calendar pane used to check if the pane is going outside the boundary of
2548 * the viewport. See calendar.scss for how $md-calendar-height is computed; an extra 20px is
2549 * also added to space the pane away from the exact edge of the screen.
2550 *
2551 * This is computed statically now, but can be changed to be measured if the circumstances
2552 * of calendar sizing are changed.
2553 */
2554 var CALENDAR_PANE_HEIGHT = 368;
2555
2556 /**
2557 * Width of the calendar pane used to check if the pane is going outside the boundary of
2558 * the viewport. See calendar.scss for how $md-calendar-width is computed; an extra 20px is
2559 * also added to space the pane away from the exact edge of the screen.
2560 *
2561 * This is computed statically now, but can be changed to be measured if the circumstances
2562 * of calendar sizing are changed.
2563 */
2564 var CALENDAR_PANE_WIDTH = 360;
2565
2566 /** Used for checking whether the current user agent is on iOS or Android. */
2567 var IS_MOBILE_REGEX = /ipad|iphone|ipod|android/i;
2568
2569 /**
2570 * Controller for md-datepicker.
2571 *
2572 * ngInject @constructor
2573 */
2574 function DatePickerCtrl($scope, $element, $attrs, $window, $mdConstant, $mdTheming, $mdUtil,
2575 $mdDateLocale, $$mdDateUtil, $$rAF, $filter, $timeout) {
2576
2577 /** @final */
2578 this.$window = $window;
2579
2580 /** @final */
2581 this.dateUtil = $$mdDateUtil;
2582
2583 /** @final */
2584 this.$mdConstant = $mdConstant;
2585
2586 /** @final */
2587 this.$mdUtil = $mdUtil;
2588
2589 /** @final */
2590 this.$$rAF = $$rAF;
2591
2592 /** @final */
2593 this.$mdDateLocale = $mdDateLocale;
2594
2595 /** @final */
2596 this.$timeout = $timeout;
2597
2598 /**
2599 * The root document element. This is used for attaching a top-level click handler to
2600 * close the calendar panel when a click outside said panel occurs. We use `documentElement`
2601 * instead of body because, when scrolling is disabled, some browsers consider the body element
2602 * to be completely off the screen and propagate events directly to the html element.
2603 * @type {!JQLite}
2604 */
2605 this.documentElement = angular.element(document.documentElement);
2606
2607 /** @type {!ngModel.NgModelController} */
2608 this.ngModelCtrl = null;
2609
2610 /** @type {HTMLInputElement} */
2611 this.inputElement = $element[0].querySelector('input');
2612
2613 /**
2614 * @final
2615 * @type {!JQLite}
2616 */
2617 this.ngInputElement = angular.element(this.inputElement);
2618
2619 /** @type {HTMLElement} */
2620 this.inputContainer = $element[0].querySelector('.md-datepicker-input-container');
2621
2622 /** @type {HTMLElement} Floating calendar pane. */
2623 this.calendarPane = $element[0].querySelector('.md-datepicker-calendar-pane');
2624
2625 /** @type {HTMLElement} Calendar icon button. */
2626 this.calendarButton = $element[0].querySelector('.md-datepicker-button');
2627
2628 /**
2629 * Element covering everything but the input in the top of the floating calendar pane.
2630 * @type {!JQLite}
2631 */
2632 this.inputMask = angular.element($element[0].querySelector('.md-datepicker-input-mask-opaque'));
2633
2634 /**
2635 * @final
2636 * @type {!JQLite}
2637 */
2638 this.$element = $element;
2639
2640 /**
2641 * @final
2642 * @type {!angular.Attributes}
2643 */
2644 this.$attrs = $attrs;
2645
2646 /**
2647 * @final
2648 * @type {!angular.Scope}
2649 */
2650 this.$scope = $scope;
2651
2652 /**
2653 * This holds the model that will be used by the calendar.
2654 * @type {Date|null|undefined}
2655 */
2656 this.date = null;
2657
2658 /** @type {boolean} */
2659 this.isFocused = false;
2660
2661 /** @type {boolean} */
2662 this.isDisabled = undefined;
2663 this.setDisabled($element[0].disabled || angular.isString($attrs.disabled));
2664
2665 /** @type {boolean} Whether the date-picker's calendar pane is open. */
2666 this.isCalendarOpen = false;
2667
2668 /** @type {boolean} Whether the calendar should open when the input is focused. */
2669 this.openOnFocus = $attrs.hasOwnProperty('mdOpenOnFocus');
2670
2671 /** @type {Object} Instance of the mdInputContainer controller */
2672 this.mdInputContainer = null;
2673
2674 /**
2675 * Element from which the calendar pane was opened. Keep track of this so that we can return
2676 * focus to it when the pane is closed.
2677 * @type {HTMLElement}
2678 */
2679 this.calendarPaneOpenedFrom = null;
2680
2681 /** @type {String} Unique id for the calendar pane. */
2682 this.calendarPaneId = 'md-date-pane-' + $mdUtil.nextUid();
2683
2684 /** Pre-bound click handler is saved so that the event listener can be removed. */
2685 this.bodyClickHandler = angular.bind(this, this.handleBodyClick);
2686
2687 /**
2688 * Name of the event that will trigger a close. Necessary to sniff the browser, because
2689 * the resize event doesn't make sense on mobile and can have a negative impact since it
2690 * triggers whenever the browser zooms in on a focused input.
2691 */
2692 this.windowEventName = IS_MOBILE_REGEX.test(
2693 navigator.userAgent || navigator.vendor || window.opera
2694 ) ? 'orientationchange' : 'resize';
2695
2696 /** Pre-bound close handler so that the event listener can be removed. */
2697 this.windowEventHandler = $mdUtil.debounce(angular.bind(this, this.closeCalendarPane), 100);
2698
2699 /** Pre-bound handler for the window blur event. Allows for it to be removed later. */
2700 this.windowBlurHandler = angular.bind(this, this.handleWindowBlur);
2701
2702 /** The built-in AngularJS date filter. */
2703 this.ngDateFilter = $filter('date');
2704
2705 /** @type {Number} Extra margin for the left side of the floating calendar pane. */
2706 this.leftMargin = 20;
2707
2708 /** @type {Number} Extra margin for the top of the floating calendar. Gets determined on the first open. */
2709 this.topMargin = null;
2710
2711 // Unless the user specifies so, the datepicker should not be a tab stop.
2712 // This is necessary because ngAria might add a tabindex to anything with an ng-model
2713 // (based on whether or not the user has turned that particular feature on/off).
2714 if ($attrs.tabindex) {
2715 this.ngInputElement.attr('tabindex', $attrs.tabindex);
2716 $attrs.$set('tabindex', null);
2717 } else {
2718 $attrs.$set('tabindex', '-1');
2719 }
2720
2721 $attrs.$set('aria-owns', this.calendarPaneId);
2722
2723 $mdTheming($element);
2724 $mdTheming(angular.element(this.calendarPane));
2725
2726 var self = this;
2727
2728 $scope.$on('$destroy', function() {
2729 self.detachCalendarPane();
2730 });
2731
2732 if ($attrs.mdIsOpen) {
2733 $scope.$watch('ctrl.isOpen', function(shouldBeOpen) {
2734 if (shouldBeOpen) {
2735 self.openCalendarPane({
2736 target: self.inputElement
2737 });
2738 } else {
2739 self.closeCalendarPane();
2740 }
2741 });
2742 }
2743
2744 // For AngularJS 1.4 and older, where there are no lifecycle hooks but bindings are
2745 // pre-assigned, manually call the $onInit hook.
2746 if (angular.version.major === 1 && angular.version.minor <= 4) {
2747 this.$onInit();
2748 }
2749 }
2750
2751 /**
2752 * AngularJS Lifecycle hook for newer AngularJS versions.
2753 * Bindings are not guaranteed to have been assigned in the controller, but they are in the
2754 * $onInit hook.
2755 */
2756 DatePickerCtrl.prototype.$onInit = function() {
2757
2758 /**
2759 * Holds locale-specific formatters, parsers, labels etc. Allows
2760 * the user to override specific ones from the $mdDateLocale provider.
2761 * @type {!Object}
2762 */
2763 this.locale = this.dateLocale ? angular.extend({}, this.$mdDateLocale, this.dateLocale)
2764 : this.$mdDateLocale;
2765
2766 this.installPropertyInterceptors();
2767 this.attachChangeListeners();
2768 this.attachInteractionListeners();
2769 };
2770
2771 /**
2772 * Sets up the controller's reference to ngModelController and
2773 * applies AngularJS's `input[type="date"]` directive.
2774 * @param {!angular.NgModelController} ngModelCtrl Instance of the ngModel controller.
2775 * @param {Object} mdInputContainer Instance of the mdInputContainer controller.
2776 * @param {Object} inputDirective Config for AngularJS's `input` directive.
2777 */
2778 DatePickerCtrl.prototype.configureNgModel = function(ngModelCtrl, mdInputContainer, inputDirective) {
2779 this.ngModelCtrl = ngModelCtrl;
2780 this.mdInputContainer = mdInputContainer;
2781
2782 // The input needs to be [type="date"] in order to be picked up by AngularJS.
2783 this.$attrs.$set('type', 'date');
2784
2785 // Invoke the `input` directive link function, adding a stub for the element.
2786 // This allows us to re-use AngularJS's logic for setting the timezone via ng-model-options.
2787 // It works by calling the link function directly which then adds the proper `$parsers` and
2788 // `$formatters` to the ngModel controller.
2789 inputDirective[0].link.pre(this.$scope, {
2790 on: angular.noop,
2791 val: angular.noop,
2792 0: {}
2793 }, this.$attrs, [ngModelCtrl]);
2794
2795 var self = this;
2796
2797 // Responds to external changes to the model value.
2798 self.ngModelCtrl.$formatters.push(function(value) {
2799 var parsedValue = angular.isDefined(value) ? value : null;
2800
2801 if (!(value instanceof Date)) {
2802 parsedValue = Date.parse(value);
2803
2804 // `parsedValue` is the time since epoch if valid or `NaN` if invalid.
2805 if (!isNaN(parsedValue) && angular.isNumber(parsedValue)) {
2806 value = new Date(parsedValue);
2807 }
2808
2809 if (value && !(value instanceof Date)) {
2810 throw Error(
2811 'The ng-model for md-datepicker must be a Date instance or a value ' +
2812 'that can be parsed into a date. Currently the model is of type: ' + typeof value
2813 );
2814 }
2815 }
2816
2817 self.onExternalChange(value);
2818
2819 return value;
2820 });
2821
2822 // Responds to external error state changes (e.g. ng-required based on another input).
2823 ngModelCtrl.$viewChangeListeners.unshift(angular.bind(this, this.updateErrorState));
2824
2825 // Forwards any events from the input to the root element. This is necessary to get `updateOn`
2826 // working for events that don't bubble (e.g. 'blur') since AngularJS binds the handlers to
2827 // the `<md-datepicker>`.
2828 var updateOn = self.$mdUtil.getModelOption(ngModelCtrl, 'updateOn');
2829
2830 if (updateOn) {
2831 this.ngInputElement.on(
2832 updateOn,
2833 angular.bind(this.$element, this.$element.triggerHandler, updateOn)
2834 );
2835 }
2836 };
2837
2838 /**
2839 * Attach event listeners for both the text input and the md-calendar.
2840 * Events are used instead of ng-model so that updates don't infinitely update the other
2841 * on a change. This should also be more performant than using a $watch.
2842 */
2843 DatePickerCtrl.prototype.attachChangeListeners = function() {
2844 var self = this;
2845
2846 self.$scope.$on('md-calendar-change', function(event, date) {
2847 self.setModelValue(date);
2848 self.onExternalChange(date);
2849 self.closeCalendarPane();
2850 });
2851
2852 self.ngInputElement.on('input', angular.bind(self, self.resizeInputElement));
2853
2854 var debounceInterval = angular.isDefined(this.debounceInterval) ?
2855 this.debounceInterval : DEFAULT_DEBOUNCE_INTERVAL;
2856 self.ngInputElement.on('input', self.$mdUtil.debounce(self.handleInputEvent,
2857 debounceInterval, self));
2858 };
2859
2860 /** Attach event listeners for user interaction. */
2861 DatePickerCtrl.prototype.attachInteractionListeners = function() {
2862 var self = this;
2863 var $scope = this.$scope;
2864 var keyCodes = this.$mdConstant.KEY_CODE;
2865
2866 // Add event listener through angular so that we can triggerHandler in unit tests.
2867 self.ngInputElement.on('keydown', function(event) {
2868 if (event.altKey && event.keyCode === keyCodes.DOWN_ARROW) {
2869 self.openCalendarPane(event);
2870 $scope.$digest();
2871 }
2872 });
2873
2874 if (self.openOnFocus) {
2875 self.ngInputElement.on('focus', angular.bind(self, self.openCalendarPane));
2876 self.ngInputElement.on('click', function(event) {
2877 event.stopPropagation();
2878 });
2879 self.ngInputElement.on('pointerdown',function(event) {
2880 if (event.target && event.target.setPointerCapture) {
2881 event.target.setPointerCapture(event.pointerId);
2882 }
2883 });
2884
2885 angular.element(self.$window).on('blur', self.windowBlurHandler);
2886
2887 $scope.$on('$destroy', function() {
2888 angular.element(self.$window).off('blur', self.windowBlurHandler);
2889 });
2890 }
2891
2892 $scope.$on('md-calendar-close', function() {
2893 self.closeCalendarPane();
2894 });
2895 };
2896
2897 /**
2898 * Capture properties set to the date-picker and imperatively handle internal changes.
2899 * This is done to avoid setting up additional $watches.
2900 */
2901 DatePickerCtrl.prototype.installPropertyInterceptors = function() {
2902 var self = this;
2903
2904 if (this.$attrs.ngDisabled) {
2905 // The expression is to be evaluated against the directive element's scope and not
2906 // the directive's isolate scope.
2907 var scope = this.$scope.$parent;
2908
2909 if (scope) {
2910 scope.$watch(this.$attrs.ngDisabled, function(isDisabled) {
2911 self.setDisabled(isDisabled);
2912 });
2913 }
2914 }
2915
2916 Object.defineProperty(this, 'placeholder', {
2917 get: function() { return self.inputElement.placeholder; },
2918 set: function(value) { self.inputElement.placeholder = value || ''; }
2919 });
2920 };
2921
2922 /**
2923 * Sets whether the date-picker is disabled.
2924 * @param {boolean} isDisabled
2925 */
2926 DatePickerCtrl.prototype.setDisabled = function(isDisabled) {
2927 this.isDisabled = isDisabled;
2928 this.inputElement.disabled = isDisabled;
2929
2930 if (this.calendarButton) {
2931 this.calendarButton.disabled = isDisabled;
2932 }
2933 };
2934
2935 /**
2936 * Sets the custom ngModel.$error flags to be consumed by ngMessages. Flags are:
2937 * - mindate: whether the selected date is before the minimum date.
2938 * - maxdate: whether the selected flag is after the maximum date.
2939 * - filtered: whether the selected date is allowed by the custom filtering function.
2940 * - valid: whether the entered text input is a valid date
2941 *
2942 * The 'required' flag is handled automatically by ngModel.
2943 *
2944 * @param {Date=} opt_date Date to check. If not given, defaults to the datepicker's model value.
2945 */
2946 DatePickerCtrl.prototype.updateErrorState = function(opt_date) {
2947 var date;
2948 if (opt_date) {
2949 date = new Date(opt_date.valueOf());
2950 } else {
2951 if (angular.isString(this.ngModelCtrl.$modelValue)) {
2952 date = new Date(this.ngModelCtrl.$modelValue);
2953 } else {
2954 date = angular.copy(this.ngModelCtrl.$modelValue);
2955 }
2956 }
2957
2958 // Clear any existing errors to get rid of anything that's no longer relevant.
2959 this.clearErrorState();
2960
2961 if (this.dateUtil.isValidDate(date)) {
2962 // Force all dates to midnight in order to ignore the time portion.
2963 date = this.dateUtil.createDateAtMidnight(date);
2964
2965 if (this.dateUtil.isValidDate(this.minDate)) {
2966 var minDate = this.dateUtil.createDateAtMidnight(this.minDate);
2967 this.ngModelCtrl.$setValidity('mindate', date >= minDate);
2968 }
2969
2970 if (this.dateUtil.isValidDate(this.maxDate)) {
2971 var maxDate = this.dateUtil.createDateAtMidnight(this.maxDate);
2972 this.ngModelCtrl.$setValidity('maxdate', date <= maxDate);
2973 }
2974
2975 if (angular.isFunction(this.dateFilter)) {
2976 this.ngModelCtrl.$setValidity('filtered', this.dateFilter(date));
2977 }
2978
2979 if (angular.isFunction(this.monthFilter)) {
2980 this.ngModelCtrl.$setValidity('filtered', this.monthFilter(date));
2981 }
2982 } else {
2983 // The date is seen as "not a valid date" if there is *something* set
2984 // (i.e.., not null or undefined), but that something isn't a valid date.
2985 this.ngModelCtrl.$setValidity('valid', date == null);
2986 }
2987
2988 var input = this.inputElement.value;
2989 var parsedDate = this.locale.parseDate(input);
2990
2991 if (!this.isInputValid(input, parsedDate) && this.ngModelCtrl.$valid) {
2992 this.ngModelCtrl.$setValidity('valid', date == null);
2993 }
2994
2995 angular.element(this.inputContainer).toggleClass(INVALID_CLASS,
2996 this.ngModelCtrl.$invalid && (this.ngModelCtrl.$touched || this.ngModelCtrl.$submitted));
2997 };
2998
2999 /**
3000 * Check to see if the input is valid, as the validation should fail if the model is invalid.
3001 *
3002 * @param {string} inputString
3003 * @param {Date} parsedDate
3004 * @return {boolean} Whether the input is valid
3005 */
3006 DatePickerCtrl.prototype.isInputValid = function (inputString, parsedDate) {
3007 return inputString === '' || (
3008 this.dateUtil.isValidDate(parsedDate) &&
3009 this.locale.isDateComplete(inputString) &&
3010 this.isDateEnabled(parsedDate)
3011 );
3012 };
3013
3014 /** Clears any error flags set by `updateErrorState`. */
3015 DatePickerCtrl.prototype.clearErrorState = function() {
3016 this.inputContainer.classList.remove(INVALID_CLASS);
3017 ['mindate', 'maxdate', 'filtered', 'valid'].forEach(function(field) {
3018 this.ngModelCtrl.$setValidity(field, true);
3019 }, this);
3020 };
3021
3022 /** Resizes the input element based on the size of its content. */
3023 DatePickerCtrl.prototype.resizeInputElement = function() {
3024 this.inputElement.size = this.inputElement.value.length + EXTRA_INPUT_SIZE;
3025 };
3026
3027 /**
3028 * Sets the model value if the user input is a valid date.
3029 * Adds an invalid class to the input element if not.
3030 */
3031 DatePickerCtrl.prototype.handleInputEvent = function() {
3032 var inputString = this.inputElement.value;
3033 var parsedDate = inputString ? this.locale.parseDate(inputString) : null;
3034 this.dateUtil.setDateTimeToMidnight(parsedDate);
3035
3036 // An input string is valid if it is either empty (representing no date)
3037 // or if it parses to a valid date that the user is allowed to select.
3038 var isValidInput = this.isInputValid(inputString, parsedDate);
3039
3040 // The datepicker's model is only updated when there is a valid input.
3041 if (isValidInput) {
3042 this.setModelValue(parsedDate);
3043 this.date = parsedDate;
3044 }
3045
3046 this.updateErrorState(parsedDate);
3047 };
3048
3049 /**
3050 * Check whether date is in range and enabled
3051 * @param {Date=} opt_date
3052 * @return {boolean} Whether the date is enabled.
3053 */
3054 DatePickerCtrl.prototype.isDateEnabled = function(opt_date) {
3055 return this.dateUtil.isDateWithinRange(opt_date, this.minDate, this.maxDate) &&
3056 (!angular.isFunction(this.dateFilter) || this.dateFilter(opt_date)) &&
3057 (!angular.isFunction(this.monthFilter) || this.monthFilter(opt_date));
3058 };
3059
3060 /** Position and attach the floating calendar to the document. */
3061 DatePickerCtrl.prototype.attachCalendarPane = function() {
3062 var calendarPane = this.calendarPane;
3063 var body = document.body;
3064
3065 calendarPane.style.transform = '';
3066 this.$element.addClass(OPEN_CLASS);
3067 this.mdInputContainer && this.mdInputContainer.element.addClass(OPEN_CLASS);
3068 angular.element(body).addClass('md-datepicker-is-showing');
3069
3070 var elementRect = this.inputContainer.getBoundingClientRect();
3071 var bodyRect = body.getBoundingClientRect();
3072
3073 if (!this.topMargin || this.topMargin < 0) {
3074 this.topMargin =
3075 (this.inputMask.parent().prop('clientHeight')
3076 - this.ngInputElement.prop('clientHeight')) / 2;
3077 }
3078
3079 // Check to see if the calendar pane would go off the screen. If so, adjust position
3080 // accordingly to keep it within the viewport.
3081 var paneTop = elementRect.top - bodyRect.top - this.topMargin;
3082 var paneLeft = elementRect.left - bodyRect.left - this.leftMargin;
3083
3084 // If ng-material has disabled body scrolling (for example, if a dialog is open),
3085 // then it's possible that the already-scrolled body has a negative top/left. In this case,
3086 // we want to treat the "real" top as (0 - bodyRect.top). In a normal scrolling situation,
3087 // though, the top of the viewport should just be the body's scroll position.
3088 var viewportTop = (bodyRect.top < 0 && document.body.scrollTop === 0) ?
3089 -bodyRect.top :
3090 document.body.scrollTop;
3091
3092 var viewportLeft = (bodyRect.left < 0 && document.body.scrollLeft === 0) ?
3093 -bodyRect.left :
3094 document.body.scrollLeft;
3095
3096 var viewportBottom = viewportTop + this.$window.innerHeight;
3097 var viewportRight = viewportLeft + this.$window.innerWidth;
3098
3099 // Creates an overlay with a hole the same size as element. We remove a pixel or two
3100 // on each end to make it overlap slightly. The overlay's background is added in
3101 // the theme in the form of a box-shadow with a huge spread.
3102 this.inputMask.css({
3103 position: 'absolute',
3104 left: this.leftMargin + 'px',
3105 top: this.topMargin + 'px',
3106 width: (elementRect.width - 1) + 'px',
3107 height: (elementRect.height - 2) + 'px'
3108 });
3109
3110 // If the right edge of the pane would be off the screen and shifting it left by the
3111 // difference would not go past the left edge of the screen. If the calendar pane is too
3112 // big to fit on the screen at all, move it to the left of the screen and scale the entire
3113 // element down to fit.
3114 if (paneLeft + CALENDAR_PANE_WIDTH > viewportRight) {
3115 if (viewportRight - CALENDAR_PANE_WIDTH > 0) {
3116 paneLeft = viewportRight - CALENDAR_PANE_WIDTH;
3117 } else {
3118 paneLeft = viewportLeft;
3119 var scale = this.$window.innerWidth / CALENDAR_PANE_WIDTH;
3120 calendarPane.style.transform = 'scale(' + scale + ')';
3121 }
3122
3123 calendarPane.classList.add('md-datepicker-pos-adjusted');
3124 }
3125
3126 // If the bottom edge of the pane would be off the screen and shifting it up by the
3127 // difference would not go past the top edge of the screen.
3128 if (paneTop + CALENDAR_PANE_HEIGHT > viewportBottom &&
3129 viewportBottom - CALENDAR_PANE_HEIGHT > viewportTop) {
3130 paneTop = viewportBottom - CALENDAR_PANE_HEIGHT;
3131 calendarPane.classList.add('md-datepicker-pos-adjusted');
3132 }
3133
3134 calendarPane.style.left = paneLeft + 'px';
3135 calendarPane.style.top = paneTop + 'px';
3136 document.body.appendChild(calendarPane);
3137
3138 // Add CSS class after one frame to trigger open animation.
3139 this.$$rAF(function() {
3140 calendarPane.classList.add('md-pane-open');
3141 });
3142 };
3143
3144 /** Detach the floating calendar pane from the document. */
3145 DatePickerCtrl.prototype.detachCalendarPane = function() {
3146 this.$element.removeClass(OPEN_CLASS);
3147 this.mdInputContainer && this.mdInputContainer.element.removeClass(OPEN_CLASS);
3148 angular.element(document.body).removeClass('md-datepicker-is-showing');
3149 this.calendarPane.classList.remove('md-pane-open');
3150 this.calendarPane.classList.remove('md-datepicker-pos-adjusted');
3151
3152 if (this.isCalendarOpen) {
3153 this.$mdUtil.enableScrolling();
3154 }
3155
3156 if (this.calendarPane.parentNode) {
3157 // Use native DOM removal because we do not want any of the
3158 // angular state of this element to be disposed.
3159 this.calendarPane.parentNode.removeChild(this.calendarPane);
3160 }
3161 };
3162
3163 /**
3164 * Open the floating calendar pane.
3165 * @param {MouseEvent|KeyboardEvent|{target: HTMLInputElement}} event
3166 */
3167 DatePickerCtrl.prototype.openCalendarPane = function(event) {
3168 if (!this.isCalendarOpen && !this.isDisabled && !this.inputFocusedOnWindowBlur) {
3169 this.isCalendarOpen = this.isOpen = true;
3170 this.calendarPaneOpenedFrom = event.target;
3171
3172 // Because the calendar pane is attached directly to the body, it is possible that the
3173 // rest of the component (input, etc) is in a different scrolling container, such as
3174 // an md-content. This means that, if the container is scrolled, the pane would remain
3175 // stationary. To remedy this, we disable scrolling while the calendar pane is open, which
3176 // also matches the native behavior for things like `<select>` on Mac and Windows.
3177 this.$mdUtil.disableScrollAround(this.calendarPane);
3178
3179 this.attachCalendarPane();
3180 this.focusCalendar();
3181 this.evalAttr('ngFocus');
3182
3183 // Attach click listener inside of a timeout because, if this open call was triggered by a
3184 // click, we don't want it to be immediately propagated up to the body and handled.
3185 var self = this;
3186 this.$mdUtil.nextTick(function() {
3187 // Use 'touchstart` in addition to click in order to work on iOS Safari, where click
3188 // events aren't propagated under most circumstances.
3189 // See http://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html
3190 self.documentElement.on('click touchstart', self.bodyClickHandler);
3191 }, false);
3192
3193 window.addEventListener(this.windowEventName, this.windowEventHandler);
3194 } else if (this.inputFocusedOnWindowBlur) {
3195 this.resetInputFocused();
3196 }
3197 };
3198
3199 /** Close the floating calendar pane. */
3200 DatePickerCtrl.prototype.closeCalendarPane = function() {
3201 if (this.isCalendarOpen) {
3202 var self = this;
3203
3204 self.detachCalendarPane();
3205 self.ngModelCtrl.$setTouched();
3206 self.evalAttr('ngBlur');
3207
3208 self.documentElement.off('click touchstart', self.bodyClickHandler);
3209 window.removeEventListener(self.windowEventName, self.windowEventHandler);
3210
3211 self.calendarPaneOpenedFrom.focus();
3212 self.calendarPaneOpenedFrom = null;
3213
3214 if (self.openOnFocus) {
3215 // Ensures that all focus events have fired before resetting
3216 // the calendar. Prevents the calendar from reopening immediately
3217 // in IE when md-open-on-focus is set. Also it needs to trigger
3218 // a digest, in order to prevent issues where the calendar wasn't
3219 // showing up on the next open.
3220 self.$timeout(reset);
3221 } else {
3222 reset();
3223 }
3224 }
3225
3226 function reset() {
3227 self.isCalendarOpen = self.isOpen = false;
3228 }
3229 };
3230
3231 /** Gets the controller instance for the calendar in the floating pane. */
3232 DatePickerCtrl.prototype.getCalendarCtrl = function() {
3233 return angular.element(this.calendarPane.querySelector('md-calendar')).controller('mdCalendar');
3234 };
3235
3236 /** Focus the calendar in the floating pane. */
3237 DatePickerCtrl.prototype.focusCalendar = function() {
3238 // Use a timeout in order to allow the calendar to be rendered, as it is gated behind an ng-if.
3239 var self = this;
3240 this.$mdUtil.nextTick(function() {
3241 self.getCalendarCtrl().focusDate(self.date);
3242 }, false);
3243 };
3244
3245 /**
3246 * Sets whether the input is currently focused.
3247 * @param {boolean} isFocused
3248 */
3249 DatePickerCtrl.prototype.setFocused = function(isFocused) {
3250 if (!isFocused) {
3251 this.ngModelCtrl.$setTouched();
3252 }
3253
3254 // The ng* expressions shouldn't be evaluated when mdOpenOnFocus is on,
3255 // because they also get called when the calendar is opened/closed.
3256 if (!this.openOnFocus) {
3257 this.evalAttr(isFocused ? 'ngFocus' : 'ngBlur');
3258 }
3259
3260 this.isFocused = isFocused;
3261 };
3262
3263 /**
3264 * Handles a click on the document body when the floating calendar pane is open.
3265 * Closes the floating calendar pane if the click is not inside of it.
3266 * @param {MouseEvent} event
3267 */
3268 DatePickerCtrl.prototype.handleBodyClick = function(event) {
3269 if (this.isCalendarOpen) {
3270 var isInCalendar = this.$mdUtil.getClosest(event.target, 'md-calendar');
3271
3272 if (!isInCalendar) {
3273 this.closeCalendarPane();
3274 }
3275
3276 this.$scope.$digest();
3277 }
3278 };
3279
3280 /**
3281 * Handles the event when the user navigates away from the current tab. Keeps track of
3282 * whether the input was focused when the event happened, in order to prevent the calendar
3283 * from re-opening.
3284 */
3285 DatePickerCtrl.prototype.handleWindowBlur = function() {
3286 this.inputFocusedOnWindowBlur = document.activeElement === this.inputElement;
3287 };
3288
3289 /**
3290 * Reset the flag inputFocusedOnWindowBlur to default state, to permit user to open calendar
3291 * again when he back to tab with calendar focused.
3292 */
3293 DatePickerCtrl.prototype.resetInputFocused = function() {
3294 this.inputFocusedOnWindowBlur = false;
3295 };
3296
3297 /**
3298 * Evaluates an attribute expression against the parent scope.
3299 * @param {String} attr Name of the attribute to be evaluated.
3300 */
3301 DatePickerCtrl.prototype.evalAttr = function(attr) {
3302 if (this.$attrs[attr]) {
3303 this.$scope.$parent.$eval(this.$attrs[attr]);
3304 }
3305 };
3306
3307 /**
3308 * Sets the ng-model value by first converting the date object into a string. Converting it
3309 * is necessary, in order to pass AngularJS's `input[type="date"]` validations. AngularJS turns
3310 * the value into a Date object afterwards, before setting it on the model.
3311 * @param {Date=} value Date to be set as the model value.
3312 */
3313 DatePickerCtrl.prototype.setModelValue = function(value) {
3314 var timezone = this.$mdUtil.getModelOption(this.ngModelCtrl, 'timezone');
3315 // Using the timezone when the offset is negative (GMT+X) causes the previous day to be
3316 // set as the model value here. This check avoids that.
3317 if (timezone == null || value == null || value.getTimezoneOffset() < 0) {
3318 this.ngModelCtrl.$setViewValue(this.ngDateFilter(value, 'yyyy-MM-dd'), 'default');
3319 } else {
3320 this.ngModelCtrl.$setViewValue(this.ngDateFilter(value, 'yyyy-MM-dd', timezone), 'default');
3321 }
3322 };
3323
3324 /**
3325 * Updates the datepicker when a model change occurred externally.
3326 * @param {Date=} value Value that was set to the model.
3327 */
3328 DatePickerCtrl.prototype.onExternalChange = function(value) {
3329 var self = this;
3330 var timezone = this.$mdUtil.getModelOption(this.ngModelCtrl, 'timezone');
3331
3332 // Update the model used by the calendar.
3333 if (this.dateUtil.isValidDate(value) && timezone != null && value.getTimezoneOffset() >= 0) {
3334 this.date = this.dateUtil.removeLocalTzAndReparseDate(value);
3335 } else {
3336 this.date = value;
3337 }
3338 // Using the timezone when the offset is negative (GMT+X) causes the previous day to be
3339 // used here. This check avoids that.
3340 if (timezone == null || value == null || value.getTimezoneOffset() < 0) {
3341 this.inputElement.value = this.locale.formatDate(value);
3342 } else {
3343 this.inputElement.value = this.locale.formatDate(value, timezone);
3344 }
3345 this.mdInputContainer && this.mdInputContainer.setHasValue(!!value);
3346 this.resizeInputElement();
3347 // This is often called from the $formatters section of the $validators pipeline.
3348 // In that case, we need to delay to let $render and $validate run, so that the checks for
3349 // error state are accurate.
3350 this.$mdUtil.nextTick(function() {self.updateErrorState();}, false, self.$scope);
3351 };
3352})();
3353
3354})(window, window.angular);
Note: See TracBrowser for help on using the repository browser.