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