github.com/abayer/test-infra@v0.0.5/mungegithub/submit-queue/www/angular-material.js (about)

     1  /*!
     2   * Angular Material Design
     3   * https://github.com/angular/material
     4   * @license MIT
     5   * v1.0.7
     6   */
     7  (function( window, angular, undefined ){
     8  "use strict";
     9  
    10  (function(){
    11  "use strict";
    12  
    13  angular.module('ngMaterial', ["ng","ngAnimate","ngAria","material.core","material.core.gestures","material.core.layout","material.core.theming.palette","material.core.theming","material.core.animate","material.components.autocomplete","material.components.backdrop","material.components.button","material.components.bottomSheet","material.components.card","material.components.checkbox","material.components.content","material.components.chips","material.components.datepicker","material.components.dialog","material.components.divider","material.components.fabActions","material.components.fabShared","material.components.fabSpeedDial","material.components.fabToolbar","material.components.fabTrigger","material.components.gridList","material.components.icon","material.components.input","material.components.menu","material.components.list","material.components.menuBar","material.components.progressCircular","material.components.progressLinear","material.components.radioButton","material.components.showHide","material.components.select","material.components.sidenav","material.components.slider","material.components.sticky","material.components.subheader","material.components.swipe","material.components.switch","material.components.tabs","material.components.toast","material.components.toolbar","material.components.tooltip","material.components.virtualRepeat","material.components.whiteframe"]);
    14  })();
    15  (function(){
    16  "use strict";
    17  
    18  /**
    19   * Initialization function that validates environment
    20   * requirements.
    21   */
    22  angular
    23    .module('material.core', [
    24      'ngAnimate',
    25      'material.core.animate',
    26      'material.core.layout',
    27      'material.core.gestures',
    28      'material.core.theming'
    29    ])
    30    .config(MdCoreConfigure)
    31    .run(DetectNgTouch);
    32  
    33  
    34  /**
    35   * Detect if the ng-Touch module is also being used.
    36   * Warn if detected.
    37   * @ngInject
    38   */
    39  function DetectNgTouch($log, $injector) {
    40    if ( $injector.has('$swipe') ) {
    41      var msg = "" +
    42        "You are using the ngTouch module. \n" +
    43        "Angular Material already has mobile click, tap, and swipe support... \n" +
    44        "ngTouch is not supported with Angular Material!";
    45      $log.warn(msg);
    46    }
    47  }
    48  DetectNgTouch.$inject = ["$log", "$injector"];
    49  
    50  /**
    51   * @ngInject
    52   */
    53  function MdCoreConfigure($provide, $mdThemingProvider) {
    54  
    55    $provide.decorator('$$rAF', ["$delegate", rAFDecorator]);
    56  
    57    $mdThemingProvider.theme('default')
    58      .primaryPalette('indigo')
    59      .accentPalette('pink')
    60      .warnPalette('deep-orange')
    61      .backgroundPalette('grey');
    62  }
    63  MdCoreConfigure.$inject = ["$provide", "$mdThemingProvider"];
    64  
    65  /**
    66   * @ngInject
    67   */
    68  function rAFDecorator($delegate) {
    69    /**
    70     * Use this to throttle events that come in often.
    71     * The throttled function will always use the *last* invocation before the
    72     * coming frame.
    73     *
    74     * For example, window resize events that fire many times a second:
    75     * If we set to use an raf-throttled callback on window resize, then
    76     * our callback will only be fired once per frame, with the last resize
    77     * event that happened before that frame.
    78     *
    79     * @param {function} callback function to debounce
    80     */
    81    $delegate.throttle = function(cb) {
    82      var queuedArgs, alreadyQueued, queueCb, context;
    83      return function debounced() {
    84        queuedArgs = arguments;
    85        context = this;
    86        queueCb = cb;
    87        if (!alreadyQueued) {
    88          alreadyQueued = true;
    89          $delegate(function() {
    90            queueCb.apply(context, Array.prototype.slice.call(queuedArgs));
    91            alreadyQueued = false;
    92          });
    93        }
    94      };
    95    };
    96    return $delegate;
    97  }
    98  rAFDecorator.$inject = ["$delegate"];
    99  
   100  })();
   101  (function(){
   102  "use strict";
   103  
   104  angular.module('material.core')
   105    .directive('mdAutofocus', MdAutofocusDirective)
   106  
   107    // Support the deprecated md-auto-focus and md-sidenav-focus as well
   108    .directive('mdAutoFocus', MdAutofocusDirective)
   109    .directive('mdSidenavFocus', MdAutofocusDirective);
   110  
   111  /**
   112   * @ngdoc directive
   113   * @name mdAutofocus
   114   * @module material.core.util
   115   *
   116   * @description
   117   *
   118   * `[md-autofocus]` provides an optional way to identify the focused element when a `$mdDialog`,
   119   * `$mdBottomSheet`, or `$mdSidenav` opens or upon page load for input-like elements.
   120   *
   121   * When one of these opens, it will find the first nested element with the `[md-autofocus]`
   122   * attribute directive and optional expression. An expression may be specified as the directive
   123   * value to enable conditional activation of the autofocus.
   124   *
   125   * @usage
   126   *
   127   * ### Dialog
   128   * <hljs lang="html">
   129   * <md-dialog>
   130   *   <form>
   131   *     <md-input-container>
   132   *       <label for="testInput">Label</label>
   133   *       <input id="testInput" type="text" md-autofocus>
   134   *     </md-input-container>
   135   *   </form>
   136   * </md-dialog>
   137   * </hljs>
   138   *
   139   * ### Bottomsheet
   140   * <hljs lang="html">
   141   * <md-bottom-sheet class="md-list md-has-header">
   142   *  <md-subheader>Comment Actions</md-subheader>
   143   *  <md-list>
   144   *    <md-list-item ng-repeat="item in items">
   145   *
   146   *      <md-button md-autofocus="$index == 2">
   147   *        <md-icon md-svg-src="{{item.icon}}"></md-icon>
   148   *        <span class="md-inline-list-icon-label">{{ item.name }}</span>
   149   *      </md-button>
   150   *
   151   *    </md-list-item>
   152   *  </md-list>
   153   * </md-bottom-sheet>
   154   * </hljs>
   155   *
   156   * ### Autocomplete
   157   * <hljs lang="html">
   158   *   <md-autocomplete
   159   *       md-autofocus
   160   *       md-selected-item="selectedItem"
   161   *       md-search-text="searchText"
   162   *       md-items="item in getMatches(searchText)"
   163   *       md-item-text="item.display">
   164   *     <span md-highlight-text="searchText">{{item.display}}</span>
   165   *   </md-autocomplete>
   166   * </hljs>
   167   *
   168   * ### Sidenav
   169   * <hljs lang="html">
   170   * <div layout="row" ng-controller="MyController">
   171   *   <md-sidenav md-component-id="left" class="md-sidenav-left">
   172   *     Left Nav!
   173   *   </md-sidenav>
   174   *
   175   *   <md-content>
   176   *     Center Content
   177   *     <md-button ng-click="openLeftMenu()">
   178   *       Open Left Menu
   179   *     </md-button>
   180   *   </md-content>
   181   *
   182   *   <md-sidenav md-component-id="right"
   183   *     md-is-locked-open="$mdMedia('min-width: 333px')"
   184   *     class="md-sidenav-right">
   185   *     <form>
   186   *       <md-input-container>
   187   *         <label for="testInput">Test input</label>
   188   *         <input id="testInput" type="text"
   189   *                ng-model="data" md-autofocus>
   190   *       </md-input-container>
   191   *     </form>
   192   *   </md-sidenav>
   193   * </div>
   194   * </hljs>
   195   **/
   196  function MdAutofocusDirective() {
   197    return {
   198      restrict: 'A',
   199  
   200      link: postLink
   201    }
   202  }
   203  
   204  function postLink(scope, element, attrs) {
   205    var attr = attrs.mdAutoFocus || attrs.mdAutofocus || attrs.mdSidenavFocus;
   206  
   207    // Setup a watcher on the proper attribute to update a class we can check for in $mdUtil
   208    scope.$watch(attr, function(canAutofocus) {
   209      element.toggleClass('_md-autofocus', canAutofocus);
   210    });
   211  }
   212  
   213  })();
   214  (function(){
   215  "use strict";
   216  
   217  angular.module('material.core')
   218  .factory('$mdConstant', MdConstantFactory);
   219  
   220  /**
   221   * Factory function that creates the grab-bag $mdConstant service.
   222   * @ngInject
   223   */
   224  function MdConstantFactory($sniffer) {
   225  
   226    var webkit = /webkit/i.test($sniffer.vendorPrefix);
   227    function vendorProperty(name) {
   228      return webkit ?  ('webkit' + name.charAt(0).toUpperCase() + name.substring(1)) : name;
   229    }
   230  
   231    return {
   232      KEY_CODE: {
   233        COMMA: 188,
   234        SEMICOLON : 186,
   235        ENTER: 13,
   236        ESCAPE: 27,
   237        SPACE: 32,
   238        PAGE_UP: 33,
   239        PAGE_DOWN: 34,
   240        END: 35,
   241        HOME: 36,
   242        LEFT_ARROW : 37,
   243        UP_ARROW : 38,
   244        RIGHT_ARROW : 39,
   245        DOWN_ARROW : 40,
   246        TAB : 9,
   247        BACKSPACE: 8,
   248        DELETE: 46
   249      },
   250      CSS: {
   251        /* Constants */
   252        TRANSITIONEND: 'transitionend' + (webkit ? ' webkitTransitionEnd' : ''),
   253        ANIMATIONEND: 'animationend' + (webkit ? ' webkitAnimationEnd' : ''),
   254  
   255        TRANSFORM: vendorProperty('transform'),
   256        TRANSFORM_ORIGIN: vendorProperty('transformOrigin'),
   257        TRANSITION: vendorProperty('transition'),
   258        TRANSITION_DURATION: vendorProperty('transitionDuration'),
   259        ANIMATION_PLAY_STATE: vendorProperty('animationPlayState'),
   260        ANIMATION_DURATION: vendorProperty('animationDuration'),
   261        ANIMATION_NAME: vendorProperty('animationName'),
   262        ANIMATION_TIMING: vendorProperty('animationTimingFunction'),
   263        ANIMATION_DIRECTION: vendorProperty('animationDirection')
   264      },
   265      /**
   266       * As defined in core/style/variables.scss
   267       *
   268       * $layout-breakpoint-xs:     600px !default;
   269       * $layout-breakpoint-sm:     960px !default;
   270       * $layout-breakpoint-md:     1280px !default;
   271       * $layout-breakpoint-lg:     1920px !default;
   272       *
   273       */
   274      MEDIA: {
   275        'xs'    : '(max-width: 599px)'                         ,
   276        'gt-xs' : '(min-width: 600px)'                         ,
   277        'sm'    : '(min-width: 600px) and (max-width: 959px)'  ,
   278        'gt-sm' : '(min-width: 960px)'                         ,
   279        'md'    : '(min-width: 960px) and (max-width: 1279px)' ,
   280        'gt-md' : '(min-width: 1280px)'                        ,
   281        'lg'    : '(min-width: 1280px) and (max-width: 1919px)',
   282        'gt-lg' : '(min-width: 1920px)'                        ,
   283        'xl'    : '(min-width: 1920px)'                        ,
   284        'print' : 'print'
   285      },
   286      MEDIA_PRIORITY: [
   287        'xl',
   288        'gt-lg',
   289        'lg',
   290        'gt-md',
   291        'md',
   292        'gt-sm',
   293        'sm',
   294        'gt-xs',
   295        'xs',
   296        'print'
   297      ]
   298    };
   299  }
   300  MdConstantFactory.$inject = ["$sniffer"];
   301  
   302  })();
   303  (function(){
   304  "use strict";
   305  
   306    angular
   307      .module('material.core')
   308      .config( ["$provide", function($provide){
   309         $provide.decorator('$mdUtil', ['$delegate', function ($delegate){
   310             /**
   311              * Inject the iterator facade to easily support iteration and accessors
   312              * @see iterator below
   313              */
   314             $delegate.iterator = MdIterator;
   315  
   316             return $delegate;
   317           }
   318         ]);
   319       }]);
   320  
   321    /**
   322     * iterator is a list facade to easily support iteration and accessors
   323     *
   324     * @param items Array list which this iterator will enumerate
   325     * @param reloop Boolean enables iterator to consider the list as an endless reloop
   326     */
   327    function MdIterator(items, reloop) {
   328      var trueFn = function() { return true; };
   329  
   330      if (items && !angular.isArray(items)) {
   331        items = Array.prototype.slice.call(items);
   332      }
   333  
   334      reloop = !!reloop;
   335      var _items = items || [ ];
   336  
   337      // Published API
   338      return {
   339        items: getItems,
   340        count: count,
   341  
   342        inRange: inRange,
   343        contains: contains,
   344        indexOf: indexOf,
   345        itemAt: itemAt,
   346  
   347        findBy: findBy,
   348  
   349        add: add,
   350        remove: remove,
   351  
   352        first: first,
   353        last: last,
   354        next: angular.bind(null, findSubsequentItem, false),
   355        previous: angular.bind(null, findSubsequentItem, true),
   356  
   357        hasPrevious: hasPrevious,
   358        hasNext: hasNext
   359  
   360      };
   361  
   362      /**
   363       * Publish copy of the enumerable set
   364       * @returns {Array|*}
   365       */
   366      function getItems() {
   367        return [].concat(_items);
   368      }
   369  
   370      /**
   371       * Determine length of the list
   372       * @returns {Array.length|*|number}
   373       */
   374      function count() {
   375        return _items.length;
   376      }
   377  
   378      /**
   379       * Is the index specified valid
   380       * @param index
   381       * @returns {Array.length|*|number|boolean}
   382       */
   383      function inRange(index) {
   384        return _items.length && ( index > -1 ) && (index < _items.length );
   385      }
   386  
   387      /**
   388       * Can the iterator proceed to the next item in the list; relative to
   389       * the specified item.
   390       *
   391       * @param item
   392       * @returns {Array.length|*|number|boolean}
   393       */
   394      function hasNext(item) {
   395        return item ? inRange(indexOf(item) + 1) : false;
   396      }
   397  
   398      /**
   399       * Can the iterator proceed to the previous item in the list; relative to
   400       * the specified item.
   401       *
   402       * @param item
   403       * @returns {Array.length|*|number|boolean}
   404       */
   405      function hasPrevious(item) {
   406        return item ? inRange(indexOf(item) - 1) : false;
   407      }
   408  
   409      /**
   410       * Get item at specified index/position
   411       * @param index
   412       * @returns {*}
   413       */
   414      function itemAt(index) {
   415        return inRange(index) ? _items[index] : null;
   416      }
   417  
   418      /**
   419       * Find all elements matching the key/value pair
   420       * otherwise return null
   421       *
   422       * @param val
   423       * @param key
   424       *
   425       * @return array
   426       */
   427      function findBy(key, val) {
   428        return _items.filter(function(item) {
   429          return item[key] === val;
   430        });
   431      }
   432  
   433      /**
   434       * Add item to list
   435       * @param item
   436       * @param index
   437       * @returns {*}
   438       */
   439      function add(item, index) {
   440        if ( !item ) return -1;
   441  
   442        if (!angular.isNumber(index)) {
   443          index = _items.length;
   444        }
   445  
   446        _items.splice(index, 0, item);
   447  
   448        return indexOf(item);
   449      }
   450  
   451      /**
   452       * Remove item from list...
   453       * @param item
   454       */
   455      function remove(item) {
   456        if ( contains(item) ){
   457          _items.splice(indexOf(item), 1);
   458        }
   459      }
   460  
   461      /**
   462       * Get the zero-based index of the target item
   463       * @param item
   464       * @returns {*}
   465       */
   466      function indexOf(item) {
   467        return _items.indexOf(item);
   468      }
   469  
   470      /**
   471       * Boolean existence check
   472       * @param item
   473       * @returns {boolean}
   474       */
   475      function contains(item) {
   476        return item && (indexOf(item) > -1);
   477      }
   478  
   479      /**
   480       * Return first item in the list
   481       * @returns {*}
   482       */
   483      function first() {
   484        return _items.length ? _items[0] : null;
   485      }
   486  
   487      /**
   488       * Return last item in the list...
   489       * @returns {*}
   490       */
   491      function last() {
   492        return _items.length ? _items[_items.length - 1] : null;
   493      }
   494  
   495      /**
   496       * Find the next item. If reloop is true and at the end of the list, it will go back to the
   497       * first item. If given, the `validate` callback will be used to determine whether the next item
   498       * is valid. If not valid, it will try to find the next item again.
   499       *
   500       * @param {boolean} backwards Specifies the direction of searching (forwards/backwards)
   501       * @param {*} item The item whose subsequent item we are looking for
   502       * @param {Function=} validate The `validate` function
   503       * @param {integer=} limit The recursion limit
   504       *
   505       * @returns {*} The subsequent item or null
   506       */
   507      function findSubsequentItem(backwards, item, validate, limit) {
   508        validate = validate || trueFn;
   509  
   510        var curIndex = indexOf(item);
   511        while (true) {
   512          if (!inRange(curIndex)) return null;
   513  
   514          var nextIndex = curIndex + (backwards ? -1 : 1);
   515          var foundItem = null;
   516          if (inRange(nextIndex)) {
   517            foundItem = _items[nextIndex];
   518          } else if (reloop) {
   519            foundItem = backwards ? last() : first();
   520            nextIndex = indexOf(foundItem);
   521          }
   522  
   523          if ((foundItem === null) || (nextIndex === limit)) return null;
   524          if (validate(foundItem)) return foundItem;
   525  
   526          if (angular.isUndefined(limit)) limit = nextIndex;
   527  
   528          curIndex = nextIndex;
   529        }
   530      }
   531    }
   532  
   533  
   534  })();
   535  (function(){
   536  "use strict";
   537  
   538  angular.module('material.core')
   539  .factory('$mdMedia', mdMediaFactory);
   540  
   541  /**
   542   * @ngdoc service
   543   * @name $mdMedia
   544   * @module material.core
   545   *
   546   * @description
   547   * `$mdMedia` is used to evaluate whether a given media query is true or false given the
   548   * current device's screen / window size. The media query will be re-evaluated on resize, allowing
   549   * you to register a watch.
   550   *
   551   * `$mdMedia` also has pre-programmed support for media queries that match the layout breakpoints:
   552   *
   553   *  <table class="md-api-table">
   554   *    <thead>
   555   *    <tr>
   556   *      <th>Breakpoint</th>
   557   *      <th>mediaQuery</th>
   558   *    </tr>
   559   *    </thead>
   560   *    <tbody>
   561   *    <tr>
   562   *      <td>xs</td>
   563   *      <td>(max-width: 599px)</td>
   564   *    </tr>
   565   *    <tr>
   566   *      <td>gt-xs</td>
   567   *      <td>(min-width: 600px)</td>
   568   *    </tr>
   569   *    <tr>
   570   *      <td>sm</td>
   571   *      <td>(min-width: 600px) and (max-width: 959px)</td>
   572   *    </tr>
   573   *    <tr>
   574   *      <td>gt-sm</td>
   575   *      <td>(min-width: 960px)</td>
   576   *    </tr>
   577   *    <tr>
   578   *      <td>md</td>
   579   *      <td>(min-width: 960px) and (max-width: 1279px)</td>
   580   *    </tr>
   581   *    <tr>
   582   *      <td>gt-md</td>
   583   *      <td>(min-width: 1280px)</td>
   584   *    </tr>
   585   *    <tr>
   586   *      <td>lg</td>
   587   *      <td>(min-width: 1280px) and (max-width: 1919px)</td>
   588   *    </tr>
   589   *    <tr>
   590   *      <td>gt-lg</td>
   591   *      <td>(min-width: 1920px)</td>
   592   *    </tr>
   593   *    <tr>
   594   *      <td>xl</td>
   595   *      <td>(min-width: 1920px)</td>
   596   *    </tr>
   597   *    <tr>
   598   *      <td>print</td>
   599   *      <td>print</td>
   600   *    </tr>
   601   *    </tbody>
   602   *  </table>
   603   *
   604   *  See Material Design's <a href="https://www.google.com/design/spec/layout/adaptive-ui.html">Layout - Adaptive UI</a> for more details.
   605   *
   606   *  <a href="https://www.google.com/design/spec/layout/adaptive-ui.html">
   607   *  <img src="https://material-design.storage.googleapis.com/publish/material_v_4/material_ext_publish/0B8olV15J7abPSGFxemFiQVRtb1k/layout_adaptive_breakpoints_01.png" width="100%" height="100%"></img>
   608   *  </a>
   609   *
   610   * @returns {boolean} a boolean representing whether or not the given media query is true or false.
   611   *
   612   * @usage
   613   * <hljs lang="js">
   614   * app.controller('MyController', function($mdMedia, $scope) {
   615   *   $scope.$watch(function() { return $mdMedia('lg'); }, function(big) {
   616   *     $scope.bigScreen = big;
   617   *   });
   618   *
   619   *   $scope.screenIsSmall = $mdMedia('sm');
   620   *   $scope.customQuery = $mdMedia('(min-width: 1234px)');
   621   *   $scope.anotherCustom = $mdMedia('max-width: 300px');
   622   * });
   623   * </hljs>
   624   * @ngInject
   625   */
   626  
   627  function mdMediaFactory($mdConstant, $rootScope, $window) {
   628    var queries = {};
   629    var mqls = {};
   630    var results = {};
   631    var normalizeCache = {};
   632  
   633    $mdMedia.getResponsiveAttribute = getResponsiveAttribute;
   634    $mdMedia.getQuery = getQuery;
   635    $mdMedia.watchResponsiveAttributes = watchResponsiveAttributes;
   636  
   637    return $mdMedia;
   638  
   639    function $mdMedia(query) {
   640      var validated = queries[query];
   641      if (angular.isUndefined(validated)) {
   642        validated = queries[query] = validate(query);
   643      }
   644  
   645      var result = results[validated];
   646      if (angular.isUndefined(result)) {
   647        result = add(validated);
   648      }
   649  
   650      return result;
   651    }
   652  
   653    function validate(query) {
   654      return $mdConstant.MEDIA[query] ||
   655             ((query.charAt(0) !== '(') ? ('(' + query + ')') : query);
   656    }
   657  
   658    function add(query) {
   659      var result = mqls[query];
   660      if ( !result ) {
   661        result = mqls[query] = $window.matchMedia(query);
   662      }
   663  
   664      result.addListener(onQueryChange);
   665      return (results[result.media] = !!result.matches);
   666    }
   667  
   668    function onQueryChange(query) {
   669      $rootScope.$evalAsync(function() {
   670        results[query.media] = !!query.matches;
   671      });
   672    }
   673  
   674    function getQuery(name) {
   675      return mqls[name];
   676    }
   677  
   678    function getResponsiveAttribute(attrs, attrName) {
   679      for (var i = 0; i < $mdConstant.MEDIA_PRIORITY.length; i++) {
   680        var mediaName = $mdConstant.MEDIA_PRIORITY[i];
   681        if (!mqls[queries[mediaName]].matches) {
   682          continue;
   683        }
   684  
   685        var normalizedName = getNormalizedName(attrs, attrName + '-' + mediaName);
   686        if (attrs[normalizedName]) {
   687          return attrs[normalizedName];
   688        }
   689      }
   690  
   691      // fallback on unprefixed
   692      return attrs[getNormalizedName(attrs, attrName)];
   693    }
   694  
   695    function watchResponsiveAttributes(attrNames, attrs, watchFn) {
   696      var unwatchFns = [];
   697      attrNames.forEach(function(attrName) {
   698        var normalizedName = getNormalizedName(attrs, attrName);
   699        if (angular.isDefined(attrs[normalizedName])) {
   700          unwatchFns.push(
   701              attrs.$observe(normalizedName, angular.bind(void 0, watchFn, null)));
   702        }
   703  
   704        for (var mediaName in $mdConstant.MEDIA) {
   705          normalizedName = getNormalizedName(attrs, attrName + '-' + mediaName);
   706          if (angular.isDefined(attrs[normalizedName])) {
   707            unwatchFns.push(
   708                attrs.$observe(normalizedName, angular.bind(void 0, watchFn, mediaName)));
   709          }
   710        }
   711      });
   712  
   713      return function unwatch() {
   714        unwatchFns.forEach(function(fn) { fn(); })
   715      };
   716    }
   717  
   718    // Improves performance dramatically
   719    function getNormalizedName(attrs, attrName) {
   720      return normalizeCache[attrName] ||
   721          (normalizeCache[attrName] = attrs.$normalize(attrName));
   722    }
   723  }
   724  mdMediaFactory.$inject = ["$mdConstant", "$rootScope", "$window"];
   725  
   726  })();
   727  (function(){
   728  "use strict";
   729  
   730  /*
   731   * This var has to be outside the angular factory, otherwise when
   732   * there are multiple material apps on the same page, each app
   733   * will create its own instance of this array and the app's IDs
   734   * will not be unique.
   735   */
   736  var nextUniqueId = 0;
   737  
   738  /**
   739   * @ngdoc module
   740   * @name material.core.util
   741   * @description
   742   * Util
   743   */
   744  angular
   745    .module('material.core')
   746    .factory('$mdUtil', UtilFactory);
   747  
   748  /**
   749   * @ngInject
   750   */
   751  function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $interpolate, $log, $rootElement, $window) {
   752    // Setup some core variables for the processTemplate method
   753    var startSymbol = $interpolate.startSymbol(),
   754      endSymbol = $interpolate.endSymbol(),
   755      usesStandardSymbols = ((startSymbol === '{{') && (endSymbol === '}}'));
   756  
   757    /**
   758     * Checks if the target element has the requested style by key
   759     * @param {DOMElement|JQLite} target Target element
   760     * @param {string} key Style key
   761     * @param {string=} expectedVal Optional expected value
   762     * @returns {boolean} Whether the target element has the style or not
   763     */
   764    var hasComputedStyle = function (target, key, expectedVal) {
   765      var hasValue = false;
   766  
   767      if ( target && target.length  ) {
   768        var computedStyles = $window.getComputedStyle(target[0]);
   769        hasValue = angular.isDefined(computedStyles[key]) && (expectedVal ? computedStyles[key] == expectedVal : true);
   770      }
   771  
   772      return hasValue;
   773    };
   774  
   775    var $mdUtil = {
   776      dom: {},
   777      now: window.performance ?
   778        angular.bind(window.performance, window.performance.now) : Date.now || function() {
   779        return new Date().getTime();
   780      },
   781  
   782      clientRect: function(element, offsetParent, isOffsetRect) {
   783        var node = getNode(element);
   784        offsetParent = getNode(offsetParent || node.offsetParent || document.body);
   785        var nodeRect = node.getBoundingClientRect();
   786  
   787        // The user can ask for an offsetRect: a rect relative to the offsetParent,
   788        // or a clientRect: a rect relative to the page
   789        var offsetRect = isOffsetRect ?
   790          offsetParent.getBoundingClientRect() :
   791        {left: 0, top: 0, width: 0, height: 0};
   792        return {
   793          left: nodeRect.left - offsetRect.left,
   794          top: nodeRect.top - offsetRect.top,
   795          width: nodeRect.width,
   796          height: nodeRect.height
   797        };
   798      },
   799      offsetRect: function(element, offsetParent) {
   800        return $mdUtil.clientRect(element, offsetParent, true);
   801      },
   802  
   803      // Annoying method to copy nodes to an array, thanks to IE
   804      nodesToArray: function(nodes) {
   805        nodes = nodes || [];
   806  
   807        var results = [];
   808        for (var i = 0; i < nodes.length; ++i) {
   809          results.push(nodes.item(i));
   810        }
   811        return results;
   812      },
   813  
   814      /**
   815       * Calculate the positive scroll offset
   816       * TODO: Check with pinch-zoom in IE/Chrome;
   817       *       https://code.google.com/p/chromium/issues/detail?id=496285
   818       */
   819      scrollTop: function(element) {
   820        element = angular.element(element || $document[0].body);
   821  
   822        var body = (element[0] == $document[0].body) ? $document[0].body : undefined;
   823        var scrollTop = body ? body.scrollTop + body.parentElement.scrollTop : 0;
   824  
   825        // Calculate the positive scroll offset
   826        return scrollTop || Math.abs(element[0].getBoundingClientRect().top);
   827      },
   828  
   829      /**
   830       * Finds the proper focus target by searching the DOM.
   831       *
   832       * @param containerEl
   833       * @param attributeVal
   834       * @returns {*}
   835       */
   836      findFocusTarget: function(containerEl, attributeVal) {
   837        var AUTO_FOCUS = '[md-autofocus]';
   838        var elToFocus;
   839  
   840        elToFocus = scanForFocusable(containerEl, attributeVal || AUTO_FOCUS);
   841  
   842        if ( !elToFocus && attributeVal != AUTO_FOCUS) {
   843          // Scan for deprecated attribute
   844          elToFocus = scanForFocusable(containerEl, '[md-auto-focus]');
   845  
   846          if ( !elToFocus ) {
   847            // Scan for fallback to 'universal' API
   848            elToFocus = scanForFocusable(containerEl, AUTO_FOCUS);
   849          }
   850        }
   851  
   852        return elToFocus;
   853  
   854        /**
   855         * Can target and nested children for specified Selector (attribute)
   856         * whose value may be an expression that evaluates to True/False.
   857         */
   858        function scanForFocusable(target, selector) {
   859          var elFound, items = target[0].querySelectorAll(selector);
   860  
   861          // Find the last child element with the focus attribute
   862          if ( items && items.length ){
   863            items.length && angular.forEach(items, function(it) {
   864              it = angular.element(it);
   865  
   866              // Check the element for the _md-autofocus class to ensure any associated expression
   867              // evaluated to true.
   868              var isFocusable = it.hasClass('_md-autofocus');
   869              if (isFocusable) elFound = it;
   870            });
   871          }
   872          return elFound;
   873        }
   874      },
   875  
   876      // Disables scroll around the passed element.
   877      disableScrollAround: function(element, parent) {
   878        $mdUtil.disableScrollAround._count = $mdUtil.disableScrollAround._count || 0;
   879        ++$mdUtil.disableScrollAround._count;
   880        if ($mdUtil.disableScrollAround._enableScrolling) return $mdUtil.disableScrollAround._enableScrolling;
   881        element = angular.element(element);
   882        var body = $document[0].body,
   883          restoreBody = disableBodyScroll(),
   884          restoreElement = disableElementScroll(parent);
   885  
   886        return $mdUtil.disableScrollAround._enableScrolling = function() {
   887          if (!--$mdUtil.disableScrollAround._count) {
   888            restoreBody();
   889            restoreElement();
   890            delete $mdUtil.disableScrollAround._enableScrolling;
   891          }
   892        };
   893  
   894        // Creates a virtual scrolling mask to absorb touchmove, keyboard, scrollbar clicking, and wheel events
   895        function disableElementScroll(element) {
   896          element = angular.element(element || body)[0];
   897          var zIndex = 50;
   898          var scrollMask = angular.element(
   899            '<div class="md-scroll-mask">' +
   900            '  <div class="md-scroll-mask-bar"></div>' +
   901            '</div>').css('z-index', zIndex);
   902          element.appendChild(scrollMask[0]);
   903  
   904          scrollMask.on('wheel', preventDefault);
   905          scrollMask.on('touchmove', preventDefault);
   906  
   907          return function restoreScroll() {
   908            scrollMask.off('wheel');
   909            scrollMask.off('touchmove');
   910            scrollMask[0].parentNode.removeChild(scrollMask[0]);
   911            delete $mdUtil.disableScrollAround._enableScrolling;
   912          };
   913  
   914          function preventDefault(e) {
   915            e.preventDefault();
   916          }
   917        }
   918  
   919        // Converts the body to a position fixed block and translate it to the proper scroll
   920        // position
   921        function disableBodyScroll() {
   922          var htmlNode = body.parentNode;
   923          var restoreHtmlStyle = htmlNode.style.cssText || '';
   924          var restoreBodyStyle = body.style.cssText || '';
   925          var scrollOffset = $mdUtil.scrollTop(body);
   926          var clientWidth = body.clientWidth;
   927  
   928          if (body.scrollHeight > body.clientHeight + 1) {
   929            applyStyles(body, {
   930              position: 'fixed',
   931              width: '100%',
   932              top: -scrollOffset + 'px'
   933            });
   934  
   935            applyStyles(htmlNode, {
   936              overflowY: 'scroll'
   937            });
   938          }
   939  
   940          if (body.clientWidth < clientWidth) applyStyles(body, {overflow: 'hidden'});
   941  
   942          return function restoreScroll() {
   943            body.style.cssText = restoreBodyStyle;
   944            htmlNode.style.cssText = restoreHtmlStyle;
   945            body.scrollTop = scrollOffset;
   946            htmlNode.scrollTop = scrollOffset;
   947          };
   948        }
   949  
   950        function applyStyles(el, styles) {
   951          for (var key in styles) {
   952            el.style[key] = styles[key];
   953          }
   954        }
   955      },
   956      enableScrolling: function() {
   957        var method = this.disableScrollAround._enableScrolling;
   958        method && method();
   959      },
   960      floatingScrollbars: function() {
   961        if (this.floatingScrollbars.cached === undefined) {
   962          var tempNode = angular.element('<div><div></div></div>').css({
   963            width: '100%',
   964            'z-index': -1,
   965            position: 'absolute',
   966            height: '35px',
   967            'overflow-y': 'scroll'
   968          });
   969          tempNode.children().css('height', '60px');
   970  
   971          $document[0].body.appendChild(tempNode[0]);
   972          this.floatingScrollbars.cached = (tempNode[0].offsetWidth == tempNode[0].childNodes[0].offsetWidth);
   973          tempNode.remove();
   974        }
   975        return this.floatingScrollbars.cached;
   976      },
   977  
   978      // Mobile safari only allows you to set focus in click event listeners...
   979      forceFocus: function(element) {
   980        var node = element[0] || element;
   981  
   982        document.addEventListener('click', function focusOnClick(ev) {
   983          if (ev.target === node && ev.$focus) {
   984            node.focus();
   985            ev.stopImmediatePropagation();
   986            ev.preventDefault();
   987            node.removeEventListener('click', focusOnClick);
   988          }
   989        }, true);
   990  
   991        var newEvent = document.createEvent('MouseEvents');
   992        newEvent.initMouseEvent('click', false, true, window, {}, 0, 0, 0, 0,
   993          false, false, false, false, 0, null);
   994        newEvent.$material = true;
   995        newEvent.$focus = true;
   996        node.dispatchEvent(newEvent);
   997      },
   998  
   999      /**
  1000       * facade to build md-backdrop element with desired styles
  1001       * NOTE: Use $compile to trigger backdrop postLink function
  1002       */
  1003      createBackdrop: function(scope, addClass) {
  1004        return $compile($mdUtil.supplant('<md-backdrop class="{0}">', [addClass]))(scope);
  1005      },
  1006  
  1007      /**
  1008       * supplant() method from Crockford's `Remedial Javascript`
  1009       * Equivalent to use of $interpolate; without dependency on
  1010       * interpolation symbols and scope. Note: the '{<token>}' can
  1011       * be property names, property chains, or array indices.
  1012       */
  1013      supplant: function(template, values, pattern) {
  1014        pattern = pattern || /\{([^\{\}]*)\}/g;
  1015        return template.replace(pattern, function(a, b) {
  1016          var p = b.split('.'),
  1017            r = values;
  1018          try {
  1019            for (var s in p) {
  1020              if (p.hasOwnProperty(s) ) {
  1021                r = r[p[s]];
  1022              }
  1023            }
  1024          } catch (e) {
  1025            r = a;
  1026          }
  1027          return (typeof r === 'string' || typeof r === 'number') ? r : a;
  1028        });
  1029      },
  1030  
  1031      fakeNgModel: function() {
  1032        return {
  1033          $fake: true,
  1034          $setTouched: angular.noop,
  1035          $setViewValue: function(value) {
  1036            this.$viewValue = value;
  1037            this.$render(value);
  1038            this.$viewChangeListeners.forEach(function(cb) {
  1039              cb();
  1040            });
  1041          },
  1042          $isEmpty: function(value) {
  1043            return ('' + value).length === 0;
  1044          },
  1045          $parsers: [],
  1046          $formatters: [],
  1047          $viewChangeListeners: [],
  1048          $render: angular.noop
  1049        };
  1050      },
  1051  
  1052      // Returns a function, that, as long as it continues to be invoked, will not
  1053      // be triggered. The function will be called after it stops being called for
  1054      // N milliseconds.
  1055      // @param wait Integer value of msecs to delay (since last debounce reset); default value 10 msecs
  1056      // @param invokeApply should the $timeout trigger $digest() dirty checking
  1057      debounce: function(func, wait, scope, invokeApply) {
  1058        var timer;
  1059  
  1060        return function debounced() {
  1061          var context = scope,
  1062            args = Array.prototype.slice.call(arguments);
  1063  
  1064          $timeout.cancel(timer);
  1065          timer = $timeout(function() {
  1066  
  1067            timer = undefined;
  1068            func.apply(context, args);
  1069  
  1070          }, wait || 10, invokeApply);
  1071        };
  1072      },
  1073  
  1074      // Returns a function that can only be triggered every `delay` milliseconds.
  1075      // In other words, the function will not be called unless it has been more
  1076      // than `delay` milliseconds since the last call.
  1077      throttle: function throttle(func, delay) {
  1078        var recent;
  1079        return function throttled() {
  1080          var context = this;
  1081          var args = arguments;
  1082          var now = $mdUtil.now();
  1083  
  1084          if (!recent || (now - recent > delay)) {
  1085            func.apply(context, args);
  1086            recent = now;
  1087          }
  1088        };
  1089      },
  1090  
  1091      /**
  1092       * Measures the number of milliseconds taken to run the provided callback
  1093       * function. Uses a high-precision timer if available.
  1094       */
  1095      time: function time(cb) {
  1096        var start = $mdUtil.now();
  1097        cb();
  1098        return $mdUtil.now() - start;
  1099      },
  1100  
  1101      /**
  1102       * Create an implicit getter that caches its `getter()`
  1103       * lookup value
  1104       */
  1105      valueOnUse : function (scope, key, getter) {
  1106        var value = null, args = Array.prototype.slice.call(arguments);
  1107        var params = (args.length > 3) ? args.slice(3) : [ ];
  1108  
  1109        Object.defineProperty(scope, key, {
  1110          get: function () {
  1111            if (value === null) value = getter.apply(scope, params);
  1112            return value;
  1113          }
  1114        });
  1115      },
  1116  
  1117      /**
  1118       * Get a unique ID.
  1119       *
  1120       * @returns {string} an unique numeric string
  1121       */
  1122      nextUid: function() {
  1123        return '' + nextUniqueId++;
  1124      },
  1125  
  1126      // Stop watchers and events from firing on a scope without destroying it,
  1127      // by disconnecting it from its parent and its siblings' linked lists.
  1128      disconnectScope: function disconnectScope(scope) {
  1129        if (!scope) return;
  1130  
  1131        // we can't destroy the root scope or a scope that has been already destroyed
  1132        if (scope.$root === scope) return;
  1133        if (scope.$$destroyed) return;
  1134  
  1135        var parent = scope.$parent;
  1136        scope.$$disconnected = true;
  1137  
  1138        // See Scope.$destroy
  1139        if (parent.$$childHead === scope) parent.$$childHead = scope.$$nextSibling;
  1140        if (parent.$$childTail === scope) parent.$$childTail = scope.$$prevSibling;
  1141        if (scope.$$prevSibling) scope.$$prevSibling.$$nextSibling = scope.$$nextSibling;
  1142        if (scope.$$nextSibling) scope.$$nextSibling.$$prevSibling = scope.$$prevSibling;
  1143  
  1144        scope.$$nextSibling = scope.$$prevSibling = null;
  1145  
  1146      },
  1147  
  1148      // Undo the effects of disconnectScope above.
  1149      reconnectScope: function reconnectScope(scope) {
  1150        if (!scope) return;
  1151  
  1152        // we can't disconnect the root node or scope already disconnected
  1153        if (scope.$root === scope) return;
  1154        if (!scope.$$disconnected) return;
  1155  
  1156        var child = scope;
  1157  
  1158        var parent = child.$parent;
  1159        child.$$disconnected = false;
  1160        // See Scope.$new for this logic...
  1161        child.$$prevSibling = parent.$$childTail;
  1162        if (parent.$$childHead) {
  1163          parent.$$childTail.$$nextSibling = child;
  1164          parent.$$childTail = child;
  1165        } else {
  1166          parent.$$childHead = parent.$$childTail = child;
  1167        }
  1168      },
  1169  
  1170      /*
  1171       * getClosest replicates jQuery.closest() to walk up the DOM tree until it finds a matching nodeName
  1172       *
  1173       * @param el Element to start walking the DOM from
  1174       * @param tagName Tag name to find closest to el, such as 'form'
  1175       * @param onlyParent Only start checking from the parent element, not `el`.
  1176       */
  1177      getClosest: function getClosest(el, tagName, onlyParent) {
  1178        if (el instanceof angular.element) el = el[0];
  1179        tagName = tagName.toUpperCase();
  1180        if (onlyParent) el = el.parentNode;
  1181        if (!el) return null;
  1182        do {
  1183          if (el.nodeName === tagName) {
  1184            return el;
  1185          }
  1186        } while (el = el.parentNode);
  1187        return null;
  1188      },
  1189  
  1190      /**
  1191       * Build polyfill for the Node.contains feature (if needed)
  1192       */
  1193      elementContains: function(node, child) {
  1194        var hasContains = (window.Node && window.Node.prototype && Node.prototype.contains);
  1195        var findFn = hasContains ? angular.bind(node, node.contains) : angular.bind(node, function(arg) {
  1196          // compares the positions of two nodes and returns a bitmask
  1197          return (node === child) || !!(this.compareDocumentPosition(arg) & 16)
  1198        });
  1199  
  1200        return findFn(child);
  1201      },
  1202  
  1203      /**
  1204       * Functional equivalent for $element.filter(‘md-bottom-sheet’)
  1205       * useful with interimElements where the element and its container are important...
  1206       *
  1207       * @param {[]} elements to scan
  1208       * @param {string} name of node to find (e.g. 'md-dialog')
  1209       * @param {boolean=} optional flag to allow deep scans; defaults to 'false'.
  1210       * @param {boolean=} optional flag to enable log warnings; defaults to false
  1211       */
  1212      extractElementByName: function(element, nodeName, scanDeep, warnNotFound) {
  1213        var found = scanTree(element);
  1214        if (!found && !!warnNotFound) {
  1215          $log.warn( $mdUtil.supplant("Unable to find node '{0}' in element '{1}'.",[nodeName, element[0].outerHTML]) );
  1216        }
  1217  
  1218        return angular.element(found || element);
  1219  
  1220        /**
  1221         * Breadth-First tree scan for element with matching `nodeName`
  1222         */
  1223        function scanTree(element) {
  1224          return scanLevel(element) || (!!scanDeep ? scanChildren(element) : null);
  1225        }
  1226  
  1227        /**
  1228         * Case-insensitive scan of current elements only (do not descend).
  1229         */
  1230        function scanLevel(element) {
  1231          if ( element ) {
  1232            for (var i = 0, len = element.length; i < len; i++) {
  1233              if (element[i].nodeName.toLowerCase() === nodeName) {
  1234                return element[i];
  1235              }
  1236            }
  1237          }
  1238          return null;
  1239        }
  1240  
  1241        /**
  1242         * Scan children of specified node
  1243         */
  1244        function scanChildren(element) {
  1245          var found;
  1246          if ( element ) {
  1247            for (var i = 0, len = element.length; i < len; i++) {
  1248              var target = element[i];
  1249              if ( !found ) {
  1250                for (var j = 0, numChild = target.childNodes.length; j < numChild; j++) {
  1251                  found = found || scanTree([target.childNodes[j]]);
  1252                }
  1253              }
  1254            }
  1255          }
  1256          return found;
  1257        }
  1258  
  1259      },
  1260  
  1261      /**
  1262       * Give optional properties with no value a boolean true if attr provided or false otherwise
  1263       */
  1264      initOptionalProperties: function(scope, attr, defaults) {
  1265        defaults = defaults || {};
  1266        angular.forEach(scope.$$isolateBindings, function(binding, key) {
  1267          if (binding.optional && angular.isUndefined(scope[key])) {
  1268            var attrIsDefined = angular.isDefined(attr[binding.attrName]);
  1269            scope[key] = angular.isDefined(defaults[key]) ? defaults[key] : attrIsDefined;
  1270          }
  1271        });
  1272      },
  1273  
  1274      /**
  1275       * Alternative to $timeout calls with 0 delay.
  1276       * nextTick() coalesces all calls within a single frame
  1277       * to minimize $digest thrashing
  1278       *
  1279       * @param callback
  1280       * @param digest
  1281       * @returns {*}
  1282       */
  1283      nextTick: function(callback, digest, scope) {
  1284        //-- grab function reference for storing state details
  1285        var nextTick = $mdUtil.nextTick;
  1286        var timeout = nextTick.timeout;
  1287        var queue = nextTick.queue || [];
  1288  
  1289        //-- add callback to the queue
  1290        queue.push(callback);
  1291  
  1292        //-- set default value for digest
  1293        if (digest == null) digest = true;
  1294  
  1295        //-- store updated digest/queue values
  1296        nextTick.digest = nextTick.digest || digest;
  1297        nextTick.queue = queue;
  1298  
  1299        //-- either return existing timeout or create a new one
  1300        return timeout || (nextTick.timeout = $timeout(processQueue, 0, false));
  1301  
  1302        /**
  1303         * Grab a copy of the current queue
  1304         * Clear the queue for future use
  1305         * Process the existing queue
  1306         * Trigger digest if necessary
  1307         */
  1308        function processQueue() {
  1309          var skip = scope && scope.$$destroyed;
  1310          var queue = !skip ? nextTick.queue : [];
  1311          var digest = !skip ? nextTick.digest : null;
  1312  
  1313          nextTick.queue = [];
  1314          nextTick.timeout = null;
  1315          nextTick.digest = false;
  1316  
  1317          queue.forEach(function(callback) {
  1318            callback();
  1319          });
  1320  
  1321          if (digest) $rootScope.$digest();
  1322        }
  1323      },
  1324  
  1325      /**
  1326       * Processes a template and replaces the start/end symbols if the application has
  1327       * overridden them.
  1328       *
  1329       * @param template The template to process whose start/end tags may be replaced.
  1330       * @returns {*}
  1331       */
  1332      processTemplate: function(template) {
  1333        if (usesStandardSymbols) {
  1334          return template;
  1335        } else {
  1336          if (!template || !angular.isString(template)) return template;
  1337          return template.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol);
  1338        }
  1339      },
  1340  
  1341      /**
  1342       * Scan up dom hierarchy for enabled parent;
  1343       */
  1344      getParentWithPointerEvents: function (element) {
  1345        var parent = element.parent();
  1346  
  1347        // jqLite might return a non-null, but still empty, parent; so check for parent and length
  1348        while (hasComputedStyle(parent, 'pointer-events', 'none')) {
  1349          parent = parent.parent();
  1350        }
  1351  
  1352        return parent;
  1353      },
  1354  
  1355      getNearestContentElement: function (element) {
  1356        var current = element.parent()[0];
  1357        // Look for the nearest parent md-content, stopping at the rootElement.
  1358        while (current && current !== $rootElement[0] && current !== document.body && current.nodeName.toUpperCase() !== 'MD-CONTENT') {
  1359          current = current.parentNode;
  1360        }
  1361        return current;
  1362      },
  1363  
  1364      /**
  1365         * Parses an attribute value, mostly a string.
  1366         * By default checks for negated values and returns `false´ if present.
  1367         * Negated values are: (native falsy) and negative strings like:
  1368         * `false` or `0`.
  1369         * @param value Attribute value which should be parsed.
  1370         * @param negatedCheck When set to false, won't check for negated values.
  1371         * @returns {boolean}
  1372         */
  1373        parseAttributeBoolean: function(value, negatedCheck) {
  1374          return value === '' || !!value && (negatedCheck === false || value !== 'false' && value !== '0');
  1375        },
  1376  
  1377      hasComputedStyle: hasComputedStyle
  1378    };
  1379  
  1380  // Instantiate other namespace utility methods
  1381  
  1382    $mdUtil.dom.animator = $$mdAnimate($mdUtil);
  1383  
  1384    return $mdUtil;
  1385  
  1386    function getNode(el) {
  1387      return el[0] || el;
  1388    }
  1389  
  1390  }
  1391  UtilFactory.$inject = ["$document", "$timeout", "$compile", "$rootScope", "$$mdAnimate", "$interpolate", "$log", "$rootElement", "$window"];
  1392  
  1393  /*
  1394   * Since removing jQuery from the demos, some code that uses `element.focus()` is broken.
  1395   * We need to add `element.focus()`, because it's testable unlike `element[0].focus`.
  1396   */
  1397  
  1398  angular.element.prototype.focus = angular.element.prototype.focus || function() {
  1399      if (this.length) {
  1400        this[0].focus();
  1401      }
  1402      return this;
  1403    };
  1404  angular.element.prototype.blur = angular.element.prototype.blur || function() {
  1405      if (this.length) {
  1406        this[0].blur();
  1407      }
  1408      return this;
  1409    };
  1410  
  1411  
  1412  })();
  1413  (function(){
  1414  "use strict";
  1415  
  1416  
  1417  angular.module('material.core')
  1418    .service('$mdAria', AriaService);
  1419  
  1420  /*
  1421   * @ngInject
  1422   */
  1423  function AriaService($$rAF, $log, $window, $interpolate) {
  1424  
  1425    return {
  1426      expect: expect,
  1427      expectAsync: expectAsync,
  1428      expectWithText: expectWithText
  1429    };
  1430  
  1431    /**
  1432     * Check if expected attribute has been specified on the target element or child
  1433     * @param element
  1434     * @param attrName
  1435     * @param {optional} defaultValue What to set the attr to if no value is found
  1436     */
  1437    function expect(element, attrName, defaultValue) {
  1438  
  1439      var node = angular.element(element)[0] || element;
  1440  
  1441      // if node exists and neither it nor its children have the attribute
  1442      if (node &&
  1443         ((!node.hasAttribute(attrName) ||
  1444          node.getAttribute(attrName).length === 0) &&
  1445          !childHasAttribute(node, attrName))) {
  1446  
  1447        defaultValue = angular.isString(defaultValue) ? defaultValue.trim() : '';
  1448        if (defaultValue.length) {
  1449          element.attr(attrName, defaultValue);
  1450        } else {
  1451          $log.warn('ARIA: Attribute "', attrName, '", required for accessibility, is missing on node:', node);
  1452        }
  1453  
  1454      }
  1455    }
  1456  
  1457    function expectAsync(element, attrName, defaultValueGetter) {
  1458      // Problem: when retrieving the element's contents synchronously to find the label,
  1459      // the text may not be defined yet in the case of a binding.
  1460      // There is a higher chance that a binding will be defined if we wait one frame.
  1461      $$rAF(function() {
  1462          expect(element, attrName, defaultValueGetter());
  1463      });
  1464    }
  1465  
  1466    function expectWithText(element, attrName) {
  1467      var content = getText(element) || "";
  1468      var hasBinding = content.indexOf($interpolate.startSymbol())>-1;
  1469  
  1470      if ( hasBinding ) {
  1471        expectAsync(element, attrName, function() {
  1472          return getText(element);
  1473        });
  1474      } else {
  1475        expect(element, attrName, content);
  1476      }
  1477    }
  1478  
  1479    function getText(element) {
  1480      return (element.text() || "").trim();
  1481    }
  1482  
  1483    function childHasAttribute(node, attrName) {
  1484      var hasChildren = node.hasChildNodes(),
  1485          hasAttr = false;
  1486  
  1487      function isHidden(el) {
  1488        var style = el.currentStyle ? el.currentStyle : $window.getComputedStyle(el);
  1489        return (style.display === 'none');
  1490      }
  1491  
  1492      if(hasChildren) {
  1493        var children = node.childNodes;
  1494        for(var i=0; i<children.length; i++){
  1495          var child = children[i];
  1496          if(child.nodeType === 1 && child.hasAttribute(attrName)) {
  1497            if(!isHidden(child)){
  1498              hasAttr = true;
  1499            }
  1500          }
  1501        }
  1502      }
  1503      return hasAttr;
  1504    }
  1505  }
  1506  AriaService.$inject = ["$$rAF", "$log", "$window", "$interpolate"];
  1507  
  1508  })();
  1509  (function(){
  1510  "use strict";
  1511  
  1512  angular
  1513    .module('material.core')
  1514    .service('$mdCompiler', mdCompilerService);
  1515  
  1516  function mdCompilerService($q, $http, $injector, $compile, $controller, $templateCache) {
  1517    /* jshint validthis: true */
  1518  
  1519    /*
  1520     * @ngdoc service
  1521     * @name $mdCompiler
  1522     * @module material.core
  1523     * @description
  1524     * The $mdCompiler service is an abstraction of angular's compiler, that allows the developer
  1525     * to easily compile an element with a templateUrl, controller, and locals.
  1526     *
  1527     * @usage
  1528     * <hljs lang="js">
  1529     * $mdCompiler.compile({
  1530     *   templateUrl: 'modal.html',
  1531     *   controller: 'ModalCtrl',
  1532     *   locals: {
  1533     *     modal: myModalInstance;
  1534     *   }
  1535     * }).then(function(compileData) {
  1536     *   compileData.element; // modal.html's template in an element
  1537     *   compileData.link(myScope); //attach controller & scope to element
  1538     * });
  1539     * </hljs>
  1540     */
  1541  
  1542     /*
  1543      * @ngdoc method
  1544      * @name $mdCompiler#compile
  1545      * @description A helper to compile an HTML template/templateUrl with a given controller,
  1546      * locals, and scope.
  1547      * @param {object} options An options object, with the following properties:
  1548      *
  1549      *    - `controller` - `{(string=|function()=}` Controller fn that should be associated with
  1550      *      newly created scope or the name of a registered controller if passed as a string.
  1551      *    - `controllerAs` - `{string=}` A controller alias name. If present the controller will be
  1552      *      published to scope under the `controllerAs` name.
  1553      *    - `template` - `{string=}` An html template as a string.
  1554      *    - `templateUrl` - `{string=}` A path to an html template.
  1555      *    - `transformTemplate` - `{function(template)=}` A function which transforms the template after
  1556      *      it is loaded. It will be given the template string as a parameter, and should
  1557      *      return a a new string representing the transformed template.
  1558      *    - `resolve` - `{Object.<string, function>=}` - An optional map of dependencies which should
  1559      *      be injected into the controller. If any of these dependencies are promises, the compiler
  1560      *      will wait for them all to be resolved, or if one is rejected before the controller is
  1561      *      instantiated `compile()` will fail..
  1562      *      * `key` - `{string}`: a name of a dependency to be injected into the controller.
  1563      *      * `factory` - `{string|function}`: If `string` then it is an alias for a service.
  1564      *        Otherwise if function, then it is injected and the return value is treated as the
  1565      *        dependency. If the result is a promise, it is resolved before its value is
  1566      *        injected into the controller.
  1567      *
  1568      * @returns {object=} promise A promise, which will be resolved with a `compileData` object.
  1569      * `compileData` has the following properties:
  1570      *
  1571      *   - `element` - `{element}`: an uncompiled element matching the provided template.
  1572      *   - `link` - `{function(scope)}`: A link function, which, when called, will compile
  1573      *     the element and instantiate the provided controller (if given).
  1574      *   - `locals` - `{object}`: The locals which will be passed into the controller once `link` is
  1575      *     called. If `bindToController` is true, they will be coppied to the ctrl instead
  1576      *   - `bindToController` - `bool`: bind the locals to the controller, instead of passing them in.
  1577      */
  1578    this.compile = function(options) {
  1579      var templateUrl = options.templateUrl;
  1580      var template = options.template || '';
  1581      var controller = options.controller;
  1582      var controllerAs = options.controllerAs;
  1583      var resolve = angular.extend({}, options.resolve || {});
  1584      var locals = angular.extend({}, options.locals || {});
  1585      var transformTemplate = options.transformTemplate || angular.identity;
  1586      var bindToController = options.bindToController;
  1587  
  1588      // Take resolve values and invoke them.
  1589      // Resolves can either be a string (value: 'MyRegisteredAngularConst'),
  1590      // or an invokable 'factory' of sorts: (value: function ValueGetter($dependency) {})
  1591      angular.forEach(resolve, function(value, key) {
  1592        if (angular.isString(value)) {
  1593          resolve[key] = $injector.get(value);
  1594        } else {
  1595          resolve[key] = $injector.invoke(value);
  1596        }
  1597      });
  1598      //Add the locals, which are just straight values to inject
  1599      //eg locals: { three: 3 }, will inject three into the controller
  1600      angular.extend(resolve, locals);
  1601  
  1602      if (templateUrl) {
  1603        resolve.$template = $http.get(templateUrl, {cache: $templateCache})
  1604          .then(function(response) {
  1605            return response.data;
  1606          });
  1607      } else {
  1608        resolve.$template = $q.when(template);
  1609      }
  1610  
  1611      // Wait for all the resolves to finish if they are promises
  1612      return $q.all(resolve).then(function(locals) {
  1613  
  1614        var compiledData;
  1615        var template = transformTemplate(locals.$template, options);
  1616        var element = options.element || angular.element('<div>').html(template.trim()).contents();
  1617        var linkFn = $compile(element);
  1618  
  1619        // Return a linking function that can be used later when the element is ready
  1620        return compiledData = {
  1621          locals: locals,
  1622          element: element,
  1623          link: function link(scope) {
  1624            locals.$scope = scope;
  1625  
  1626            //Instantiate controller if it exists, because we have scope
  1627            if (controller) {
  1628              var invokeCtrl = $controller(controller, locals, true);
  1629              if (bindToController) {
  1630                angular.extend(invokeCtrl.instance, locals);
  1631              }
  1632              var ctrl = invokeCtrl();
  1633              //See angular-route source for this logic
  1634              element.data('$ngControllerController', ctrl);
  1635              element.children().data('$ngControllerController', ctrl);
  1636  
  1637              if (controllerAs) {
  1638                scope[controllerAs] = ctrl;
  1639              }
  1640  
  1641              // Publish reference to this controller
  1642              compiledData.controller = ctrl;
  1643            }
  1644            return linkFn(scope);
  1645          }
  1646        };
  1647      });
  1648  
  1649    };
  1650  }
  1651  mdCompilerService.$inject = ["$q", "$http", "$injector", "$compile", "$controller", "$templateCache"];
  1652  
  1653  })();
  1654  (function(){
  1655  "use strict";
  1656  
  1657  var HANDLERS = {};
  1658  
  1659  /* The state of the current 'pointer'
  1660   * The pointer represents the state of the current touch.
  1661   * It contains normalized x and y coordinates from DOM events,
  1662   * as well as other information abstracted from the DOM.
  1663   */
  1664   
  1665  var pointer, lastPointer, forceSkipClickHijack = false;
  1666  
  1667  /**
  1668   * The position of the most recent click if that click was on a label element.
  1669   * @type {{x: number, y: number}?}
  1670   */
  1671  var lastLabelClickPos = null;
  1672  
  1673  // Used to attach event listeners once when multiple ng-apps are running.
  1674  var isInitialized = false;
  1675  
  1676  angular
  1677    .module('material.core.gestures', [ ])
  1678    .provider('$mdGesture', MdGestureProvider)
  1679    .factory('$$MdGestureHandler', MdGestureHandler)
  1680    .run( attachToDocument );
  1681  
  1682  /**
  1683     * @ngdoc service
  1684     * @name $mdGestureProvider
  1685     * @module material.core.gestures
  1686     *
  1687     * @description
  1688     * In some scenarios on Mobile devices (without jQuery), the click events should NOT be hijacked.
  1689     * `$mdGestureProvider` is used to configure the Gesture module to ignore or skip click hijacking on mobile
  1690     * devices.
  1691     *
  1692     * <hljs lang="js">
  1693     *   app.config(function($mdGestureProvider) {
  1694     *
  1695     *     // For mobile devices without jQuery loaded, do not
  1696     *     // intercept click events during the capture phase.
  1697     *     $mdGestureProvider.skipClickHijack();
  1698     *
  1699     *   });
  1700     * </hljs>
  1701     *
  1702     */
  1703  function MdGestureProvider() { }
  1704  
  1705  MdGestureProvider.prototype = {
  1706  
  1707    // Publish access to setter to configure a variable  BEFORE the
  1708    // $mdGesture service is instantiated...
  1709    skipClickHijack: function() {
  1710      return forceSkipClickHijack = true;
  1711    },
  1712  
  1713    /**
  1714     * $get is used to build an instance of $mdGesture
  1715     * @ngInject
  1716     */
  1717    $get : ["$$MdGestureHandler", "$$rAF", "$timeout", function($$MdGestureHandler, $$rAF, $timeout) {
  1718         return new MdGesture($$MdGestureHandler, $$rAF, $timeout);
  1719    }]
  1720  };
  1721  
  1722  
  1723  
  1724  /**
  1725   * MdGesture factory construction function
  1726   * @ngInject
  1727   */
  1728  function MdGesture($$MdGestureHandler, $$rAF, $timeout) {
  1729    var userAgent = navigator.userAgent || navigator.vendor || window.opera;
  1730    var isIos = userAgent.match(/ipad|iphone|ipod/i);
  1731    var isAndroid = userAgent.match(/android/i);
  1732    var hasJQuery =  (typeof window.jQuery !== 'undefined') && (angular.element === window.jQuery);
  1733  
  1734    var self = {
  1735      handler: addHandler,
  1736      register: register,
  1737      // On mobile w/out jQuery, we normally intercept clicks. Should we skip that?
  1738      isHijackingClicks: (isIos || isAndroid) && !hasJQuery && !forceSkipClickHijack
  1739    };
  1740  
  1741    if (self.isHijackingClicks) {
  1742      var maxClickDistance = 6;
  1743      self.handler('click', {
  1744        options: {
  1745          maxDistance: maxClickDistance
  1746        },
  1747        onEnd: checkDistanceAndEmit('click')
  1748      });
  1749  
  1750      self.handler('focus', {
  1751        options: {
  1752          maxDistance: maxClickDistance
  1753        },
  1754        onEnd: function(ev, pointer) {
  1755          if (pointer.distance < this.state.options.maxDistance) {
  1756            if (canFocus(ev.target)) {
  1757              this.dispatchEvent(ev, 'focus', pointer);
  1758              ev.target.focus();
  1759            }
  1760          }
  1761  
  1762          function canFocus(element) {
  1763            var focusableElements = ['INPUT', 'SELECT', 'BUTTON', 'TEXTAREA', 'VIDEO', 'AUDIO'];
  1764  
  1765            return (element.getAttribute('tabindex') != '-1') &&
  1766                !element.hasAttribute('DISABLED') &&
  1767                (element.hasAttribute('tabindex') || element.hasAttribute('href') ||
  1768                (focusableElements.indexOf(element.nodeName) != -1));
  1769          }
  1770        }
  1771      });
  1772  
  1773      self.handler('mouseup', {
  1774        options: {
  1775          maxDistance: maxClickDistance
  1776        },
  1777        onEnd: checkDistanceAndEmit('mouseup')
  1778      });
  1779  
  1780      self.handler('mousedown', {
  1781        onStart: function(ev) {
  1782          this.dispatchEvent(ev, 'mousedown');
  1783        }
  1784      });
  1785    }
  1786  
  1787    function checkDistanceAndEmit(eventName) {
  1788      return function(ev, pointer) {
  1789        if (pointer.distance < this.state.options.maxDistance) {
  1790          this.dispatchEvent(ev, eventName, pointer);
  1791        }
  1792      };
  1793    }
  1794  
  1795    /*
  1796     * Register an element to listen for a handler.
  1797     * This allows an element to override the default options for a handler.
  1798     * Additionally, some handlers like drag and hold only dispatch events if
  1799     * the domEvent happens inside an element that's registered to listen for these events.
  1800     *
  1801     * @see GestureHandler for how overriding of default options works.
  1802     * @example $mdGesture.register(myElement, 'drag', { minDistance: 20, horziontal: false })
  1803     */
  1804    function register(element, handlerName, options) {
  1805      var handler = HANDLERS[handlerName.replace(/^\$md./, '')];
  1806      if (!handler) {
  1807        throw new Error('Failed to register element with handler ' + handlerName + '. ' +
  1808        'Available handlers: ' + Object.keys(HANDLERS).join(', '));
  1809      }
  1810      return handler.registerElement(element, options);
  1811    }
  1812  
  1813    /*
  1814     * add a handler to $mdGesture. see below.
  1815     */
  1816    function addHandler(name, definition) {
  1817      var handler = new $$MdGestureHandler(name);
  1818      angular.extend(handler, definition);
  1819      HANDLERS[name] = handler;
  1820  
  1821      return self;
  1822    }
  1823  
  1824    /*
  1825     * Register handlers. These listen to touch/start/move events, interpret them,
  1826     * and dispatch gesture events depending on options & conditions. These are all
  1827     * instances of GestureHandler.
  1828     * @see GestureHandler 
  1829     */
  1830    return self
  1831      /*
  1832       * The press handler dispatches an event on touchdown/touchend.
  1833       * It's a simple abstraction of touch/mouse/pointer start and end.
  1834       */
  1835      .handler('press', {
  1836        onStart: function (ev, pointer) {
  1837          this.dispatchEvent(ev, '$md.pressdown');
  1838        },
  1839        onEnd: function (ev, pointer) {
  1840          this.dispatchEvent(ev, '$md.pressup');
  1841        }
  1842      })
  1843  
  1844      /*
  1845       * The hold handler dispatches an event if the user keeps their finger within
  1846       * the same <maxDistance> area for <delay> ms.
  1847       * The hold handler will only run if a parent of the touch target is registered
  1848       * to listen for hold events through $mdGesture.register()
  1849       */
  1850      .handler('hold', {
  1851        options: {
  1852          maxDistance: 6,
  1853          delay: 500
  1854        },
  1855        onCancel: function () {
  1856          $timeout.cancel(this.state.timeout);
  1857        },
  1858        onStart: function (ev, pointer) {
  1859          // For hold, require a parent to be registered with $mdGesture.register()
  1860          // Because we prevent scroll events, this is necessary.
  1861          if (!this.state.registeredParent) return this.cancel();
  1862  
  1863          this.state.pos = {x: pointer.x, y: pointer.y};
  1864          this.state.timeout = $timeout(angular.bind(this, function holdDelayFn() {
  1865            this.dispatchEvent(ev, '$md.hold');
  1866            this.cancel(); //we're done!
  1867          }), this.state.options.delay, false);
  1868        },
  1869        onMove: function (ev, pointer) {
  1870          // Don't scroll while waiting for hold.
  1871          // If we don't preventDefault touchmove events here, Android will assume we don't
  1872          // want to listen to anymore touch events. It will start scrolling and stop sending
  1873          // touchmove events.
  1874          ev.preventDefault();
  1875  
  1876          // If the user moves greater than <maxDistance> pixels, stop the hold timer
  1877          // set in onStart
  1878          var dx = this.state.pos.x - pointer.x;
  1879          var dy = this.state.pos.y - pointer.y;
  1880          if (Math.sqrt(dx * dx + dy * dy) > this.options.maxDistance) {
  1881            this.cancel();
  1882          }
  1883        },
  1884        onEnd: function () {
  1885          this.onCancel();
  1886        }
  1887      })
  1888  
  1889      /*
  1890       * The drag handler dispatches a drag event if the user holds and moves his finger greater than
  1891       * <minDistance> px in the x or y direction, depending on options.horizontal.
  1892       * The drag will be cancelled if the user moves his finger greater than <minDistance>*<cancelMultiplier> in
  1893       * the perpendicular direction. Eg if the drag is horizontal and the user moves his finger <minDistance>*<cancelMultiplier>
  1894       * pixels vertically, this handler won't consider the move part of a drag.
  1895       */
  1896      .handler('drag', {
  1897        options: {
  1898          minDistance: 6,
  1899          horizontal: true,
  1900          cancelMultiplier: 1.5
  1901        },
  1902        onStart: function (ev) {
  1903          // For drag, require a parent to be registered with $mdGesture.register()
  1904          if (!this.state.registeredParent) this.cancel();
  1905        },
  1906        onMove: function (ev, pointer) {
  1907          var shouldStartDrag, shouldCancel;
  1908          // Don't scroll while deciding if this touchmove qualifies as a drag event.
  1909          // If we don't preventDefault touchmove events here, Android will assume we don't
  1910          // want to listen to anymore touch events. It will start scrolling and stop sending
  1911          // touchmove events.
  1912          ev.preventDefault();
  1913  
  1914          if (!this.state.dragPointer) {
  1915            if (this.state.options.horizontal) {
  1916              shouldStartDrag = Math.abs(pointer.distanceX) > this.state.options.minDistance;
  1917              shouldCancel = Math.abs(pointer.distanceY) > this.state.options.minDistance * this.state.options.cancelMultiplier;
  1918            } else {
  1919              shouldStartDrag = Math.abs(pointer.distanceY) > this.state.options.minDistance;
  1920              shouldCancel = Math.abs(pointer.distanceX) > this.state.options.minDistance * this.state.options.cancelMultiplier;
  1921            }
  1922  
  1923            if (shouldStartDrag) {
  1924              // Create a new pointer representing this drag, starting at this point where the drag started.
  1925              this.state.dragPointer = makeStartPointer(ev);
  1926              updatePointerState(ev, this.state.dragPointer);
  1927              this.dispatchEvent(ev, '$md.dragstart', this.state.dragPointer);
  1928  
  1929            } else if (shouldCancel) {
  1930              this.cancel();
  1931            }
  1932          } else {
  1933            this.dispatchDragMove(ev);
  1934          }
  1935        },
  1936        // Only dispatch dragmove events every frame; any more is unnecessray
  1937        dispatchDragMove: $$rAF.throttle(function (ev) {
  1938          // Make sure the drag didn't stop while waiting for the next frame
  1939          if (this.state.isRunning) {
  1940            updatePointerState(ev, this.state.dragPointer);
  1941            this.dispatchEvent(ev, '$md.drag', this.state.dragPointer);
  1942          }
  1943        }),
  1944        onEnd: function (ev, pointer) {
  1945          if (this.state.dragPointer) {
  1946            updatePointerState(ev, this.state.dragPointer);
  1947            this.dispatchEvent(ev, '$md.dragend', this.state.dragPointer);
  1948          }
  1949        }
  1950      })
  1951  
  1952      /*
  1953       * The swipe handler will dispatch a swipe event if, on the end of a touch,
  1954       * the velocity and distance were high enough.
  1955       */
  1956      .handler('swipe', {
  1957        options: {
  1958          minVelocity: 0.65,
  1959          minDistance: 10
  1960        },
  1961        onEnd: function (ev, pointer) {
  1962          var eventType;
  1963  
  1964          if (Math.abs(pointer.velocityX) > this.state.options.minVelocity &&
  1965            Math.abs(pointer.distanceX) > this.state.options.minDistance) {
  1966            eventType = pointer.directionX == 'left' ? '$md.swipeleft' : '$md.swiperight';
  1967            this.dispatchEvent(ev, eventType);
  1968          }
  1969          else if (Math.abs(pointer.velocityY) > this.state.options.minVelocity &&
  1970            Math.abs(pointer.distanceY) > this.state.options.minDistance) {
  1971            eventType = pointer.directionY == 'up' ? '$md.swipeup' : '$md.swipedown';
  1972            this.dispatchEvent(ev, eventType);
  1973          }
  1974        }
  1975      });
  1976  
  1977  }
  1978  MdGesture.$inject = ["$$MdGestureHandler", "$$rAF", "$timeout"];
  1979  
  1980  /**
  1981   * MdGestureHandler
  1982   * A GestureHandler is an object which is able to dispatch custom dom events
  1983   * based on native dom {touch,pointer,mouse}{start,move,end} events.
  1984   *
  1985   * A gesture will manage its lifecycle through the start,move,end, and cancel
  1986   * functions, which are called by native dom events.
  1987   *
  1988   * A gesture has the concept of 'options' (eg a swipe's required velocity), which can be
  1989   * overridden by elements registering through $mdGesture.register()
  1990   */
  1991  function GestureHandler (name) {
  1992    this.name = name;
  1993    this.state = {};
  1994  }
  1995  
  1996  function MdGestureHandler() {
  1997    var hasJQuery =  (typeof window.jQuery !== 'undefined') && (angular.element === window.jQuery);
  1998  
  1999    GestureHandler.prototype = {
  2000      options: {},
  2001      // jQuery listeners don't work with custom DOMEvents, so we have to dispatch events
  2002      // differently when jQuery is loaded
  2003      dispatchEvent: hasJQuery ?  jQueryDispatchEvent : nativeDispatchEvent,
  2004  
  2005      // These are overridden by the registered handler
  2006      onStart: angular.noop,
  2007      onMove: angular.noop,
  2008      onEnd: angular.noop,
  2009      onCancel: angular.noop,
  2010  
  2011      // onStart sets up a new state for the handler, which includes options from the
  2012      // nearest registered parent element of ev.target.
  2013      start: function (ev, pointer) {
  2014        if (this.state.isRunning) return;
  2015        var parentTarget = this.getNearestParent(ev.target);
  2016        // Get the options from the nearest registered parent
  2017        var parentTargetOptions = parentTarget && parentTarget.$mdGesture[this.name] || {};
  2018  
  2019        this.state = {
  2020          isRunning: true,
  2021          // Override the default options with the nearest registered parent's options
  2022          options: angular.extend({}, this.options, parentTargetOptions),
  2023          // Pass in the registered parent node to the state so the onStart listener can use
  2024          registeredParent: parentTarget
  2025        };
  2026        this.onStart(ev, pointer);
  2027      },
  2028      move: function (ev, pointer) {
  2029        if (!this.state.isRunning) return;
  2030        this.onMove(ev, pointer);
  2031      },
  2032      end: function (ev, pointer) {
  2033        if (!this.state.isRunning) return;
  2034        this.onEnd(ev, pointer);
  2035        this.state.isRunning = false;
  2036      },
  2037      cancel: function (ev, pointer) {
  2038        this.onCancel(ev, pointer);
  2039        this.state = {};
  2040      },
  2041  
  2042      // Find and return the nearest parent element that has been registered to
  2043      // listen for this handler via $mdGesture.register(element, 'handlerName').
  2044      getNearestParent: function (node) {
  2045        var current = node;
  2046        while (current) {
  2047          if ((current.$mdGesture || {})[this.name]) {
  2048            return current;
  2049          }
  2050          current = current.parentNode;
  2051        }
  2052        return null;
  2053      },
  2054  
  2055      // Called from $mdGesture.register when an element registers itself with a handler.
  2056      // Store the options the user gave on the DOMElement itself. These options will
  2057      // be retrieved with getNearestParent when the handler starts.
  2058      registerElement: function (element, options) {
  2059        var self = this;
  2060        element[0].$mdGesture = element[0].$mdGesture || {};
  2061        element[0].$mdGesture[this.name] = options || {};
  2062        element.on('$destroy', onDestroy);
  2063  
  2064        return onDestroy;
  2065  
  2066        function onDestroy() {
  2067          delete element[0].$mdGesture[self.name];
  2068          element.off('$destroy', onDestroy);
  2069        }
  2070      }
  2071    };
  2072  
  2073    return GestureHandler;
  2074  
  2075    /*
  2076     * Dispatch an event with jQuery
  2077     * TODO: Make sure this sends bubbling events
  2078     *
  2079     * @param srcEvent the original DOM touch event that started this.
  2080     * @param eventType the name of the custom event to send (eg 'click' or '$md.drag')
  2081     * @param eventPointer the pointer object that matches this event.
  2082     */
  2083    function jQueryDispatchEvent(srcEvent, eventType, eventPointer) {
  2084      eventPointer = eventPointer || pointer;
  2085      var eventObj = new angular.element.Event(eventType);
  2086  
  2087      eventObj.$material = true;
  2088      eventObj.pointer = eventPointer;
  2089      eventObj.srcEvent = srcEvent;
  2090  
  2091      angular.extend(eventObj, {
  2092        clientX: eventPointer.x,
  2093        clientY: eventPointer.y,
  2094        screenX: eventPointer.x,
  2095        screenY: eventPointer.y,
  2096        pageX: eventPointer.x,
  2097        pageY: eventPointer.y,
  2098        ctrlKey: srcEvent.ctrlKey,
  2099        altKey: srcEvent.altKey,
  2100        shiftKey: srcEvent.shiftKey,
  2101        metaKey: srcEvent.metaKey
  2102      });
  2103      angular.element(eventPointer.target).trigger(eventObj);
  2104    }
  2105  
  2106    /*
  2107     * NOTE: nativeDispatchEvent is very performance sensitive.
  2108     * @param srcEvent the original DOM touch event that started this.
  2109     * @param eventType the name of the custom event to send (eg 'click' or '$md.drag')
  2110     * @param eventPointer the pointer object that matches this event.
  2111     */
  2112    function nativeDispatchEvent(srcEvent, eventType, eventPointer) {
  2113      eventPointer = eventPointer || pointer;
  2114      var eventObj;
  2115  
  2116      if (eventType === 'click' || eventType == 'mouseup' || eventType == 'mousedown' ) {
  2117        eventObj = document.createEvent('MouseEvents');
  2118        eventObj.initMouseEvent(
  2119          eventType, true, true, window, srcEvent.detail,
  2120          eventPointer.x, eventPointer.y, eventPointer.x, eventPointer.y,
  2121          srcEvent.ctrlKey, srcEvent.altKey, srcEvent.shiftKey, srcEvent.metaKey,
  2122          srcEvent.button, srcEvent.relatedTarget || null
  2123        );
  2124  
  2125      } else {
  2126        eventObj = document.createEvent('CustomEvent');
  2127        eventObj.initCustomEvent(eventType, true, true, {});
  2128      }
  2129      eventObj.$material = true;
  2130      eventObj.pointer = eventPointer;
  2131      eventObj.srcEvent = srcEvent;
  2132      eventPointer.target.dispatchEvent(eventObj);
  2133    }
  2134  
  2135  }
  2136  
  2137  /**
  2138   * Attach Gestures: hook document and check shouldHijack clicks
  2139   * @ngInject
  2140   */
  2141  function attachToDocument( $mdGesture, $$MdGestureHandler ) {
  2142  
  2143    // Polyfill document.contains for IE11.
  2144    // TODO: move to util
  2145    document.contains || (document.contains = function (node) {
  2146      return document.body.contains(node);
  2147    });
  2148  
  2149    if (!isInitialized && $mdGesture.isHijackingClicks ) {
  2150      /*
  2151       * If hijack clicks is true, we preventDefault any click that wasn't
  2152       * sent by ngMaterial. This is because on older Android & iOS, a false, or 'ghost',
  2153       * click event will be sent ~400ms after a touchend event happens.
  2154       * The only way to know if this click is real is to prevent any normal
  2155       * click events, and add a flag to events sent by material so we know not to prevent those.
  2156       * 
  2157       * Two exceptions to click events that should be prevented are:
  2158       *  - click events sent by the keyboard (eg form submit)
  2159       *  - events that originate from an Ionic app
  2160       */
  2161      document.addEventListener('click'    , clickHijacker     , true);
  2162      document.addEventListener('mouseup'  , mouseInputHijacker, true);
  2163      document.addEventListener('mousedown', mouseInputHijacker, true);
  2164      document.addEventListener('focus'    , mouseInputHijacker, true);
  2165  
  2166      isInitialized = true;
  2167    }
  2168  
  2169    function mouseInputHijacker(ev) {
  2170      var isKeyClick = !ev.clientX && !ev.clientY;
  2171      if (!isKeyClick && !ev.$material && !ev.isIonicTap
  2172        && !isInputEventFromLabelClick(ev)) {
  2173        ev.preventDefault();
  2174        ev.stopPropagation();
  2175      }
  2176    }
  2177  
  2178    function clickHijacker(ev) {
  2179      var isKeyClick = ev.clientX === 0 && ev.clientY === 0;
  2180      if (!isKeyClick && !ev.$material && !ev.isIonicTap
  2181        && !isInputEventFromLabelClick(ev)) {
  2182        ev.preventDefault();
  2183        ev.stopPropagation();
  2184        lastLabelClickPos = null;
  2185      } else {
  2186        lastLabelClickPos = null;
  2187        if (ev.target.tagName.toLowerCase() == 'label') {
  2188          lastLabelClickPos = {x: ev.x, y: ev.y};
  2189        }
  2190      }
  2191    }
  2192  
  2193  
  2194    // Listen to all events to cover all platforms.
  2195    var START_EVENTS = 'mousedown touchstart pointerdown';
  2196    var MOVE_EVENTS = 'mousemove touchmove pointermove';
  2197    var END_EVENTS = 'mouseup mouseleave touchend touchcancel pointerup pointercancel';
  2198  
  2199    angular.element(document)
  2200      .on(START_EVENTS, gestureStart)
  2201      .on(MOVE_EVENTS, gestureMove)
  2202      .on(END_EVENTS, gestureEnd)
  2203      // For testing
  2204      .on('$$mdGestureReset', function gestureClearCache () {
  2205        lastPointer = pointer = null;
  2206      });
  2207  
  2208    /*
  2209     * When a DOM event happens, run all registered gesture handlers' lifecycle
  2210     * methods which match the DOM event.
  2211     * Eg when a 'touchstart' event happens, runHandlers('start') will call and
  2212     * run `handler.cancel()` and `handler.start()` on all registered handlers.
  2213     */
  2214    function runHandlers(handlerEvent, event) {
  2215      var handler;
  2216      for (var name in HANDLERS) {
  2217        handler = HANDLERS[name];
  2218        if( handler instanceof $$MdGestureHandler ) {
  2219  
  2220          if (handlerEvent === 'start') {
  2221            // Run cancel to reset any handlers' state
  2222            handler.cancel();
  2223          }
  2224          handler[handlerEvent](event, pointer);
  2225  
  2226        }
  2227      }
  2228    }
  2229  
  2230    /*
  2231     * gestureStart vets if a start event is legitimate (and not part of a 'ghost click' from iOS/Android)
  2232     * If it is legitimate, we initiate the pointer state and mark the current pointer's type
  2233     * For example, for a touchstart event, mark the current pointer as a 'touch' pointer, so mouse events
  2234     * won't effect it.
  2235     */
  2236    function gestureStart(ev) {
  2237      // If we're already touched down, abort
  2238      if (pointer) return;
  2239  
  2240      var now = +Date.now();
  2241  
  2242      // iOS & old android bug: after a touch event, a click event is sent 350 ms later.
  2243      // If <400ms have passed, don't allow an event of a different type than the previous event
  2244      if (lastPointer && !typesMatch(ev, lastPointer) && (now - lastPointer.endTime < 1500)) {
  2245        return;
  2246      }
  2247  
  2248      pointer = makeStartPointer(ev);
  2249  
  2250      runHandlers('start', ev);
  2251    }
  2252    /*
  2253     * If a move event happens of the right type, update the pointer and run all the move handlers.
  2254     * "of the right type": if a mousemove happens but our pointer started with a touch event, do nothing.
  2255     */
  2256    function gestureMove(ev) {
  2257      if (!pointer || !typesMatch(ev, pointer)) return;
  2258  
  2259      updatePointerState(ev, pointer);
  2260      runHandlers('move', ev);
  2261    }
  2262    /*
  2263     * If an end event happens of the right type, update the pointer, run endHandlers, and save the pointer as 'lastPointer'
  2264     */
  2265    function gestureEnd(ev) {
  2266      if (!pointer || !typesMatch(ev, pointer)) return;
  2267  
  2268      updatePointerState(ev, pointer);
  2269      pointer.endTime = +Date.now();
  2270  
  2271      runHandlers('end', ev);
  2272  
  2273      lastPointer = pointer;
  2274      pointer = null;
  2275    }
  2276  
  2277  }
  2278  attachToDocument.$inject = ["$mdGesture", "$$MdGestureHandler"];
  2279  
  2280  // ********************
  2281  // Module Functions
  2282  // ********************
  2283  
  2284  /*
  2285   * Initiate the pointer. x, y, and the pointer's type.
  2286   */
  2287  function makeStartPointer(ev) {
  2288    var point = getEventPoint(ev);
  2289    var startPointer = {
  2290      startTime: +Date.now(),
  2291      target: ev.target,
  2292      // 'p' for pointer events, 'm' for mouse, 't' for touch
  2293      type: ev.type.charAt(0)
  2294    };
  2295    startPointer.startX = startPointer.x = point.pageX;
  2296    startPointer.startY = startPointer.y = point.pageY;
  2297    return startPointer;
  2298  }
  2299  
  2300  /*
  2301   * return whether the pointer's type matches the event's type.
  2302   * Eg if a touch event happens but the pointer has a mouse type, return false.
  2303   */
  2304  function typesMatch(ev, pointer) {
  2305    return ev && pointer && ev.type.charAt(0) === pointer.type;
  2306  }
  2307  
  2308  /**
  2309   * Gets whether the given event is an input event that was caused by clicking on an
  2310   * associated label element.
  2311   *
  2312   * This is necessary because the browser will, upon clicking on a label element, fire an
  2313   * *extra* click event on its associated input (if any). mdGesture is able to flag the label
  2314   * click as with `$material` correctly, but not the second input click.
  2315   *
  2316   * In order to determine whether an input event is from a label click, we compare the (x, y) for
  2317   * the event to the (x, y) for the most recent label click (which is cleared whenever a non-label
  2318   * click occurs). Unfortunately, there are no event properties that tie the input and the label
  2319   * together (such as relatedTarget).
  2320   *
  2321   * @param {MouseEvent} event
  2322   * @returns {boolean}
  2323   */
  2324  function isInputEventFromLabelClick(event) {
  2325    return lastLabelClickPos
  2326        && lastLabelClickPos.x == event.x
  2327        && lastLabelClickPos.y == event.y;
  2328  }
  2329  
  2330  /*
  2331   * Update the given pointer based upon the given DOMEvent.
  2332   * Distance, velocity, direction, duration, etc
  2333   */
  2334  function updatePointerState(ev, pointer) {
  2335    var point = getEventPoint(ev);
  2336    var x = pointer.x = point.pageX;
  2337    var y = pointer.y = point.pageY;
  2338  
  2339    pointer.distanceX = x - pointer.startX;
  2340    pointer.distanceY = y - pointer.startY;
  2341    pointer.distance = Math.sqrt(
  2342      pointer.distanceX * pointer.distanceX + pointer.distanceY * pointer.distanceY
  2343    );
  2344  
  2345    pointer.directionX = pointer.distanceX > 0 ? 'right' : pointer.distanceX < 0 ? 'left' : '';
  2346    pointer.directionY = pointer.distanceY > 0 ? 'down' : pointer.distanceY < 0 ? 'up' : '';
  2347  
  2348    pointer.duration = +Date.now() - pointer.startTime;
  2349    pointer.velocityX = pointer.distanceX / pointer.duration;
  2350    pointer.velocityY = pointer.distanceY / pointer.duration;
  2351  }
  2352  
  2353  /*
  2354   * Normalize the point where the DOM event happened whether it's touch or mouse.
  2355   * @returns point event obj with pageX and pageY on it.
  2356   */
  2357  function getEventPoint(ev) {
  2358    ev = ev.originalEvent || ev; // support jQuery events
  2359    return (ev.touches && ev.touches[0]) ||
  2360      (ev.changedTouches && ev.changedTouches[0]) ||
  2361      ev;
  2362  }
  2363  
  2364  })();
  2365  (function(){
  2366  "use strict";
  2367  
  2368  angular.module('material.core')
  2369    .provider('$$interimElement', InterimElementProvider);
  2370  
  2371  /*
  2372   * @ngdoc service
  2373   * @name $$interimElement
  2374   * @module material.core
  2375   *
  2376   * @description
  2377   *
  2378   * Factory that contructs `$$interimElement.$service` services.
  2379   * Used internally in material design for elements that appear on screen temporarily.
  2380   * The service provides a promise-like API for interacting with the temporary
  2381   * elements.
  2382   *
  2383   * ```js
  2384   * app.service('$mdToast', function($$interimElement) {
  2385   *   var $mdToast = $$interimElement(toastDefaultOptions);
  2386   *   return $mdToast;
  2387   * });
  2388   * ```
  2389   * @param {object=} defaultOptions Options used by default for the `show` method on the service.
  2390   *
  2391   * @returns {$$interimElement.$service}
  2392   *
  2393   */
  2394  
  2395  function InterimElementProvider() {
  2396    createInterimElementProvider.$get = InterimElementFactory;
  2397    InterimElementFactory.$inject = ["$document", "$q", "$$q", "$rootScope", "$timeout", "$rootElement", "$animate", "$mdUtil", "$mdCompiler", "$mdTheming", "$injector"];
  2398    return createInterimElementProvider;
  2399  
  2400    /**
  2401     * Returns a new provider which allows configuration of a new interimElement
  2402     * service. Allows configuration of default options & methods for options,
  2403     * as well as configuration of 'preset' methods (eg dialog.basic(): basic is a preset method)
  2404     */
  2405    function createInterimElementProvider(interimFactoryName) {
  2406      var EXPOSED_METHODS = ['onHide', 'onShow', 'onRemove'];
  2407  
  2408      var customMethods = {};
  2409      var providerConfig = {
  2410        presets: {}
  2411      };
  2412  
  2413      var provider = {
  2414        setDefaults: setDefaults,
  2415        addPreset: addPreset,
  2416        addMethod: addMethod,
  2417        $get: factory
  2418      };
  2419  
  2420      /**
  2421       * all interim elements will come with the 'build' preset
  2422       */
  2423      provider.addPreset('build', {
  2424        methods: ['controller', 'controllerAs', 'resolve',
  2425          'template', 'templateUrl', 'themable', 'transformTemplate', 'parent']
  2426      });
  2427  
  2428      factory.$inject = ["$$interimElement", "$injector"];
  2429      return provider;
  2430  
  2431      /**
  2432       * Save the configured defaults to be used when the factory is instantiated
  2433       */
  2434      function setDefaults(definition) {
  2435        providerConfig.optionsFactory = definition.options;
  2436        providerConfig.methods = (definition.methods || []).concat(EXPOSED_METHODS);
  2437        return provider;
  2438      }
  2439  
  2440      /**
  2441       * Add a method to the factory that isn't specific to any interim element operations
  2442       */
  2443  
  2444      function addMethod(name, fn) {
  2445        customMethods[name] = fn;
  2446        return provider;
  2447      }
  2448  
  2449      /**
  2450       * Save the configured preset to be used when the factory is instantiated
  2451       */
  2452      function addPreset(name, definition) {
  2453        definition = definition || {};
  2454        definition.methods = definition.methods || [];
  2455        definition.options = definition.options || function() { return {}; };
  2456  
  2457        if (/^cancel|hide|show$/.test(name)) {
  2458          throw new Error("Preset '" + name + "' in " + interimFactoryName + " is reserved!");
  2459        }
  2460        if (definition.methods.indexOf('_options') > -1) {
  2461          throw new Error("Method '_options' in " + interimFactoryName + " is reserved!");
  2462        }
  2463        providerConfig.presets[name] = {
  2464          methods: definition.methods.concat(EXPOSED_METHODS),
  2465          optionsFactory: definition.options,
  2466          argOption: definition.argOption
  2467        };
  2468        return provider;
  2469      }
  2470  
  2471      function addPresetMethod(presetName, methodName, method) {
  2472        providerConfig.presets[presetName][methodName] = method;
  2473      }
  2474  
  2475      /**
  2476       * Create a factory that has the given methods & defaults implementing interimElement
  2477       */
  2478      /* @ngInject */
  2479      function factory($$interimElement, $injector) {
  2480        var defaultMethods;
  2481        var defaultOptions;
  2482        var interimElementService = $$interimElement();
  2483  
  2484        /*
  2485         * publicService is what the developer will be using.
  2486         * It has methods hide(), cancel(), show(), build(), and any other
  2487         * presets which were set during the config phase.
  2488         */
  2489        var publicService = {
  2490          hide: interimElementService.hide,
  2491          cancel: interimElementService.cancel,
  2492          show: showInterimElement,
  2493  
  2494          // Special internal method to destroy an interim element without animations
  2495          // used when navigation changes causes a $scope.$destroy() action
  2496          destroy : destroyInterimElement
  2497        };
  2498  
  2499  
  2500        defaultMethods = providerConfig.methods || [];
  2501        // This must be invoked after the publicService is initialized
  2502        defaultOptions = invokeFactory(providerConfig.optionsFactory, {});
  2503  
  2504        // Copy over the simple custom methods
  2505        angular.forEach(customMethods, function(fn, name) {
  2506          publicService[name] = fn;
  2507        });
  2508  
  2509        angular.forEach(providerConfig.presets, function(definition, name) {
  2510          var presetDefaults = invokeFactory(definition.optionsFactory, {});
  2511          var presetMethods = (definition.methods || []).concat(defaultMethods);
  2512  
  2513          // Every interimElement built with a preset has a field called `$type`,
  2514          // which matches the name of the preset.
  2515          // Eg in preset 'confirm', options.$type === 'confirm'
  2516          angular.extend(presetDefaults, { $type: name });
  2517  
  2518          // This creates a preset class which has setter methods for every
  2519          // method given in the `.addPreset()` function, as well as every
  2520          // method given in the `.setDefaults()` function.
  2521          //
  2522          // @example
  2523          // .setDefaults({
  2524          //   methods: ['hasBackdrop', 'clickOutsideToClose', 'escapeToClose', 'targetEvent'],
  2525          //   options: dialogDefaultOptions
  2526          // })
  2527          // .addPreset('alert', {
  2528          //   methods: ['title', 'ok'],
  2529          //   options: alertDialogOptions
  2530          // })
  2531          //
  2532          // Set values will be passed to the options when interimElement.show() is called.
  2533          function Preset(opts) {
  2534            this._options = angular.extend({}, presetDefaults, opts);
  2535          }
  2536          angular.forEach(presetMethods, function(name) {
  2537            Preset.prototype[name] = function(value) {
  2538              this._options[name] = value;
  2539              return this;
  2540            };
  2541          });
  2542  
  2543          // Create shortcut method for one-linear methods
  2544          if (definition.argOption) {
  2545            var methodName = 'show' + name.charAt(0).toUpperCase() + name.slice(1);
  2546            publicService[methodName] = function(arg) {
  2547              var config = publicService[name](arg);
  2548              return publicService.show(config);
  2549            };
  2550          }
  2551  
  2552          // eg $mdDialog.alert() will return a new alert preset
  2553          publicService[name] = function(arg) {
  2554            // If argOption is supplied, eg `argOption: 'content'`, then we assume
  2555            // if the argument is not an options object then it is the `argOption` option.
  2556            //
  2557            // @example `$mdToast.simple('hello')` // sets options.content to hello
  2558            //                                     // because argOption === 'content'
  2559            if (arguments.length && definition.argOption &&
  2560                !angular.isObject(arg) && !angular.isArray(arg))  {
  2561  
  2562              return (new Preset())[definition.argOption](arg);
  2563  
  2564            } else {
  2565              return new Preset(arg);
  2566            }
  2567  
  2568          };
  2569        });
  2570  
  2571        return publicService;
  2572  
  2573        /**
  2574         *
  2575         */
  2576        function showInterimElement(opts) {
  2577          // opts is either a preset which stores its options on an _options field,
  2578          // or just an object made up of options
  2579          opts = opts || { };
  2580          if (opts._options) opts = opts._options;
  2581  
  2582          return interimElementService.show(
  2583            angular.extend({}, defaultOptions, opts)
  2584          );
  2585        }
  2586  
  2587        /**
  2588         *  Special method to hide and destroy an interimElement WITHOUT
  2589         *  any 'leave` or hide animations ( an immediate force hide/remove )
  2590         *
  2591         *  NOTE: This calls the onRemove() subclass method for each component...
  2592         *  which must have code to respond to `options.$destroy == true`
  2593         */
  2594        function destroyInterimElement(opts) {
  2595            return interimElementService.destroy(opts);
  2596        }
  2597  
  2598        /**
  2599         * Helper to call $injector.invoke with a local of the factory name for
  2600         * this provider.
  2601         * If an $mdDialog is providing options for a dialog and tries to inject
  2602         * $mdDialog, a circular dependency error will happen.
  2603         * We get around that by manually injecting $mdDialog as a local.
  2604         */
  2605        function invokeFactory(factory, defaultVal) {
  2606          var locals = {};
  2607          locals[interimFactoryName] = publicService;
  2608          return $injector.invoke(factory || function() { return defaultVal; }, {}, locals);
  2609        }
  2610  
  2611      }
  2612  
  2613    }
  2614  
  2615    /* @ngInject */
  2616    function InterimElementFactory($document, $q, $$q, $rootScope, $timeout, $rootElement, $animate,
  2617                                   $mdUtil, $mdCompiler, $mdTheming, $injector ) {
  2618      return function createInterimElementService() {
  2619        var SHOW_CANCELLED = false;
  2620  
  2621        /*
  2622         * @ngdoc service
  2623         * @name $$interimElement.$service
  2624         *
  2625         * @description
  2626         * A service used to control inserting and removing an element into the DOM.
  2627         *
  2628         */
  2629        var service, stack = [];
  2630  
  2631        // Publish instance $$interimElement service;
  2632        // ... used as $mdDialog, $mdToast, $mdMenu, and $mdSelect
  2633  
  2634        return service = {
  2635          show: show,
  2636          hide: hide,
  2637          cancel: cancel,
  2638          destroy : destroy,
  2639          $injector_: $injector
  2640        };
  2641  
  2642        /*
  2643         * @ngdoc method
  2644         * @name $$interimElement.$service#show
  2645         * @kind function
  2646         *
  2647         * @description
  2648         * Adds the `$interimElement` to the DOM and returns a special promise that will be resolved or rejected
  2649         * with hide or cancel, respectively. To external cancel/hide, developers should use the
  2650         *
  2651         * @param {*} options is hashMap of settings
  2652         * @returns a Promise
  2653         *
  2654         */
  2655        function show(options) {
  2656          options = options || {};
  2657          var interimElement = new InterimElement(options || {});
  2658          var hideExisting = !options.skipHide && stack.length ? service.hide() : $q.when(true);
  2659  
  2660          // This hide()s only the current interim element before showing the next, new one
  2661          // NOTE: this is not reversible (e.g. interim elements are not stackable)
  2662  
  2663          hideExisting.finally(function() {
  2664  
  2665            stack.push(interimElement);
  2666            interimElement
  2667              .show()
  2668              .catch(function( reason ) {
  2669                //$log.error("InterimElement.show() error: " + reason );
  2670                return reason;
  2671              });
  2672  
  2673          });
  2674  
  2675          // Return a promise that will be resolved when the interim
  2676          // element is hidden or cancelled...
  2677  
  2678          return interimElement.deferred.promise;
  2679        }
  2680  
  2681        /*
  2682         * @ngdoc method
  2683         * @name $$interimElement.$service#hide
  2684         * @kind function
  2685         *
  2686         * @description
  2687         * Removes the `$interimElement` from the DOM and resolves the promise returned from `show`
  2688         *
  2689         * @param {*} resolveParam Data to resolve the promise with
  2690         * @returns a Promise that will be resolved after the element has been removed.
  2691         *
  2692         */
  2693        function hide(reason, options) {
  2694          if ( !stack.length ) return $q.when(reason);
  2695          options = options || {};
  2696  
  2697          if (options.closeAll) {
  2698            var promise = $q.all(stack.reverse().map(closeElement));
  2699            stack = [];
  2700            return promise;
  2701          } else if (options.closeTo !== undefined) {
  2702            return $q.all(stack.splice(options.closeTo).map(closeElement));
  2703          } else {
  2704            var interim = stack.pop();
  2705            return closeElement(interim);
  2706          }
  2707  
  2708          function closeElement(interim) {
  2709            interim
  2710              .remove(reason, false, options || { })
  2711              .catch(function( reason ) {
  2712                //$log.error("InterimElement.hide() error: " + reason );
  2713                return reason;
  2714              });
  2715            return interim.deferred.promise;
  2716          }
  2717        }
  2718  
  2719        /*
  2720         * @ngdoc method
  2721         * @name $$interimElement.$service#cancel
  2722         * @kind function
  2723         *
  2724         * @description
  2725         * Removes the `$interimElement` from the DOM and rejects the promise returned from `show`
  2726         *
  2727         * @param {*} reason Data to reject the promise with
  2728         * @returns Promise that will be resolved after the element has been removed.
  2729         *
  2730         */
  2731        function cancel(reason, options) {
  2732          var interim = stack.pop();
  2733          if ( !interim ) return $q.when(reason);
  2734  
  2735          interim
  2736            .remove(reason, true, options || { })
  2737            .catch(function( reason ) {
  2738              //$log.error("InterimElement.cancel() error: " + reason );
  2739              return reason;
  2740            });
  2741  
  2742          return interim.deferred.promise;
  2743        }
  2744  
  2745        /*
  2746         * Special method to quick-remove the interim element without animations
  2747         * Note: interim elements are in "interim containers"
  2748         */
  2749        function destroy(target) {
  2750          var interim = !target ? stack.shift() : null;
  2751          var cntr = angular.element(target).length ? angular.element(target)[0].parentNode : null;
  2752  
  2753          if (cntr) {
  2754              // Try to find the interim element in the stack which corresponds to the supplied DOM element.
  2755              var filtered = stack.filter(function(entry) {
  2756                    var currNode = entry.options.element[0];
  2757                    return  (currNode === cntr);
  2758                  });
  2759  
  2760              // Note: this function might be called when the element already has been removed, in which
  2761              //       case we won't find any matches. That's ok.
  2762              if (filtered.length > 0) {
  2763                interim = filtered[0];
  2764                stack.splice(stack.indexOf(interim), 1);
  2765              }
  2766          }
  2767  
  2768          return interim ? interim.remove(SHOW_CANCELLED, false, {'$destroy':true}) : $q.when(SHOW_CANCELLED);
  2769        }
  2770  
  2771        /*
  2772         * Internal Interim Element Object
  2773         * Used internally to manage the DOM element and related data
  2774         */
  2775        function InterimElement(options) {
  2776          var self, element, showAction = $q.when(true);
  2777  
  2778          options = configureScopeAndTransitions(options);
  2779  
  2780          return self = {
  2781            options : options,
  2782            deferred: $q.defer(),
  2783            show    : createAndTransitionIn,
  2784            remove  : transitionOutAndRemove
  2785          };
  2786  
  2787          /**
  2788           * Compile, link, and show this interim element
  2789           * Use optional autoHided and transition-in effects
  2790           */
  2791          function createAndTransitionIn() {
  2792            return $q(function(resolve, reject){
  2793  
  2794              compileElement(options)
  2795                .then(function( compiledData ) {
  2796                  element = linkElement( compiledData, options );
  2797  
  2798                  showAction = showElement(element, options, compiledData.controller)
  2799                    .then(resolve, rejectAll );
  2800  
  2801                }, rejectAll);
  2802  
  2803              function rejectAll(fault) {
  2804                // Force the '$md<xxx>.show()' promise to reject
  2805                self.deferred.reject(fault);
  2806  
  2807                // Continue rejection propagation
  2808                reject(fault);
  2809              }
  2810            });
  2811          }
  2812  
  2813          /**
  2814           * After the show process has finished/rejected:
  2815           * - announce 'removing',
  2816           * - perform the transition-out, and
  2817           * - perform optional clean up scope.
  2818           */
  2819          function transitionOutAndRemove(response, isCancelled, opts) {
  2820  
  2821            // abort if the show() and compile failed
  2822            if ( !element ) return $q.when(false);
  2823  
  2824            options = angular.extend(options || {}, opts || {});
  2825            options.cancelAutoHide && options.cancelAutoHide();
  2826            options.element.triggerHandler('$mdInterimElementRemove');
  2827  
  2828            if ( options.$destroy === true ) {
  2829  
  2830              return hideElement(options.element, options).then(function(){
  2831                (isCancelled && rejectAll(response)) || resolveAll(response);
  2832              });
  2833  
  2834            } else {
  2835  
  2836              $q.when(showAction)
  2837                  .finally(function() {
  2838                    hideElement(options.element, options).then(function() {
  2839  
  2840                      (isCancelled && rejectAll(response)) || resolveAll(response);
  2841  
  2842                    }, rejectAll);
  2843                  });
  2844  
  2845              return self.deferred.promise;
  2846            }
  2847  
  2848  
  2849            /**
  2850             * The `show()` returns a promise that will be resolved when the interim
  2851             * element is hidden or cancelled...
  2852             */
  2853            function resolveAll(response) {
  2854              self.deferred.resolve(response);
  2855            }
  2856  
  2857            /**
  2858             * Force the '$md<xxx>.show()' promise to reject
  2859             */
  2860            function rejectAll(fault) {
  2861              self.deferred.reject(fault);
  2862            }
  2863          }
  2864  
  2865          /**
  2866           * Prepare optional isolated scope and prepare $animate with default enter and leave
  2867           * transitions for the new element instance.
  2868           */
  2869          function configureScopeAndTransitions(options) {
  2870            options = options || { };
  2871            if ( options.template ) {
  2872              options.template = $mdUtil.processTemplate(options.template);
  2873            }
  2874  
  2875            return angular.extend({
  2876              preserveScope: false,
  2877              cancelAutoHide : angular.noop,
  2878              scope: options.scope || $rootScope.$new(options.isolateScope),
  2879  
  2880              /**
  2881               * Default usage to enable $animate to transition-in; can be easily overridden via 'options'
  2882               */
  2883              onShow: function transitionIn(scope, element, options) {
  2884                return $animate.enter(element, options.parent);
  2885              },
  2886  
  2887              /**
  2888               * Default usage to enable $animate to transition-out; can be easily overridden via 'options'
  2889               */
  2890              onRemove: function transitionOut(scope, element) {
  2891                // Element could be undefined if a new element is shown before
  2892                // the old one finishes compiling.
  2893                return element && $animate.leave(element) || $q.when();
  2894              }
  2895            }, options );
  2896  
  2897          }
  2898  
  2899          /**
  2900           * Compile an element with a templateUrl, controller, and locals
  2901           */
  2902          function compileElement(options) {
  2903  
  2904            var compiled = !options.skipCompile ? $mdCompiler.compile(options) : null;
  2905  
  2906            return compiled || $q(function (resolve) {
  2907                resolve({
  2908                  locals: {},
  2909                  link: function () {
  2910                    return options.element;
  2911                  }
  2912                });
  2913              });
  2914          }
  2915  
  2916          /**
  2917           *  Link an element with compiled configuration
  2918           */
  2919          function linkElement(compileData, options){
  2920            angular.extend(compileData.locals, options);
  2921  
  2922            var element = compileData.link(options.scope);
  2923  
  2924            // Search for parent at insertion time, if not specified
  2925            options.element = element;
  2926            options.parent = findParent(element, options);
  2927            if (options.themable) $mdTheming(element);
  2928  
  2929            return element;
  2930          }
  2931  
  2932          /**
  2933           * Search for parent at insertion time, if not specified
  2934           */
  2935          function findParent(element, options) {
  2936            var parent = options.parent;
  2937  
  2938            // Search for parent at insertion time, if not specified
  2939            if (angular.isFunction(parent)) {
  2940              parent = parent(options.scope, element, options);
  2941            } else if (angular.isString(parent)) {
  2942              parent = angular.element($document[0].querySelector(parent));
  2943            } else {
  2944              parent = angular.element(parent);
  2945            }
  2946  
  2947            // If parent querySelector/getter function fails, or it's just null,
  2948            // find a default.
  2949            if (!(parent || {}).length) {
  2950              var el;
  2951              if ($rootElement[0] && $rootElement[0].querySelector) {
  2952                el = $rootElement[0].querySelector(':not(svg) > body');
  2953              }
  2954              if (!el) el = $rootElement[0];
  2955              if (el.nodeName == '#comment') {
  2956                el = $document[0].body;
  2957              }
  2958              return angular.element(el);
  2959            }
  2960  
  2961            return parent;
  2962          }
  2963  
  2964          /**
  2965           * If auto-hide is enabled, start timer and prepare cancel function
  2966           */
  2967          function startAutoHide() {
  2968            var autoHideTimer, cancelAutoHide = angular.noop;
  2969  
  2970            if (options.hideDelay) {
  2971              autoHideTimer = $timeout(service.hide, options.hideDelay) ;
  2972              cancelAutoHide = function() {
  2973                $timeout.cancel(autoHideTimer);
  2974              }
  2975            }
  2976  
  2977            // Cache for subsequent use
  2978            options.cancelAutoHide = function() {
  2979              cancelAutoHide();
  2980              options.cancelAutoHide = undefined;
  2981            }
  2982          }
  2983  
  2984          /**
  2985           * Show the element ( with transitions), notify complete and start
  2986           * optional auto-Hide
  2987           */
  2988          function showElement(element, options, controller) {
  2989            // Trigger onShowing callback before the `show()` starts
  2990            var notifyShowing = options.onShowing || angular.noop;
  2991            // Trigger onComplete callback when the `show()` finishes
  2992            var notifyComplete = options.onComplete || angular.noop;
  2993  
  2994            notifyShowing(options.scope, element, options, controller);
  2995  
  2996            return $q(function (resolve, reject) {
  2997              try {
  2998                // Start transitionIn
  2999                $q.when(options.onShow(options.scope, element, options, controller))
  3000                  .then(function () {
  3001                    notifyComplete(options.scope, element, options);
  3002                    startAutoHide();
  3003  
  3004                    resolve(element);
  3005  
  3006                  }, reject );
  3007  
  3008              } catch(e) {
  3009                reject(e.message);
  3010              }
  3011            });
  3012          }
  3013  
  3014          function hideElement(element, options) {
  3015            var announceRemoving = options.onRemoving || angular.noop;
  3016  
  3017            return $$q(function (resolve, reject) {
  3018              try {
  3019                // Start transitionIn
  3020                var action = $$q.when( options.onRemove(options.scope, element, options) || true );
  3021  
  3022                // Trigger callback *before* the remove operation starts
  3023                announceRemoving(element, action);
  3024  
  3025                if ( options.$destroy == true ) {
  3026  
  3027                  // For $destroy, onRemove should be synchronous
  3028                  resolve(element);
  3029  
  3030                } else {
  3031  
  3032                  // Wait until transition-out is done
  3033                  action.then(function () {
  3034  
  3035                    if (!options.preserveScope && options.scope ) {
  3036                      options.scope.$destroy();
  3037                    }
  3038  
  3039                    resolve(element);
  3040  
  3041                  }, reject );
  3042                }
  3043  
  3044              } catch(e) {
  3045                reject(e.message);
  3046              }
  3047            });
  3048          }
  3049  
  3050        }
  3051      };
  3052  
  3053    }
  3054  
  3055  }
  3056  
  3057  })();
  3058  (function(){
  3059  "use strict";
  3060  
  3061  (function() {
  3062    'use strict';
  3063  
  3064    var $mdUtil, $interpolate, $log;
  3065  
  3066    var SUFFIXES = /(-gt)?-(sm|md|lg|print)/g;
  3067    var WHITESPACE = /\s+/g;
  3068  
  3069    var FLEX_OPTIONS = ['grow', 'initial', 'auto', 'none', 'noshrink', 'nogrow' ];
  3070    var LAYOUT_OPTIONS = ['row', 'column'];
  3071    var ALIGNMENT_MAIN_AXIS= [ "", "start", "center", "end", "stretch", "space-around", "space-between" ];
  3072    var ALIGNMENT_CROSS_AXIS= [ "", "start", "center", "end", "stretch" ];
  3073  
  3074    var config = {
  3075      /**
  3076       * Enable directive attribute-to-class conversions
  3077       * Developers can use `<body md-layout-css />` to quickly
  3078       * disable the Layout directives and prohibit the injection of Layout classNames
  3079       */
  3080      enabled: true,
  3081  
  3082      /**
  3083       * List of mediaQuery breakpoints and associated suffixes
  3084       *
  3085       *   [
  3086       *    { suffix: "sm", mediaQuery: "screen and (max-width: 599px)" },
  3087       *    { suffix: "md", mediaQuery: "screen and (min-width: 600px) and (max-width: 959px)" }
  3088       *   ]
  3089       */
  3090      breakpoints: []
  3091    };
  3092  
  3093    registerLayoutAPI( angular.module('material.core.layout', ['ng']) );
  3094  
  3095    /**
  3096     *   registerLayoutAPI()
  3097     *
  3098     *   The original ngMaterial Layout solution used attribute selectors and CSS.
  3099     *
  3100     *  ```html
  3101     *  <div layout="column"> My Content </div>
  3102     *  ```
  3103     *
  3104     *  ```css
  3105     *  [layout] {
  3106     *    box-sizing: border-box;
  3107     *    display:flex;
  3108     *  }
  3109     *  [layout=column] {
  3110     *    flex-direction : column
  3111     *  }
  3112     *  ```
  3113     *
  3114     *  Use of attribute selectors creates significant performance impacts in some
  3115     *  browsers... mainly IE.
  3116     *
  3117     *  This module registers directives that allow the same layout attributes to be
  3118     *  interpreted and converted to class selectors. The directive will add equivalent classes to each element that
  3119     *  contains a Layout directive.
  3120     *
  3121     * ```html
  3122     *   <div layout="column" class="layout layout-column"> My Content </div>
  3123     *```
  3124     *
  3125     *  ```css
  3126     *  .layout {
  3127     *    box-sizing: border-box;
  3128     *    display:flex;
  3129     *  }
  3130     *  .layout-column {
  3131     *    flex-direction : column
  3132     *  }
  3133     *  ```
  3134     */
  3135    function registerLayoutAPI(module){
  3136      var PREFIX_REGEXP = /^((?:x|data)[\:\-_])/i;
  3137      var SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g;
  3138  
  3139      // NOTE: these are also defined in constants::MEDIA_PRIORITY and constants::MEDIA
  3140      var BREAKPOINTS     = [ "", "xs", "gt-xs", "sm", "gt-sm", "md", "gt-md", "lg", "gt-lg", "xl", "print" ];
  3141      var API_WITH_VALUES = [ "layout", "flex", "flex-order", "flex-offset", "layout-align" ];
  3142      var API_NO_VALUES   = [ "show", "hide", "layout-padding", "layout-margin" ];
  3143  
  3144  
  3145      // Build directive registration functions for the standard Layout API... for all breakpoints.
  3146      angular.forEach(BREAKPOINTS, function(mqb) {
  3147  
  3148        // Attribute directives with expected, observable value(s)
  3149        angular.forEach( API_WITH_VALUES, function(name){
  3150          var fullName = mqb ? name + "-" + mqb : name;
  3151          module.directive( directiveNormalize(fullName), attributeWithObserve(fullName));
  3152        });
  3153  
  3154        // Attribute directives with no expected value(s)
  3155        angular.forEach( API_NO_VALUES, function(name){
  3156          var fullName = mqb ? name + "-" + mqb : name;
  3157          module.directive( directiveNormalize(fullName), attributeWithoutValue(fullName));
  3158        });
  3159  
  3160      });
  3161  
  3162      // Register other, special directive functions for the Layout features:
  3163      module
  3164        .directive('mdLayoutCss'  , disableLayoutDirective )
  3165        .directive('ngCloak'      ,  buildCloakInterceptor('ng-cloak'))
  3166  
  3167        .directive('layoutWrap'   , attributeWithoutValue('layout-wrap'))
  3168        .directive('layoutNowrap' , attributeWithoutValue('layout-nowrap'))
  3169        .directive('layoutNoWrap' , attributeWithoutValue('layout-no-wrap'))
  3170        .directive('layoutFill'   , attributeWithoutValue('layout-fill'))
  3171  
  3172        // !! Deprecated attributes: use the `-lt` (aka less-than) notations
  3173  
  3174        .directive('layoutLtMd'     , warnAttrNotSupported('layout-lt-md', true))
  3175        .directive('layoutLtLg'     , warnAttrNotSupported('layout-lt-lg', true))
  3176        .directive('flexLtMd'       , warnAttrNotSupported('flex-lt-md', true))
  3177        .directive('flexLtLg'       , warnAttrNotSupported('flex-lt-lg', true))
  3178  
  3179        .directive('layoutAlignLtMd', warnAttrNotSupported('layout-align-lt-md'))
  3180        .directive('layoutAlignLtLg', warnAttrNotSupported('layout-align-lt-lg'))
  3181        .directive('flexOrderLtMd'  , warnAttrNotSupported('flex-order-lt-md'))
  3182        .directive('flexOrderLtLg'  , warnAttrNotSupported('flex-order-lt-lg'))
  3183        .directive('offsetLtMd'     , warnAttrNotSupported('flex-offset-lt-md'))
  3184        .directive('offsetLtLg'     , warnAttrNotSupported('flex-offset-lt-lg'))
  3185  
  3186        .directive('hideLtMd'       , warnAttrNotSupported('hide-lt-md'))
  3187        .directive('hideLtLg'       , warnAttrNotSupported('hide-lt-lg'))
  3188        .directive('showLtMd'       , warnAttrNotSupported('show-lt-md'))
  3189        .directive('showLtLg'       , warnAttrNotSupported('show-lt-lg'));
  3190  
  3191      /**
  3192       * Converts snake_case to camelCase.
  3193       * Also there is special case for Moz prefix starting with upper case letter.
  3194       * @param name Name to normalize
  3195       */
  3196      function directiveNormalize(name) {
  3197        return name
  3198          .replace(PREFIX_REGEXP, '')
  3199          .replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) {
  3200            return offset ? letter.toUpperCase() : letter;
  3201          });
  3202      }
  3203  
  3204    }
  3205  
  3206    /**
  3207     * Special directive that will disable ALL Layout conversions of layout
  3208     * attribute(s) to classname(s).
  3209     *
  3210     * <link rel="stylesheet" href="angular-material.min.css">
  3211     * <link rel="stylesheet" href="angular-material.layout.css">
  3212     *
  3213     * <body md-layout-css>
  3214     *  ...
  3215     * </body>
  3216     *
  3217     * Note: Using md-layout-css directive requires the developer to load the Material
  3218     * Layout Attribute stylesheet (which only uses attribute selectors):
  3219     *
  3220     *       `angular-material.layout.css`
  3221     *
  3222     * Another option is to use the LayoutProvider to configure and disable the attribute
  3223     * conversions; this would obviate the use of the `md-layout-css` directive
  3224     *
  3225     */
  3226    function disableLayoutDirective() {
  3227      return {
  3228        restrict : 'A',
  3229        priority : '900',
  3230        compile  : function(element, attr) {
  3231          config.enabled = false;
  3232          return angular.noop;
  3233        }
  3234      };
  3235    }
  3236  
  3237    /**
  3238     * Tail-hook ngCloak to delay the uncloaking while Layout transformers
  3239     * finish processing. Eliminates flicker with Material.Layoouts
  3240     */
  3241    function buildCloakInterceptor(className) {
  3242      return [ '$timeout', function($timeout){
  3243        return {
  3244          restrict : 'A',
  3245          priority : -10,   // run after normal ng-cloak
  3246          compile  : function( element ) {
  3247            if (!config.enabled) return angular.noop;
  3248  
  3249            // Re-add the cloak
  3250            element.addClass(className);
  3251  
  3252            return function( scope, element ) {
  3253              // Wait while layout injectors configure, then uncloak
  3254              // NOTE: $rAF does not delay enough... and this is a 1x-only event,
  3255              //       $timeout is acceptable.
  3256              $timeout( function(){
  3257                element.removeClass(className);
  3258              }, 10, false);
  3259            };
  3260          }
  3261        };
  3262      }];
  3263    }
  3264  
  3265  
  3266    // *********************************************************************************
  3267    //
  3268    // These functions create registration functions for ngMaterial Layout attribute directives
  3269    // This provides easy translation to switch ngMaterial attribute selectors to
  3270    // CLASS selectors and directives; which has huge performance implications
  3271    // for IE Browsers
  3272    //
  3273    // *********************************************************************************
  3274  
  3275    /**
  3276     * Creates a directive registration function where a possible dynamic attribute
  3277     * value will be observed/watched.
  3278     * @param {string} className attribute name; eg `layout-gt-md` with value ="row"
  3279     */
  3280    function attributeWithObserve(className) {
  3281  
  3282      return ['$mdUtil', '$interpolate', "$log", function(_$mdUtil_, _$interpolate_, _$log_) {
  3283        $mdUtil = _$mdUtil_;
  3284        $interpolate = _$interpolate_;
  3285        $log = _$log_;
  3286  
  3287        return {
  3288          restrict: 'A',
  3289          compile: function(element, attr) {
  3290            var linkFn;
  3291            if (config.enabled) {
  3292              // immediately replace static (non-interpolated) invalid values...
  3293  
  3294              validateAttributeUsage(className, attr, element, $log);
  3295  
  3296              validateAttributeValue( className,
  3297                getNormalizedAttrValue(className, attr, ""),
  3298                buildUpdateFn(element, className, attr)
  3299              );
  3300  
  3301              linkFn = translateWithValueToCssClass;
  3302            }
  3303  
  3304            // Use for postLink to account for transforms after ng-transclude.
  3305            return linkFn || angular.noop;
  3306          }
  3307        };
  3308      }];
  3309  
  3310      /**
  3311       * Add as transformed class selector(s), then
  3312       * remove the deprecated attribute selector
  3313       */
  3314      function translateWithValueToCssClass(scope, element, attrs) {
  3315        var updateFn = updateClassWithValue(element, className, attrs);
  3316        var unwatch = attrs.$observe(attrs.$normalize(className), updateFn);
  3317  
  3318        updateFn(getNormalizedAttrValue(className, attrs, ""));
  3319        scope.$on("$destroy", function() { unwatch() });
  3320      }
  3321    }
  3322  
  3323    /**
  3324     * Creates a registration function for ngMaterial Layout attribute directive.
  3325     * This is a `simple` transpose of attribute usage to class usage; where we ignore
  3326     * any attribute value
  3327     */
  3328    function attributeWithoutValue(className) {
  3329      return ['$mdUtil', '$interpolate', "$log", function(_$mdUtil_, _$interpolate_, _$log_) {
  3330        $mdUtil = _$mdUtil_;
  3331        $interpolate = _$interpolate_;
  3332        $log = _$log_;
  3333  
  3334        return {
  3335          restrict: 'A',
  3336          compile: function(element, attr) {
  3337            var linkFn;
  3338            if (config.enabled) {
  3339              // immediately replace static (non-interpolated) invalid values...
  3340  
  3341              validateAttributeValue( className,
  3342                getNormalizedAttrValue(className, attr, ""),
  3343                buildUpdateFn(element, className, attr)
  3344              );
  3345  
  3346              translateToCssClass(null, element);
  3347  
  3348              // Use for postLink to account for transforms after ng-transclude.
  3349              linkFn = translateToCssClass;
  3350            }
  3351  
  3352            return linkFn || angular.noop;
  3353          }
  3354        };
  3355      }];
  3356  
  3357      /**
  3358       * Add as transformed class selector, then
  3359       * remove the deprecated attribute selector
  3360       */
  3361      function translateToCssClass(scope, element) {
  3362        element.addClass(className);
  3363      }
  3364    }
  3365  
  3366  
  3367  
  3368    /**
  3369     * After link-phase, do NOT remove deprecated layout attribute selector.
  3370     * Instead watch the attribute so interpolated data-bindings to layout
  3371     * selectors will continue to be supported.
  3372     *
  3373     * $observe() the className and update with new class (after removing the last one)
  3374     *
  3375     * e.g. `layout="{{layoutDemo.direction}}"` will update...
  3376     *
  3377     * NOTE: The value must match one of the specified styles in the CSS.
  3378     * For example `flex-gt-md="{{size}}`  where `scope.size == 47` will NOT work since
  3379     * only breakpoints for 0, 5, 10, 15... 100, 33, 34, 66, 67 are defined.
  3380     *
  3381     */
  3382    function updateClassWithValue(element, className) {
  3383      var lastClass;
  3384  
  3385      return function updateClassFn(newValue) {
  3386        var value = validateAttributeValue(className, newValue || "");
  3387        if ( angular.isDefined(value) ) {
  3388          if (lastClass) element.removeClass(lastClass);
  3389          lastClass = !value ? className : className + "-" + value.replace(WHITESPACE, "-");
  3390          element.addClass(lastClass);
  3391        }
  3392      };
  3393    }
  3394  
  3395    /**
  3396     * Provide console warning that this layout attribute has been deprecated
  3397     *
  3398     */
  3399    function warnAttrNotSupported(className) {
  3400      var parts = className.split("-");
  3401      return ["$log", function($log) {
  3402        $log.warn(className + "has been deprecated. Please use a `" + parts[0] + "-gt-<xxx>` variant.");
  3403        return angular.noop;
  3404      }];
  3405    }
  3406  
  3407    /**
  3408     * Centralize warnings for known flexbox issues (especially IE-related issues)
  3409     */
  3410    function validateAttributeUsage(className, attr, element, $log){
  3411      var message, usage, url;
  3412      var nodeName = element[0].nodeName.toLowerCase();
  3413  
  3414      switch(className.replace(SUFFIXES,"")) {
  3415        case "flex":
  3416          if ((nodeName == "md-button") || (nodeName == "fieldset")){
  3417            // @see https://github.com/philipwalton/flexbugs#9-some-html-elements-cant-be-flex-containers
  3418            // Use <div flex> wrapper inside (preferred) or outside
  3419  
  3420            usage = "<" + nodeName + " " + className + "></" + nodeName + ">";
  3421            url = "https://github.com/philipwalton/flexbugs#9-some-html-elements-cant-be-flex-containers";
  3422            message = "Markup '{0}' may not work as expected in IE Browsers. Consult '{1}' for details.";
  3423  
  3424            $log.warn( $mdUtil.supplant(message, [usage, url]) );
  3425          }
  3426      }
  3427  
  3428    }
  3429  
  3430  
  3431    /**
  3432     * For the Layout attribute value, validate or replace with default
  3433     * fallback value
  3434     */
  3435    function validateAttributeValue(className, value, updateFn) {
  3436      var origValue = value;
  3437  
  3438      if (!needsInterpolation(value)) {
  3439        switch (className.replace(SUFFIXES,"")) {
  3440          case 'layout'        :
  3441            if ( !findIn(value, LAYOUT_OPTIONS) ) {
  3442              value = LAYOUT_OPTIONS[0];    // 'row';
  3443            }
  3444            break;
  3445  
  3446          case 'flex'          :
  3447            if (!findIn(value, FLEX_OPTIONS)) {
  3448              if (isNaN(value)) {
  3449                value = '';
  3450              }
  3451            }
  3452            break;
  3453  
  3454          case 'flex-offset' :
  3455          case 'flex-order'    :
  3456            if (!value || isNaN(+value)) {
  3457              value = '0';
  3458            }
  3459            break;
  3460  
  3461          case 'layout-align'  :
  3462            var axis = extractAlignAxis(value);
  3463            value = $mdUtil.supplant("{main}-{cross}",axis);
  3464            break;
  3465  
  3466          case 'layout-padding' :
  3467          case 'layout-margin'  :
  3468          case 'layout-fill'    :
  3469          case 'layout-wrap'    :
  3470          case 'layout-nowrap'  :
  3471          case 'layout-nowrap' :
  3472            value = '';
  3473            break;
  3474        }
  3475  
  3476        if (value != origValue) {
  3477          (updateFn || angular.noop)(value);
  3478        }
  3479      }
  3480  
  3481      return value;
  3482    }
  3483  
  3484    /**
  3485     * Replace current attribute value with fallback value
  3486     */
  3487    function buildUpdateFn(element, className, attrs) {
  3488      return function updateAttrValue(fallback) {
  3489        if (!needsInterpolation(fallback)) {
  3490          // Do not modify the element's attribute value; so
  3491          // uses '<ui-layout layout="/api/sidebar.html" />' will not
  3492          // be affected. Just update the attrs value.
  3493          attrs[attrs.$normalize(className)] = fallback;
  3494        }
  3495      };
  3496    }
  3497  
  3498    /**
  3499     * See if the original value has interpolation symbols:
  3500     * e.g.  flex-gt-md="{{triggerPoint}}"
  3501     */
  3502    function needsInterpolation(value) {
  3503      return (value || "").indexOf($interpolate.startSymbol()) > -1;
  3504    }
  3505  
  3506    function getNormalizedAttrValue(className, attrs, defaultVal) {
  3507      var normalizedAttr = attrs.$normalize(className);
  3508      return attrs[normalizedAttr] ? attrs[normalizedAttr].replace(WHITESPACE, "-") : defaultVal || null;
  3509    }
  3510  
  3511    function findIn(item, list, replaceWith) {
  3512      item = replaceWith && item ? item.replace(WHITESPACE, replaceWith) : item;
  3513  
  3514      var found = false;
  3515      if (item) {
  3516        list.forEach(function(it) {
  3517          it = replaceWith ? it.replace(WHITESPACE, replaceWith) : it;
  3518          found = found || (it === item);
  3519        });
  3520      }
  3521      return found;
  3522    }
  3523  
  3524    function extractAlignAxis(attrValue) {
  3525      var axis = {
  3526        main : "start",
  3527        cross: "stretch"
  3528      }, values;
  3529  
  3530      attrValue = (attrValue || "");
  3531  
  3532      if ( attrValue.indexOf("-") == 0 || attrValue.indexOf(" ") == 0) {
  3533        // For missing main-axis values
  3534        attrValue = "none" + attrValue;
  3535      }
  3536  
  3537      values = attrValue.toLowerCase().trim().replace(WHITESPACE, "-").split("-");
  3538      if ( values.length && (values[0] === "space") ) {
  3539        // for main-axis values of "space-around" or "space-between"
  3540        values = [ values[0]+"-"+values[1],values[2] ];
  3541      }
  3542  
  3543      if ( values.length > 0 ) axis.main  = values[0] || axis.main;
  3544      if ( values.length > 1 ) axis.cross = values[1] || axis.cross;
  3545  
  3546      if ( ALIGNMENT_MAIN_AXIS.indexOf(axis.main) < 0 )   axis.main = "start";
  3547      if ( ALIGNMENT_CROSS_AXIS.indexOf(axis.cross) < 0 ) axis.cross = "stretch";
  3548  
  3549      return axis;
  3550    }
  3551  
  3552  
  3553  })();
  3554  
  3555  })();
  3556  (function(){
  3557  "use strict";
  3558  
  3559    /**
  3560     * @ngdoc module
  3561     * @name material.core.componentRegistry
  3562     *
  3563     * @description
  3564     * A component instance registration service.
  3565     * Note: currently this as a private service in the SideNav component.
  3566     */
  3567    angular.module('material.core')
  3568      .factory('$mdComponentRegistry', ComponentRegistry);
  3569  
  3570    /*
  3571     * @private
  3572     * @ngdoc factory
  3573     * @name ComponentRegistry
  3574     * @module material.core.componentRegistry
  3575     *
  3576     */
  3577    function ComponentRegistry($log, $q) {
  3578  
  3579      var self;
  3580      var instances = [ ];
  3581      var pendings = { };
  3582  
  3583      return self = {
  3584        /**
  3585         * Used to print an error when an instance for a handle isn't found.
  3586         */
  3587        notFoundError: function(handle) {
  3588          $log.error('No instance found for handle', handle);
  3589        },
  3590        /**
  3591         * Return all registered instances as an array.
  3592         */
  3593        getInstances: function() {
  3594          return instances;
  3595        },
  3596  
  3597        /**
  3598         * Get a registered instance.
  3599         * @param handle the String handle to look up for a registered instance.
  3600         */
  3601        get: function(handle) {
  3602          if ( !isValidID(handle) ) return null;
  3603  
  3604          var i, j, instance;
  3605          for(i = 0, j = instances.length; i < j; i++) {
  3606            instance = instances[i];
  3607            if(instance.$$mdHandle === handle) {
  3608              return instance;
  3609            }
  3610          }
  3611          return null;
  3612        },
  3613  
  3614        /**
  3615         * Register an instance.
  3616         * @param instance the instance to register
  3617         * @param handle the handle to identify the instance under.
  3618         */
  3619        register: function(instance, handle) {
  3620          if ( !handle ) return angular.noop;
  3621  
  3622          instance.$$mdHandle = handle;
  3623          instances.push(instance);
  3624          resolveWhen();
  3625  
  3626          return deregister;
  3627  
  3628          /**
  3629           * Remove registration for an instance
  3630           */
  3631          function deregister() {
  3632            var index = instances.indexOf(instance);
  3633            if (index !== -1) {
  3634              instances.splice(index, 1);
  3635            }
  3636          }
  3637  
  3638          /**
  3639           * Resolve any pending promises for this instance
  3640           */
  3641          function resolveWhen() {
  3642            var dfd = pendings[handle];
  3643            if ( dfd ) {
  3644              dfd.resolve( instance );
  3645              delete pendings[handle];
  3646            }
  3647          }
  3648        },
  3649  
  3650        /**
  3651         * Async accessor to registered component instance
  3652         * If not available then a promise is created to notify
  3653         * all listeners when the instance is registered.
  3654         */
  3655        when : function(handle) {
  3656          if ( isValidID(handle) ) {
  3657            var deferred = $q.defer();
  3658            var instance = self.get(handle);
  3659  
  3660            if ( instance )  {
  3661              deferred.resolve( instance );
  3662            } else {
  3663              pendings[handle] = deferred;
  3664            }
  3665  
  3666            return deferred.promise;
  3667          }
  3668          return $q.reject("Invalid `md-component-id` value.");
  3669        }
  3670  
  3671      };
  3672  
  3673      function isValidID(handle){
  3674        return handle && (handle !== "");
  3675      }
  3676  
  3677    }
  3678    ComponentRegistry.$inject = ["$log", "$q"];
  3679  
  3680  })();
  3681  (function(){
  3682  "use strict";
  3683  
  3684  angular.module('material.core.theming.palette', [])
  3685  .constant('$mdColorPalette', {
  3686    'red': {
  3687      '50': '#ffebee',
  3688      '100': '#ffcdd2',
  3689      '200': '#ef9a9a',
  3690      '300': '#e57373',
  3691      '400': '#ef5350',
  3692      '500': '#f44336',
  3693      '600': '#e53935',
  3694      '700': '#d32f2f',
  3695      '800': '#c62828',
  3696      '900': '#b71c1c',
  3697      'A100': '#ff8a80',
  3698      'A200': '#ff5252',
  3699      'A400': '#ff1744',
  3700      'A700': '#d50000',
  3701      'contrastDefaultColor': 'light',
  3702      'contrastDarkColors': '50 100 200 300 A100',
  3703      'contrastStrongLightColors': '400 500 600 700 A200 A400 A700'
  3704    },
  3705    'pink': {
  3706      '50': '#fce4ec',
  3707      '100': '#f8bbd0',
  3708      '200': '#f48fb1',
  3709      '300': '#f06292',
  3710      '400': '#ec407a',
  3711      '500': '#e91e63',
  3712      '600': '#d81b60',
  3713      '700': '#c2185b',
  3714      '800': '#ad1457',
  3715      '900': '#880e4f',
  3716      'A100': '#ff80ab',
  3717      'A200': '#ff4081',
  3718      'A400': '#f50057',
  3719      'A700': '#c51162',
  3720      'contrastDefaultColor': 'light',
  3721      'contrastDarkColors': '50 100 200 A100',
  3722      'contrastStrongLightColors': '500 600 A200 A400 A700'
  3723    },
  3724    'purple': {
  3725      '50': '#f3e5f5',
  3726      '100': '#e1bee7',
  3727      '200': '#ce93d8',
  3728      '300': '#ba68c8',
  3729      '400': '#ab47bc',
  3730      '500': '#9c27b0',
  3731      '600': '#8e24aa',
  3732      '700': '#7b1fa2',
  3733      '800': '#6a1b9a',
  3734      '900': '#4a148c',
  3735      'A100': '#ea80fc',
  3736      'A200': '#e040fb',
  3737      'A400': '#d500f9',
  3738      'A700': '#aa00ff',
  3739      'contrastDefaultColor': 'light',
  3740      'contrastDarkColors': '50 100 200 A100',
  3741      'contrastStrongLightColors': '300 400 A200 A400 A700'
  3742    },
  3743    'deep-purple': {
  3744      '50': '#ede7f6',
  3745      '100': '#d1c4e9',
  3746      '200': '#b39ddb',
  3747      '300': '#9575cd',
  3748      '400': '#7e57c2',
  3749      '500': '#673ab7',
  3750      '600': '#5e35b1',
  3751      '700': '#512da8',
  3752      '800': '#4527a0',
  3753      '900': '#311b92',
  3754      'A100': '#b388ff',
  3755      'A200': '#7c4dff',
  3756      'A400': '#651fff',
  3757      'A700': '#6200ea',
  3758      'contrastDefaultColor': 'light',
  3759      'contrastDarkColors': '50 100 200 A100',
  3760      'contrastStrongLightColors': '300 400 A200'
  3761    },
  3762    'indigo': {
  3763      '50': '#e8eaf6',
  3764      '100': '#c5cae9',
  3765      '200': '#9fa8da',
  3766      '300': '#7986cb',
  3767      '400': '#5c6bc0',
  3768      '500': '#3f51b5',
  3769      '600': '#3949ab',
  3770      '700': '#303f9f',
  3771      '800': '#283593',
  3772      '900': '#1a237e',
  3773      'A100': '#8c9eff',
  3774      'A200': '#536dfe',
  3775      'A400': '#3d5afe',
  3776      'A700': '#304ffe',
  3777      'contrastDefaultColor': 'light',
  3778      'contrastDarkColors': '50 100 200 A100',
  3779      'contrastStrongLightColors': '300 400 A200 A400'
  3780    },
  3781    'blue': {
  3782      '50': '#e3f2fd',
  3783      '100': '#bbdefb',
  3784      '200': '#90caf9',
  3785      '300': '#64b5f6',
  3786      '400': '#42a5f5',
  3787      '500': '#2196f3',
  3788      '600': '#1e88e5',
  3789      '700': '#1976d2',
  3790      '800': '#1565c0',
  3791      '900': '#0d47a1',
  3792      'A100': '#82b1ff',
  3793      'A200': '#448aff',
  3794      'A400': '#2979ff',
  3795      'A700': '#2962ff',
  3796      'contrastDefaultColor': 'light',
  3797      'contrastDarkColors': '50 100 200 300 400 A100',
  3798      'contrastStrongLightColors': '500 600 700 A200 A400 A700'
  3799    },
  3800    'light-blue': {
  3801      '50': '#e1f5fe',
  3802      '100': '#b3e5fc',
  3803      '200': '#81d4fa',
  3804      '300': '#4fc3f7',
  3805      '400': '#29b6f6',
  3806      '500': '#03a9f4',
  3807      '600': '#039be5',
  3808      '700': '#0288d1',
  3809      '800': '#0277bd',
  3810      '900': '#01579b',
  3811      'A100': '#80d8ff',
  3812      'A200': '#40c4ff',
  3813      'A400': '#00b0ff',
  3814      'A700': '#0091ea',
  3815      'contrastDefaultColor': 'dark',
  3816      'contrastLightColors': '600 700 800 900 A700',
  3817      'contrastStrongLightColors': '600 700 800 A700'
  3818    },
  3819    'cyan': {
  3820      '50': '#e0f7fa',
  3821      '100': '#b2ebf2',
  3822      '200': '#80deea',
  3823      '300': '#4dd0e1',
  3824      '400': '#26c6da',
  3825      '500': '#00bcd4',
  3826      '600': '#00acc1',
  3827      '700': '#0097a7',
  3828      '800': '#00838f',
  3829      '900': '#006064',
  3830      'A100': '#84ffff',
  3831      'A200': '#18ffff',
  3832      'A400': '#00e5ff',
  3833      'A700': '#00b8d4',
  3834      'contrastDefaultColor': 'dark',
  3835      'contrastLightColors': '700 800 900',
  3836      'contrastStrongLightColors': '700 800 900'
  3837    },
  3838    'teal': {
  3839      '50': '#e0f2f1',
  3840      '100': '#b2dfdb',
  3841      '200': '#80cbc4',
  3842      '300': '#4db6ac',
  3843      '400': '#26a69a',
  3844      '500': '#009688',
  3845      '600': '#00897b',
  3846      '700': '#00796b',
  3847      '800': '#00695c',
  3848      '900': '#004d40',
  3849      'A100': '#a7ffeb',
  3850      'A200': '#64ffda',
  3851      'A400': '#1de9b6',
  3852      'A700': '#00bfa5',
  3853      'contrastDefaultColor': 'dark',
  3854      'contrastLightColors': '500 600 700 800 900',
  3855      'contrastStrongLightColors': '500 600 700'
  3856    },
  3857    'green': {
  3858      '50': '#e8f5e9',
  3859      '100': '#c8e6c9',
  3860      '200': '#a5d6a7',
  3861      '300': '#81c784',
  3862      '400': '#66bb6a',
  3863      '500': '#4caf50',
  3864      '600': '#43a047',
  3865      '700': '#388e3c',
  3866      '800': '#2e7d32',
  3867      '900': '#1b5e20',
  3868      'A100': '#b9f6ca',
  3869      'A200': '#69f0ae',
  3870      'A400': '#00e676',
  3871      'A700': '#00c853',
  3872      'contrastDefaultColor': 'dark',
  3873      'contrastLightColors': '600 700 800 900',
  3874      'contrastStrongLightColors': '600 700'
  3875    },
  3876    'light-green': {
  3877      '50': '#f1f8e9',
  3878      '100': '#dcedc8',
  3879      '200': '#c5e1a5',
  3880      '300': '#aed581',
  3881      '400': '#9ccc65',
  3882      '500': '#8bc34a',
  3883      '600': '#7cb342',
  3884      '700': '#689f38',
  3885      '800': '#558b2f',
  3886      '900': '#33691e',
  3887      'A100': '#ccff90',
  3888      'A200': '#b2ff59',
  3889      'A400': '#76ff03',
  3890      'A700': '#64dd17',
  3891      'contrastDefaultColor': 'dark',
  3892      'contrastLightColors': '700 800 900',
  3893      'contrastStrongLightColors': '700 800 900'
  3894    },
  3895    'lime': {
  3896      '50': '#f9fbe7',
  3897      '100': '#f0f4c3',
  3898      '200': '#e6ee9c',
  3899      '300': '#dce775',
  3900      '400': '#d4e157',
  3901      '500': '#cddc39',
  3902      '600': '#c0ca33',
  3903      '700': '#afb42b',
  3904      '800': '#9e9d24',
  3905      '900': '#827717',
  3906      'A100': '#f4ff81',
  3907      'A200': '#eeff41',
  3908      'A400': '#c6ff00',
  3909      'A700': '#aeea00',
  3910      'contrastDefaultColor': 'dark',
  3911      'contrastLightColors': '900',
  3912      'contrastStrongLightColors': '900'
  3913    },
  3914    'yellow': {
  3915      '50': '#fffde7',
  3916      '100': '#fff9c4',
  3917      '200': '#fff59d',
  3918      '300': '#fff176',
  3919      '400': '#ffee58',
  3920      '500': '#ffeb3b',
  3921      '600': '#fdd835',
  3922      '700': '#fbc02d',
  3923      '800': '#f9a825',
  3924      '900': '#f57f17',
  3925      'A100': '#ffff8d',
  3926      'A200': '#ffff00',
  3927      'A400': '#ffea00',
  3928      'A700': '#ffd600',
  3929      'contrastDefaultColor': 'dark'
  3930    },
  3931    'amber': {
  3932      '50': '#fff8e1',
  3933      '100': '#ffecb3',
  3934      '200': '#ffe082',
  3935      '300': '#ffd54f',
  3936      '400': '#ffca28',
  3937      '500': '#ffc107',
  3938      '600': '#ffb300',
  3939      '700': '#ffa000',
  3940      '800': '#ff8f00',
  3941      '900': '#ff6f00',
  3942      'A100': '#ffe57f',
  3943      'A200': '#ffd740',
  3944      'A400': '#ffc400',
  3945      'A700': '#ffab00',
  3946      'contrastDefaultColor': 'dark'
  3947    },
  3948    'orange': {
  3949      '50': '#fff3e0',
  3950      '100': '#ffe0b2',
  3951      '200': '#ffcc80',
  3952      '300': '#ffb74d',
  3953      '400': '#ffa726',
  3954      '500': '#ff9800',
  3955      '600': '#fb8c00',
  3956      '700': '#f57c00',
  3957      '800': '#ef6c00',
  3958      '900': '#e65100',
  3959      'A100': '#ffd180',
  3960      'A200': '#ffab40',
  3961      'A400': '#ff9100',
  3962      'A700': '#ff6d00',
  3963      'contrastDefaultColor': 'dark',
  3964      'contrastLightColors': '800 900',
  3965      'contrastStrongLightColors': '800 900'
  3966    },
  3967    'deep-orange': {
  3968      '50': '#fbe9e7',
  3969      '100': '#ffccbc',
  3970      '200': '#ffab91',
  3971      '300': '#ff8a65',
  3972      '400': '#ff7043',
  3973      '500': '#ff5722',
  3974      '600': '#f4511e',
  3975      '700': '#e64a19',
  3976      '800': '#d84315',
  3977      '900': '#bf360c',
  3978      'A100': '#ff9e80',
  3979      'A200': '#ff6e40',
  3980      'A400': '#ff3d00',
  3981      'A700': '#dd2c00',
  3982      'contrastDefaultColor': 'light',
  3983      'contrastDarkColors': '50 100 200 300 400 A100 A200',
  3984      'contrastStrongLightColors': '500 600 700 800 900 A400 A700'
  3985    },
  3986    'brown': {
  3987      '50': '#efebe9',
  3988      '100': '#d7ccc8',
  3989      '200': '#bcaaa4',
  3990      '300': '#a1887f',
  3991      '400': '#8d6e63',
  3992      '500': '#795548',
  3993      '600': '#6d4c41',
  3994      '700': '#5d4037',
  3995      '800': '#4e342e',
  3996      '900': '#3e2723',
  3997      'A100': '#d7ccc8',
  3998      'A200': '#bcaaa4',
  3999      'A400': '#8d6e63',
  4000      'A700': '#5d4037',
  4001      'contrastDefaultColor': 'light',
  4002      'contrastDarkColors': '50 100 200',
  4003      'contrastStrongLightColors': '300 400'
  4004    },
  4005    'grey': {
  4006      '50': '#fafafa',
  4007      '100': '#f5f5f5',
  4008      '200': '#eeeeee',
  4009      '300': '#e0e0e0',
  4010      '400': '#bdbdbd',
  4011      '500': '#9e9e9e',
  4012      '600': '#757575',
  4013      '700': '#616161',
  4014      '800': '#424242',
  4015      '900': '#212121',
  4016      '1000': '#000000',
  4017      'A100': '#ffffff',
  4018      'A200': '#eeeeee',
  4019      'A400': '#bdbdbd',
  4020      'A700': '#616161',
  4021      'contrastDefaultColor': 'dark',
  4022      'contrastLightColors': '600 700 800 900'
  4023    },
  4024    'blue-grey': {
  4025      '50': '#eceff1',
  4026      '100': '#cfd8dc',
  4027      '200': '#b0bec5',
  4028      '300': '#90a4ae',
  4029      '400': '#78909c',
  4030      '500': '#607d8b',
  4031      '600': '#546e7a',
  4032      '700': '#455a64',
  4033      '800': '#37474f',
  4034      '900': '#263238',
  4035      'A100': '#cfd8dc',
  4036      'A200': '#b0bec5',
  4037      'A400': '#78909c',
  4038      'A700': '#455a64',
  4039      'contrastDefaultColor': 'light',
  4040      'contrastDarkColors': '50 100 200 300',
  4041      'contrastStrongLightColors': '400 500'
  4042    }
  4043  });
  4044  
  4045  })();
  4046  (function(){
  4047  "use strict";
  4048  
  4049  angular.module('material.core.theming', ['material.core.theming.palette'])
  4050    .directive('mdTheme', ThemingDirective)
  4051    .directive('mdThemable', ThemableDirective)
  4052    .provider('$mdTheming', ThemingProvider)
  4053    .run(generateAllThemes);
  4054  
  4055  /**
  4056   * @ngdoc service
  4057   * @name $mdThemingProvider
  4058   * @module material.core.theming
  4059   *
  4060   * @description Provider to configure the `$mdTheming` service.
  4061   */
  4062  
  4063  /**
  4064   * @ngdoc method
  4065   * @name $mdThemingProvider#setNonce
  4066   * @param {string} nonceValue The nonce to be added as an attribute to the theme style tags.
  4067   * Setting a value allows the use CSP policy without using the unsafe-inline directive.
  4068   */
  4069  
  4070  /**
  4071   * @ngdoc method
  4072   * @name $mdThemingProvider#setDefaultTheme
  4073   * @param {string} themeName Default theme name to be applied to elements. Default value is `default`.
  4074   */
  4075  
  4076  /**
  4077   * @ngdoc method
  4078   * @name $mdThemingProvider#alwaysWatchTheme
  4079   * @param {boolean} watch Whether or not to always watch themes for changes and re-apply
  4080   * classes when they change. Default is `false`. Enabling can reduce performance.
  4081   */
  4082  
  4083  /* Some Example Valid Theming Expressions
  4084   * =======================================
  4085   *
  4086   * Intention group expansion: (valid for primary, accent, warn, background)
  4087   *
  4088   * {{primary-100}} - grab shade 100 from the primary palette
  4089   * {{primary-100-0.7}} - grab shade 100, apply opacity of 0.7
  4090   * {{primary-100-contrast}} - grab shade 100's contrast color
  4091   * {{primary-hue-1}} - grab the shade assigned to hue-1 from the primary palette
  4092   * {{primary-hue-1-0.7}} - apply 0.7 opacity to primary-hue-1
  4093   * {{primary-color}} - Generates .md-hue-1, .md-hue-2, .md-hue-3 with configured shades set for each hue
  4094   * {{primary-color-0.7}} - Apply 0.7 opacity to each of the above rules
  4095   * {{primary-contrast}} - Generates .md-hue-1, .md-hue-2, .md-hue-3 with configured contrast (ie. text) color shades set for each hue
  4096   * {{primary-contrast-0.7}} - Apply 0.7 opacity to each of the above rules
  4097   *
  4098   * Foreground expansion: Applies rgba to black/white foreground text
  4099   *
  4100   * {{foreground-1}} - used for primary text
  4101   * {{foreground-2}} - used for secondary text/divider
  4102   * {{foreground-3}} - used for disabled text
  4103   * {{foreground-4}} - used for dividers
  4104   *
  4105   */
  4106  
  4107  // In memory generated CSS rules; registered by theme.name
  4108  var GENERATED = { };
  4109  
  4110  // In memory storage of defined themes and color palettes (both loaded by CSS, and user specified)
  4111  var PALETTES;
  4112  var THEMES;
  4113  
  4114  var DARK_FOREGROUND = {
  4115    name: 'dark',
  4116    '1': 'rgba(0,0,0,0.87)',
  4117    '2': 'rgba(0,0,0,0.54)',
  4118    '3': 'rgba(0,0,0,0.26)',
  4119    '4': 'rgba(0,0,0,0.12)'
  4120  };
  4121  var LIGHT_FOREGROUND = {
  4122    name: 'light',
  4123    '1': 'rgba(255,255,255,1.0)',
  4124    '2': 'rgba(255,255,255,0.7)',
  4125    '3': 'rgba(255,255,255,0.3)',
  4126    '4': 'rgba(255,255,255,0.12)'
  4127  };
  4128  
  4129  var DARK_SHADOW = '1px 1px 0px rgba(0,0,0,0.4), -1px -1px 0px rgba(0,0,0,0.4)';
  4130  var LIGHT_SHADOW = '';
  4131  
  4132  var DARK_CONTRAST_COLOR = colorToRgbaArray('rgba(0,0,0,0.87)');
  4133  var LIGHT_CONTRAST_COLOR = colorToRgbaArray('rgba(255,255,255,0.87)');
  4134  var STRONG_LIGHT_CONTRAST_COLOR = colorToRgbaArray('rgb(255,255,255)');
  4135  
  4136  var THEME_COLOR_TYPES = ['primary', 'accent', 'warn', 'background'];
  4137  var DEFAULT_COLOR_TYPE = 'primary';
  4138  
  4139  // A color in a theme will use these hues by default, if not specified by user.
  4140  var LIGHT_DEFAULT_HUES = {
  4141    'accent': {
  4142      'default': 'A200',
  4143      'hue-1': 'A100',
  4144      'hue-2': 'A400',
  4145      'hue-3': 'A700'
  4146    },
  4147    'background': {
  4148      'default': 'A100',
  4149      'hue-1': '300',
  4150      'hue-2': '800',
  4151      'hue-3': '900'
  4152    }
  4153  };
  4154  
  4155  var DARK_DEFAULT_HUES = {
  4156    'background': {
  4157      'default': '800',
  4158      'hue-1': '600',
  4159      'hue-2': '300',
  4160      'hue-3': '900'
  4161    }
  4162  };
  4163  THEME_COLOR_TYPES.forEach(function(colorType) {
  4164    // Color types with unspecified default hues will use these default hue values
  4165    var defaultDefaultHues = {
  4166      'default': '500',
  4167      'hue-1': '300',
  4168      'hue-2': '800',
  4169      'hue-3': 'A100'
  4170    };
  4171    if (!LIGHT_DEFAULT_HUES[colorType]) LIGHT_DEFAULT_HUES[colorType] = defaultDefaultHues;
  4172    if (!DARK_DEFAULT_HUES[colorType]) DARK_DEFAULT_HUES[colorType] = defaultDefaultHues;
  4173  });
  4174  
  4175  var VALID_HUE_VALUES = [
  4176    '50', '100', '200', '300', '400', '500', '600',
  4177    '700', '800', '900', 'A100', 'A200', 'A400', 'A700'
  4178  ];
  4179  
  4180  // Whether or not themes are to be generated on-demand (vs. eagerly).
  4181  var generateOnDemand = false;
  4182  
  4183  // Nonce to be added as an attribute to the generated themes style tags.
  4184  var nonce = null;
  4185  
  4186  function ThemingProvider($mdColorPalette) {
  4187    PALETTES = { };
  4188    THEMES = { };
  4189  
  4190    var themingProvider;
  4191    var defaultTheme = 'default';
  4192    var alwaysWatchTheme = false;
  4193  
  4194    // Load JS Defined Palettes
  4195    angular.extend(PALETTES, $mdColorPalette);
  4196  
  4197    // Default theme defined in core.js
  4198  
  4199    ThemingService.$inject = ["$rootScope", "$log"];
  4200    return themingProvider = {
  4201      definePalette: definePalette,
  4202      extendPalette: extendPalette,
  4203      theme: registerTheme,
  4204  
  4205      setNonce: function(nonceValue) {
  4206        nonce = nonceValue;
  4207      },
  4208      setDefaultTheme: function(theme) {
  4209        defaultTheme = theme;
  4210      },
  4211      alwaysWatchTheme: function(alwaysWatch) {
  4212        alwaysWatchTheme = alwaysWatch;
  4213      },
  4214      generateThemesOnDemand: function(onDemand) {
  4215        generateOnDemand = onDemand;
  4216      },
  4217      $get: ThemingService,
  4218      _LIGHT_DEFAULT_HUES: LIGHT_DEFAULT_HUES,
  4219      _DARK_DEFAULT_HUES: DARK_DEFAULT_HUES,
  4220      _PALETTES: PALETTES,
  4221      _THEMES: THEMES,
  4222      _parseRules: parseRules,
  4223      _rgba: rgba
  4224    };
  4225  
  4226    // Example: $mdThemingProvider.definePalette('neonRed', { 50: '#f5fafa', ... });
  4227    function definePalette(name, map) {
  4228      map = map || {};
  4229      PALETTES[name] = checkPaletteValid(name, map);
  4230      return themingProvider;
  4231    }
  4232  
  4233    // Returns an new object which is a copy of a given palette `name` with variables from
  4234    // `map` overwritten
  4235    // Example: var neonRedMap = $mdThemingProvider.extendPalette('red', { 50: '#f5fafafa' });
  4236    function extendPalette(name, map) {
  4237      return checkPaletteValid(name,  angular.extend({}, PALETTES[name] || {}, map) );
  4238    }
  4239  
  4240    // Make sure that palette has all required hues
  4241    function checkPaletteValid(name, map) {
  4242      var missingColors = VALID_HUE_VALUES.filter(function(field) {
  4243        return !map[field];
  4244      });
  4245      if (missingColors.length) {
  4246        throw new Error("Missing colors %1 in palette %2!"
  4247                        .replace('%1', missingColors.join(', '))
  4248                        .replace('%2', name));
  4249      }
  4250  
  4251      return map;
  4252    }
  4253  
  4254    // Register a theme (which is a collection of color palettes to use with various states
  4255    // ie. warn, accent, primary )
  4256    // Optionally inherit from an existing theme
  4257    // $mdThemingProvider.theme('custom-theme').primaryPalette('red');
  4258    function registerTheme(name, inheritFrom) {
  4259      if (THEMES[name]) return THEMES[name];
  4260  
  4261      inheritFrom = inheritFrom || 'default';
  4262  
  4263      var parentTheme = typeof inheritFrom === 'string' ? THEMES[inheritFrom] : inheritFrom;
  4264      var theme = new Theme(name);
  4265  
  4266      if (parentTheme) {
  4267        angular.forEach(parentTheme.colors, function(color, colorType) {
  4268          theme.colors[colorType] = {
  4269            name: color.name,
  4270            // Make sure a COPY of the hues is given to the child color,
  4271            // not the same reference.
  4272            hues: angular.extend({}, color.hues)
  4273          };
  4274        });
  4275      }
  4276      THEMES[name] = theme;
  4277  
  4278      return theme;
  4279    }
  4280  
  4281    function Theme(name) {
  4282      var self = this;
  4283      self.name = name;
  4284      self.colors = {};
  4285  
  4286      self.dark = setDark;
  4287      setDark(false);
  4288  
  4289      function setDark(isDark) {
  4290        isDark = arguments.length === 0 ? true : !!isDark;
  4291  
  4292        // If no change, abort
  4293        if (isDark === self.isDark) return;
  4294  
  4295        self.isDark = isDark;
  4296  
  4297        self.foregroundPalette = self.isDark ? LIGHT_FOREGROUND : DARK_FOREGROUND;
  4298        self.foregroundShadow = self.isDark ? DARK_SHADOW : LIGHT_SHADOW;
  4299  
  4300        // Light and dark themes have different default hues.
  4301        // Go through each existing color type for this theme, and for every
  4302        // hue value that is still the default hue value from the previous light/dark setting,
  4303        // set it to the default hue value from the new light/dark setting.
  4304        var newDefaultHues = self.isDark ? DARK_DEFAULT_HUES : LIGHT_DEFAULT_HUES;
  4305        var oldDefaultHues = self.isDark ? LIGHT_DEFAULT_HUES : DARK_DEFAULT_HUES;
  4306        angular.forEach(newDefaultHues, function(newDefaults, colorType) {
  4307          var color = self.colors[colorType];
  4308          var oldDefaults = oldDefaultHues[colorType];
  4309          if (color) {
  4310            for (var hueName in color.hues) {
  4311              if (color.hues[hueName] === oldDefaults[hueName]) {
  4312                color.hues[hueName] = newDefaults[hueName];
  4313              }
  4314            }
  4315          }
  4316        });
  4317  
  4318        return self;
  4319      }
  4320  
  4321      THEME_COLOR_TYPES.forEach(function(colorType) {
  4322        var defaultHues = (self.isDark ? DARK_DEFAULT_HUES : LIGHT_DEFAULT_HUES)[colorType];
  4323        self[colorType + 'Palette'] = function setPaletteType(paletteName, hues) {
  4324          var color = self.colors[colorType] = {
  4325            name: paletteName,
  4326            hues: angular.extend({}, defaultHues, hues)
  4327          };
  4328  
  4329          Object.keys(color.hues).forEach(function(name) {
  4330            if (!defaultHues[name]) {
  4331              throw new Error("Invalid hue name '%1' in theme %2's %3 color %4. Available hue names: %4"
  4332                .replace('%1', name)
  4333                .replace('%2', self.name)
  4334                .replace('%3', paletteName)
  4335                .replace('%4', Object.keys(defaultHues).join(', '))
  4336              );
  4337            }
  4338          });
  4339          Object.keys(color.hues).map(function(key) {
  4340            return color.hues[key];
  4341          }).forEach(function(hueValue) {
  4342            if (VALID_HUE_VALUES.indexOf(hueValue) == -1) {
  4343              throw new Error("Invalid hue value '%1' in theme %2's %3 color %4. Available hue values: %5"
  4344                .replace('%1', hueValue)
  4345                .replace('%2', self.name)
  4346                .replace('%3', colorType)
  4347                .replace('%4', paletteName)
  4348                .replace('%5', VALID_HUE_VALUES.join(', '))
  4349              );
  4350            }
  4351          });
  4352          return self;
  4353        };
  4354  
  4355        self[colorType + 'Color'] = function() {
  4356          var args = Array.prototype.slice.call(arguments);
  4357          console.warn('$mdThemingProviderTheme.' + colorType + 'Color() has been deprecated. ' +
  4358                       'Use $mdThemingProviderTheme.' + colorType + 'Palette() instead.');
  4359          return self[colorType + 'Palette'].apply(self, args);
  4360        };
  4361      });
  4362    }
  4363  
  4364    /**
  4365     * @ngdoc service
  4366     * @name $mdTheming
  4367     *
  4368     * @description
  4369     *
  4370     * Service that makes an element apply theming related classes to itself.
  4371     *
  4372     * ```js
  4373     * app.directive('myFancyDirective', function($mdTheming) {
  4374     *   return {
  4375     *     restrict: 'e',
  4376     *     link: function(scope, el, attrs) {
  4377     *       $mdTheming(el);
  4378     *     }
  4379     *   };
  4380     * });
  4381     * ```
  4382     * @param {el=} element to apply theming to
  4383     */
  4384    /* @ngInject */
  4385    function ThemingService($rootScope, $log) {
  4386          // Allow us to be invoked via a linking function signature.
  4387      var applyTheme = function (scope, el) {
  4388            if (el === undefined) { el = scope; scope = undefined; }
  4389            if (scope === undefined) { scope = $rootScope; }
  4390            applyTheme.inherit(el, el);
  4391          };
  4392  
  4393      applyTheme.THEMES = angular.extend({}, THEMES);
  4394      applyTheme.inherit = inheritTheme;
  4395      applyTheme.registered = registered;
  4396      applyTheme.defaultTheme = function() { return defaultTheme; };
  4397      applyTheme.generateTheme = function(name) { generateTheme(name, nonce); };
  4398  
  4399      return applyTheme;
  4400  
  4401      /**
  4402       * Determine is specified theme name is a valid, registered theme
  4403       */
  4404      function registered(themeName) {
  4405        if (themeName === undefined || themeName === '') return true;
  4406        return applyTheme.THEMES[themeName] !== undefined;
  4407      }
  4408  
  4409      /**
  4410       * Get theme name for the element, then update with Theme CSS class
  4411       */
  4412      function inheritTheme (el, parent) {
  4413        var ctrl = parent.controller('mdTheme');
  4414        var attrThemeValue = el.attr('md-theme-watch');
  4415        var watchTheme = (alwaysWatchTheme || angular.isDefined(attrThemeValue)) && attrThemeValue != 'false';
  4416  
  4417        updateThemeClass(lookupThemeName());
  4418  
  4419        el.on('$destroy', watchTheme ? $rootScope.$watch(lookupThemeName, updateThemeClass) : angular.noop );
  4420  
  4421        /**
  4422         * Find the theme name from the parent controller or element data
  4423         */
  4424        function lookupThemeName() {
  4425          // As a few components (dialog) add their controllers later, we should also watch for a controller init.
  4426          ctrl = parent.controller('mdTheme') || el.data('$mdThemeController');
  4427          return ctrl && ctrl.$mdTheme || (defaultTheme == 'default' ? '' : defaultTheme);
  4428        }
  4429  
  4430        /**
  4431         * Remove old theme class and apply a new one
  4432         * NOTE: if not a valid theme name, then the current name is not changed
  4433         */
  4434        function updateThemeClass(theme) {
  4435          if (!theme) return;
  4436          if (!registered(theme)) {
  4437            $log.warn('Attempted to use unregistered theme \'' + theme + '\'. ' +
  4438                      'Register it with $mdThemingProvider.theme().');
  4439          }
  4440  
  4441          var oldTheme = el.data('$mdThemeName');
  4442          if (oldTheme) el.removeClass('md-' + oldTheme +'-theme');
  4443          el.addClass('md-' + theme + '-theme');
  4444          el.data('$mdThemeName', theme);
  4445          if (ctrl) {
  4446            el.data('$mdThemeController', ctrl);
  4447          }
  4448        }
  4449      }
  4450  
  4451    }
  4452  }
  4453  ThemingProvider.$inject = ["$mdColorPalette"];
  4454  
  4455  function ThemingDirective($mdTheming, $interpolate, $log) {
  4456    return {
  4457      priority: 100,
  4458      link: {
  4459        pre: function(scope, el, attrs) {
  4460          var ctrl = {
  4461            $setTheme: function(theme) {
  4462              if (!$mdTheming.registered(theme)) {
  4463                $log.warn('attempted to use unregistered theme \'' + theme + '\'');
  4464              }
  4465              ctrl.$mdTheme = theme;
  4466            }
  4467          };
  4468          el.data('$mdThemeController', ctrl);
  4469          ctrl.$setTheme($interpolate(attrs.mdTheme)(scope));
  4470          attrs.$observe('mdTheme', ctrl.$setTheme);
  4471        }
  4472      }
  4473    };
  4474  }
  4475  ThemingDirective.$inject = ["$mdTheming", "$interpolate", "$log"];
  4476  
  4477  function ThemableDirective($mdTheming) {
  4478    return $mdTheming;
  4479  }
  4480  ThemableDirective.$inject = ["$mdTheming"];
  4481  
  4482  function parseRules(theme, colorType, rules) {
  4483    checkValidPalette(theme, colorType);
  4484  
  4485    rules = rules.replace(/THEME_NAME/g, theme.name);
  4486    var generatedRules = [];
  4487    var color = theme.colors[colorType];
  4488  
  4489    var themeNameRegex = new RegExp('.md-' + theme.name + '-theme', 'g');
  4490    // Matches '{{ primary-color }}', etc
  4491    var hueRegex = new RegExp('(\'|")?{{\\s*(' + colorType + ')-(color|contrast)-?(\\d\\.?\\d*)?\\s*}}(\"|\')?','g');
  4492    var simpleVariableRegex = /'?"?\{\{\s*([a-zA-Z]+)-(A?\d+|hue\-[0-3]|shadow)-?(\d\.?\d*)?(contrast)?\s*\}\}'?"?/g;
  4493    var palette = PALETTES[color.name];
  4494  
  4495    // find and replace simple variables where we use a specific hue, not an entire palette
  4496    // eg. "{{primary-100}}"
  4497    //\(' + THEME_COLOR_TYPES.join('\|') + '\)'
  4498    rules = rules.replace(simpleVariableRegex, function(match, colorType, hue, opacity, contrast) {
  4499      if (colorType === 'foreground') {
  4500        if (hue == 'shadow') {
  4501          return theme.foregroundShadow;
  4502        } else {
  4503          return theme.foregroundPalette[hue] || theme.foregroundPalette['1'];
  4504        }
  4505      }
  4506      if (hue.indexOf('hue') === 0) {
  4507        hue = theme.colors[colorType].hues[hue];
  4508      }
  4509      return rgba( (PALETTES[ theme.colors[colorType].name ][hue] || '')[contrast ? 'contrast' : 'value'], opacity );
  4510    });
  4511  
  4512    // For each type, generate rules for each hue (ie. default, md-hue-1, md-hue-2, md-hue-3)
  4513    angular.forEach(color.hues, function(hueValue, hueName) {
  4514      var newRule = rules
  4515        .replace(hueRegex, function(match, _, colorType, hueType, opacity) {
  4516          return rgba(palette[hueValue][hueType === 'color' ? 'value' : 'contrast'], opacity);
  4517        });
  4518      if (hueName !== 'default') {
  4519        newRule = newRule.replace(themeNameRegex, '.md-' + theme.name + '-theme.md-' + hueName);
  4520      }
  4521  
  4522      // Don't apply a selector rule to the default theme, making it easier to override
  4523      // styles of the base-component
  4524      if (theme.name == 'default') {
  4525        var themeRuleRegex = /((?:(?:(?: |>|\.|\w|-|:|\(|\)|\[|\]|"|'|=)+) )?)((?:(?:\w|\.|-)+)?)\.md-default-theme((?: |>|\.|\w|-|:|\(|\)|\[|\]|"|'|=)*)/g;
  4526        newRule = newRule.replace(themeRuleRegex, function(match, prefix, target, suffix) {
  4527          return match + ', ' + prefix + target + suffix;
  4528        });
  4529      }
  4530      generatedRules.push(newRule);
  4531    });
  4532  
  4533    return generatedRules;
  4534  }
  4535  
  4536  var rulesByType = {};
  4537  
  4538  // Generate our themes at run time given the state of THEMES and PALETTES
  4539  function generateAllThemes($injector) {
  4540    var head = document.head;
  4541    var firstChild = head ? head.firstElementChild : null;
  4542    var themeCss = $injector.has('$MD_THEME_CSS') ? $injector.get('$MD_THEME_CSS') : '';
  4543  
  4544    if ( !firstChild ) return;
  4545    if (themeCss.length === 0) return; // no rules, so no point in running this expensive task
  4546  
  4547    // Expose contrast colors for palettes to ensure that text is always readable
  4548    angular.forEach(PALETTES, sanitizePalette);
  4549  
  4550    // MD_THEME_CSS is a string generated by the build process that includes all the themable
  4551    // components as templates
  4552  
  4553    // Break the CSS into individual rules
  4554    var rules = themeCss
  4555                    .split(/\}(?!(\}|'|"|;))/)
  4556                    .filter(function(rule) { return rule && rule.length; })
  4557                    .map(function(rule) { return rule.trim() + '}'; });
  4558  
  4559  
  4560    var ruleMatchRegex = new RegExp('md-(' + THEME_COLOR_TYPES.join('|') + ')', 'g');
  4561  
  4562    THEME_COLOR_TYPES.forEach(function(type) {
  4563      rulesByType[type] = '';
  4564    });
  4565  
  4566  
  4567    // Sort the rules based on type, allowing us to do color substitution on a per-type basis
  4568    rules.forEach(function(rule) {
  4569      var match = rule.match(ruleMatchRegex);
  4570      // First: test that if the rule has '.md-accent', it goes into the accent set of rules
  4571      for (var i = 0, type; type = THEME_COLOR_TYPES[i]; i++) {
  4572        if (rule.indexOf('.md-' + type) > -1) {
  4573          return rulesByType[type] += rule;
  4574        }
  4575      }
  4576  
  4577      // If no eg 'md-accent' class is found, try to just find 'accent' in the rule and guess from
  4578      // there
  4579      for (i = 0; type = THEME_COLOR_TYPES[i]; i++) {
  4580        if (rule.indexOf(type) > -1) {
  4581          return rulesByType[type] += rule;
  4582        }
  4583      }
  4584  
  4585      // Default to the primary array
  4586      return rulesByType[DEFAULT_COLOR_TYPE] += rule;
  4587    });
  4588  
  4589    // If themes are being generated on-demand, quit here. The user will later manually
  4590    // call generateTheme to do this on a theme-by-theme basis.
  4591    if (generateOnDemand) return;
  4592  
  4593    angular.forEach(THEMES, function(theme) {
  4594      if (!GENERATED[theme.name]) {
  4595        generateTheme(theme.name, nonce);
  4596      }
  4597    });
  4598  
  4599  
  4600    // *************************
  4601    // Internal functions
  4602    // *************************
  4603  
  4604    // The user specifies a 'default' contrast color as either light or dark,
  4605    // then explicitly lists which hues are the opposite contrast (eg. A100 has dark, A200 has light)
  4606    function sanitizePalette(palette) {
  4607      var defaultContrast = palette.contrastDefaultColor;
  4608      var lightColors = palette.contrastLightColors || [];
  4609      var strongLightColors = palette.contrastStrongLightColors || [];
  4610      var darkColors = palette.contrastDarkColors || [];
  4611  
  4612      // These colors are provided as space-separated lists
  4613      if (typeof lightColors === 'string') lightColors = lightColors.split(' ');
  4614      if (typeof strongLightColors === 'string') strongLightColors = strongLightColors.split(' ');
  4615      if (typeof darkColors === 'string') darkColors = darkColors.split(' ');
  4616  
  4617      // Cleanup after ourselves
  4618      delete palette.contrastDefaultColor;
  4619      delete palette.contrastLightColors;
  4620      delete palette.contrastStrongLightColors;
  4621      delete palette.contrastDarkColors;
  4622  
  4623      // Change { 'A100': '#fffeee' } to { 'A100': { value: '#fffeee', contrast:DARK_CONTRAST_COLOR }
  4624      angular.forEach(palette, function(hueValue, hueName) {
  4625        if (angular.isObject(hueValue)) return; // Already converted
  4626        // Map everything to rgb colors
  4627        var rgbValue = colorToRgbaArray(hueValue);
  4628        if (!rgbValue) {
  4629          throw new Error("Color %1, in palette %2's hue %3, is invalid. Hex or rgb(a) color expected."
  4630                          .replace('%1', hueValue)
  4631                          .replace('%2', palette.name)
  4632                          .replace('%3', hueName));
  4633        }
  4634  
  4635        palette[hueName] = {
  4636          value: rgbValue,
  4637          contrast: getContrastColor()
  4638        };
  4639        function getContrastColor() {
  4640          if (defaultContrast === 'light') {
  4641            if (darkColors.indexOf(hueName) > -1) {
  4642              return DARK_CONTRAST_COLOR;
  4643            } else {
  4644              return strongLightColors.indexOf(hueName) > -1 ? STRONG_LIGHT_CONTRAST_COLOR
  4645                : LIGHT_CONTRAST_COLOR;
  4646            }
  4647          } else {
  4648            if (lightColors.indexOf(hueName) > -1) {
  4649              return strongLightColors.indexOf(hueName) > -1 ? STRONG_LIGHT_CONTRAST_COLOR
  4650                : LIGHT_CONTRAST_COLOR;
  4651            } else {
  4652              return DARK_CONTRAST_COLOR;
  4653            }
  4654          }
  4655        }
  4656      });
  4657    }
  4658  }
  4659  generateAllThemes.$inject = ["$injector"];
  4660  
  4661  function generateTheme(name, nonce) {
  4662    var theme = THEMES[name];
  4663    var head = document.head;
  4664    var firstChild = head ? head.firstElementChild : null;
  4665  
  4666    if (!GENERATED[name]) {
  4667      // For each theme, use the color palettes specified for
  4668      // `primary`, `warn` and `accent` to generate CSS rules.
  4669      THEME_COLOR_TYPES.forEach(function(colorType) {
  4670        var styleStrings = parseRules(theme, colorType, rulesByType[colorType]);
  4671        while (styleStrings.length) {
  4672          var styleContent = styleStrings.shift();
  4673          if (styleContent) {
  4674            var style = document.createElement('style');
  4675            style.setAttribute('md-theme-style', '');
  4676            if (nonce) {
  4677              style.setAttribute('nonce', nonce);
  4678            }
  4679            style.appendChild(document.createTextNode(styleContent));
  4680            head.insertBefore(style, firstChild);
  4681          }
  4682        }
  4683      });
  4684  
  4685  
  4686      if (theme.colors.primary.name == theme.colors.accent.name) {
  4687        console.warn('$mdThemingProvider: Using the same palette for primary and' +
  4688                     ' accent. This violates the material design spec.');
  4689      }
  4690  
  4691      GENERATED[theme.name] = true;
  4692    }
  4693  
  4694  }
  4695  
  4696  
  4697  function checkValidPalette(theme, colorType) {
  4698    // If theme attempts to use a palette that doesnt exist, throw error
  4699    if (!PALETTES[ (theme.colors[colorType] || {}).name ]) {
  4700      throw new Error(
  4701        "You supplied an invalid color palette for theme %1's %2 palette. Available palettes: %3"
  4702                      .replace('%1', theme.name)
  4703                      .replace('%2', colorType)
  4704                      .replace('%3', Object.keys(PALETTES).join(', '))
  4705      );
  4706    }
  4707  }
  4708  
  4709  function colorToRgbaArray(clr) {
  4710    if (angular.isArray(clr) && clr.length == 3) return clr;
  4711    if (/^rgb/.test(clr)) {
  4712      return clr.replace(/(^\s*rgba?\(|\)\s*$)/g, '').split(',').map(function(value, i) {
  4713        return i == 3 ? parseFloat(value, 10) : parseInt(value, 10);
  4714      });
  4715    }
  4716    if (clr.charAt(0) == '#') clr = clr.substring(1);
  4717    if (!/^([a-fA-F0-9]{3}){1,2}$/g.test(clr)) return;
  4718  
  4719    var dig = clr.length / 3;
  4720    var red = clr.substr(0, dig);
  4721    var grn = clr.substr(dig, dig);
  4722    var blu = clr.substr(dig * 2);
  4723    if (dig === 1) {
  4724      red += red;
  4725      grn += grn;
  4726      blu += blu;
  4727    }
  4728    return [parseInt(red, 16), parseInt(grn, 16), parseInt(blu, 16)];
  4729  }
  4730  
  4731  function rgba(rgbArray, opacity) {
  4732    if ( !rgbArray ) return "rgb('0,0,0')";
  4733  
  4734    if (rgbArray.length == 4) {
  4735      rgbArray = angular.copy(rgbArray);
  4736      opacity ? rgbArray.pop() : opacity = rgbArray.pop();
  4737    }
  4738    return opacity && (typeof opacity == 'number' || (typeof opacity == 'string' && opacity.length)) ?
  4739      'rgba(' + rgbArray.join(',') + ',' + opacity + ')' :
  4740      'rgb(' + rgbArray.join(',') + ')';
  4741  }
  4742  
  4743  
  4744  })();
  4745  (function(){
  4746  "use strict";
  4747  
  4748  (function() {
  4749    'use strict';
  4750  
  4751    /**
  4752     * @ngdoc service
  4753     * @name $mdButtonInkRipple
  4754     * @module material.core
  4755     *
  4756     * @description
  4757     * Provides ripple effects for md-button.  See $mdInkRipple service for all possible configuration options.
  4758     *
  4759     * @param {object=} scope Scope within the current context
  4760     * @param {object=} element The element the ripple effect should be applied to
  4761     * @param {object=} options (Optional) Configuration options to override the default ripple configuration
  4762     */
  4763  
  4764    angular.module('material.core')
  4765      .factory('$mdButtonInkRipple', MdButtonInkRipple);
  4766  
  4767    function MdButtonInkRipple($mdInkRipple) {
  4768      return {
  4769        attach: function attachRipple(scope, element, options) {
  4770          options = angular.extend(optionsForElement(element), options);
  4771  
  4772          return $mdInkRipple.attach(scope, element, options);
  4773        }
  4774      };
  4775  
  4776      function optionsForElement(element) {
  4777        if (element.hasClass('md-icon-button')) {
  4778          return {
  4779            isMenuItem: element.hasClass('md-menu-item'),
  4780            fitRipple: true,
  4781            center: true
  4782          };
  4783        } else {
  4784          return {
  4785            isMenuItem: element.hasClass('md-menu-item'),
  4786            dimBackground: true
  4787          }
  4788        }
  4789      };
  4790    }
  4791    MdButtonInkRipple.$inject = ["$mdInkRipple"];;
  4792  })();
  4793  
  4794  })();
  4795  (function(){
  4796  "use strict";
  4797  
  4798  (function() {
  4799    'use strict';
  4800  
  4801      /**
  4802     * @ngdoc service
  4803     * @name $mdCheckboxInkRipple
  4804     * @module material.core
  4805     *
  4806     * @description
  4807     * Provides ripple effects for md-checkbox.  See $mdInkRipple service for all possible configuration options.
  4808     *
  4809     * @param {object=} scope Scope within the current context
  4810     * @param {object=} element The element the ripple effect should be applied to
  4811     * @param {object=} options (Optional) Configuration options to override the defaultripple configuration
  4812     */
  4813  
  4814    angular.module('material.core')
  4815      .factory('$mdCheckboxInkRipple', MdCheckboxInkRipple);
  4816  
  4817    function MdCheckboxInkRipple($mdInkRipple) {
  4818      return {
  4819        attach: attach
  4820      };
  4821  
  4822      function attach(scope, element, options) {
  4823        return $mdInkRipple.attach(scope, element, angular.extend({
  4824          center: true,
  4825          dimBackground: false,
  4826          fitRipple: true
  4827        }, options));
  4828      };
  4829    }
  4830    MdCheckboxInkRipple.$inject = ["$mdInkRipple"];;
  4831  })();
  4832  
  4833  })();
  4834  (function(){
  4835  "use strict";
  4836  
  4837  (function() {
  4838    'use strict';
  4839  
  4840    /**
  4841     * @ngdoc service
  4842     * @name $mdListInkRipple
  4843     * @module material.core
  4844     *
  4845     * @description
  4846     * Provides ripple effects for md-list.  See $mdInkRipple service for all possible configuration options.
  4847     *
  4848     * @param {object=} scope Scope within the current context
  4849     * @param {object=} element The element the ripple effect should be applied to
  4850     * @param {object=} options (Optional) Configuration options to override the defaultripple configuration
  4851     */
  4852  
  4853    angular.module('material.core')
  4854      .factory('$mdListInkRipple', MdListInkRipple);
  4855  
  4856    function MdListInkRipple($mdInkRipple) {
  4857      return {
  4858        attach: attach
  4859      };
  4860  
  4861      function attach(scope, element, options) {
  4862        return $mdInkRipple.attach(scope, element, angular.extend({
  4863          center: false,
  4864          dimBackground: true,
  4865          outline: false,
  4866          rippleSize: 'full'
  4867        }, options));
  4868      };
  4869    }
  4870    MdListInkRipple.$inject = ["$mdInkRipple"];;
  4871  })();
  4872  
  4873  })();
  4874  (function(){
  4875  "use strict";
  4876  
  4877  /**
  4878   * @ngdoc module
  4879   * @name material.core.ripple
  4880   * @description
  4881   * Ripple
  4882   */
  4883  angular.module('material.core')
  4884      .factory('$mdInkRipple', InkRippleService)
  4885      .directive('mdInkRipple', InkRippleDirective)
  4886      .directive('mdNoInk', attrNoDirective)
  4887      .directive('mdNoBar', attrNoDirective)
  4888      .directive('mdNoStretch', attrNoDirective);
  4889  
  4890  var DURATION = 450;
  4891  
  4892  /**
  4893   * @ngdoc directive
  4894   * @name mdInkRipple
  4895   * @module material.core.ripple
  4896   *
  4897   * @description
  4898   * The `md-ink-ripple` directive allows you to specify the ripple color or id a ripple is allowed.
  4899   *
  4900   * @param {string|boolean} md-ink-ripple A color string `#FF0000` or boolean (`false` or `0`) for preventing ripple
  4901   *
  4902   * @usage
  4903   * ### String values
  4904   * <hljs lang="html">
  4905   *   <ANY md-ink-ripple="#FF0000">
  4906   *     Ripples in red
  4907   *   </ANY>
  4908   *
  4909   *   <ANY md-ink-ripple="false">
  4910   *     Not rippling
  4911   *   </ANY>
  4912   * </hljs>
  4913   *
  4914   * ### Interpolated values
  4915   * <hljs lang="html">
  4916   *   <ANY md-ink-ripple="{{ randomColor() }}">
  4917   *     Ripples with the return value of 'randomColor' function
  4918   *   </ANY>
  4919   *
  4920   *   <ANY md-ink-ripple="{{ canRipple() }}">
  4921   *     Ripples if 'canRipple' function return value is not 'false' or '0'
  4922   *   </ANY>
  4923   * </hljs>
  4924   */
  4925  function InkRippleDirective ($mdButtonInkRipple, $mdCheckboxInkRipple) {
  4926    return {
  4927      controller: angular.noop,
  4928      link:       function (scope, element, attr) {
  4929        attr.hasOwnProperty('mdInkRippleCheckbox')
  4930            ? $mdCheckboxInkRipple.attach(scope, element)
  4931            : $mdButtonInkRipple.attach(scope, element);
  4932      }
  4933    };
  4934  }
  4935  InkRippleDirective.$inject = ["$mdButtonInkRipple", "$mdCheckboxInkRipple"];
  4936  
  4937  /**
  4938   * @ngdoc service
  4939   * @name $mdInkRipple
  4940   * @module material.core.ripple
  4941   *
  4942   * @description
  4943   * `$mdInkRipple` is a service for adding ripples to any element
  4944   *
  4945   * @usage
  4946   * <hljs lang="js">
  4947   * app.factory('$myElementInkRipple', function($mdInkRipple) {
  4948   *   return {
  4949   *     attach: function (scope, element, options) {
  4950   *       return $mdInkRipple.attach(scope, element, angular.extend({
  4951   *         center: false,
  4952   *         dimBackground: true
  4953   *       }, options));
  4954   *     }
  4955   *   };
  4956   * });
  4957   *
  4958   * app.controller('myController', function ($scope, $element, $myElementInkRipple) {
  4959   *   $scope.onClick = function (ev) {
  4960   *     $myElementInkRipple.attach($scope, angular.element(ev.target), { center: true });
  4961   *   }
  4962   * });
  4963   * </hljs>
  4964   */
  4965  
  4966  /**
  4967   * @ngdoc method
  4968   * @name $mdInkRipple#attach
  4969   *
  4970   * @description
  4971   * Attaching given scope, element and options to inkRipple controller
  4972   *
  4973   * @param {object=} scope Scope within the current context
  4974   * @param {object=} element The element the ripple effect should be applied to
  4975   * @param {object=} options (Optional) Configuration options to override the defaultRipple configuration
  4976   * * `center` -  Whether the ripple should start from the center of the container element
  4977   * * `dimBackground` - Whether the background should be dimmed with the ripple color
  4978   * * `colorElement` - The element the ripple should take its color from, defined by css property `color`
  4979   * * `fitRipple` - Whether the ripple should fill the element
  4980   */
  4981  function InkRippleService ($injector) {
  4982    return { attach: attach };
  4983    function attach (scope, element, options) {
  4984      if (element.controller('mdNoInk')) return angular.noop;
  4985      return $injector.instantiate(InkRippleCtrl, {
  4986        $scope:        scope,
  4987        $element:      element,
  4988        rippleOptions: options
  4989      });
  4990    }
  4991  }
  4992  InkRippleService.$inject = ["$injector"];
  4993  
  4994  /**
  4995   * Controller used by the ripple service in order to apply ripples
  4996   * @ngInject
  4997   */
  4998  function InkRippleCtrl ($scope, $element, rippleOptions, $window, $timeout, $mdUtil) {
  4999    this.$window    = $window;
  5000    this.$timeout   = $timeout;
  5001    this.$mdUtil    = $mdUtil;
  5002    this.$scope     = $scope;
  5003    this.$element   = $element;
  5004    this.options    = rippleOptions;
  5005    this.mousedown  = false;
  5006    this.ripples    = [];
  5007    this.timeout    = null; // Stores a reference to the most-recent ripple timeout
  5008    this.lastRipple = null;
  5009  
  5010    $mdUtil.valueOnUse(this, 'container', this.createContainer);
  5011  
  5012    this.$element.addClass('md-ink-ripple');
  5013  
  5014    // attach method for unit tests
  5015    ($element.controller('mdInkRipple') || {}).createRipple = angular.bind(this, this.createRipple);
  5016    ($element.controller('mdInkRipple') || {}).setColor = angular.bind(this, this.color);
  5017  
  5018    this.bindEvents();
  5019  }
  5020  InkRippleCtrl.$inject = ["$scope", "$element", "rippleOptions", "$window", "$timeout", "$mdUtil"];
  5021  
  5022  
  5023  /**
  5024   * Either remove or unlock any remaining ripples when the user mouses off of the element (either by
  5025   * mouseup or mouseleave event)
  5026   */
  5027  function autoCleanup (self, cleanupFn) {
  5028  
  5029    if ( self.mousedown || self.lastRipple ) {
  5030      self.mousedown = false;
  5031      self.$mdUtil.nextTick( angular.bind(self, cleanupFn), false);
  5032    }
  5033  
  5034  }
  5035  
  5036  
  5037  /**
  5038   * Returns the color that the ripple should be (either based on CSS or hard-coded)
  5039   * @returns {string}
  5040   */
  5041  InkRippleCtrl.prototype.color = function (value) {
  5042    var self = this;
  5043  
  5044    // If assigning a color value, apply it to background and the ripple color
  5045    if (angular.isDefined(value)) {
  5046      self._color = self._parseColor(value);
  5047    }
  5048  
  5049    // If color lookup, use assigned, defined, or inherited
  5050    return self._color || self._parseColor( self.inkRipple() ) || self._parseColor( getElementColor() );
  5051  
  5052    /**
  5053     * Finds the color element and returns its text color for use as default ripple color
  5054     * @returns {string}
  5055     */
  5056    function getElementColor () {
  5057      var items = self.options && self.options.colorElement ? self.options.colorElement : [];
  5058      var elem =  items.length ? items[ 0 ] : self.$element[ 0 ];
  5059  
  5060      return elem ? self.$window.getComputedStyle(elem).color : 'rgb(0,0,0)';
  5061    }
  5062  };
  5063  
  5064  /**
  5065   * Updating the ripple colors based on the current inkRipple value
  5066   * or the element's computed style color
  5067   */
  5068  InkRippleCtrl.prototype.calculateColor = function () {
  5069    return this.color();
  5070  };
  5071  
  5072  
  5073  /**
  5074   * Takes a string color and converts it to RGBA format
  5075   * @param color {string}
  5076   * @param [multiplier] {int}
  5077   * @returns {string}
  5078   */
  5079  
  5080  InkRippleCtrl.prototype._parseColor = function parseColor (color, multiplier) {
  5081    multiplier = multiplier || 1;
  5082  
  5083    if (!color) return;
  5084    if (color.indexOf('rgba') === 0) return color.replace(/\d?\.?\d*\s*\)\s*$/, (0.1 * multiplier).toString() + ')');
  5085    if (color.indexOf('rgb') === 0) return rgbToRGBA(color);
  5086    if (color.indexOf('#') === 0) return hexToRGBA(color);
  5087  
  5088    /**
  5089     * Converts hex value to RGBA string
  5090     * @param color {string}
  5091     * @returns {string}
  5092     */
  5093    function hexToRGBA (color) {
  5094      var hex   = color[ 0 ] === '#' ? color.substr(1) : color,
  5095        dig   = hex.length / 3,
  5096        red   = hex.substr(0, dig),
  5097        green = hex.substr(dig, dig),
  5098        blue  = hex.substr(dig * 2);
  5099      if (dig === 1) {
  5100        red += red;
  5101        green += green;
  5102        blue += blue;
  5103      }
  5104      return 'rgba(' + parseInt(red, 16) + ',' + parseInt(green, 16) + ',' + parseInt(blue, 16) + ',0.1)';
  5105    }
  5106  
  5107    /**
  5108     * Converts an RGB color to RGBA
  5109     * @param color {string}
  5110     * @returns {string}
  5111     */
  5112    function rgbToRGBA (color) {
  5113      return color.replace(')', ', 0.1)').replace('(', 'a(');
  5114    }
  5115  
  5116  };
  5117  
  5118  /**
  5119   * Binds events to the root element for
  5120   */
  5121  InkRippleCtrl.prototype.bindEvents = function () {
  5122    this.$element.on('mousedown', angular.bind(this, this.handleMousedown));
  5123    this.$element.on('mouseup touchend', angular.bind(this, this.handleMouseup));
  5124    this.$element.on('mouseleave', angular.bind(this, this.handleMouseup));
  5125    this.$element.on('touchmove', angular.bind(this, this.handleTouchmove));
  5126  };
  5127  
  5128  /**
  5129   * Create a new ripple on every mousedown event from the root element
  5130   * @param event {MouseEvent}
  5131   */
  5132  InkRippleCtrl.prototype.handleMousedown = function (event) {
  5133    if ( this.mousedown ) return;
  5134  
  5135    // When jQuery is loaded, we have to get the original event
  5136    if (event.hasOwnProperty('originalEvent')) event = event.originalEvent;
  5137    this.mousedown = true;
  5138    if (this.options.center) {
  5139      this.createRipple(this.container.prop('clientWidth') / 2, this.container.prop('clientWidth') / 2);
  5140    } else {
  5141  
  5142      // We need to calculate the relative coordinates if the target is a sublayer of the ripple element
  5143      if (event.srcElement !== this.$element[0]) {
  5144        var layerRect = this.$element[0].getBoundingClientRect();
  5145        var layerX = event.clientX - layerRect.left;
  5146        var layerY = event.clientY - layerRect.top;
  5147  
  5148        this.createRipple(layerX, layerY);
  5149      } else {
  5150        this.createRipple(event.offsetX, event.offsetY);
  5151      }
  5152    }
  5153  };
  5154  
  5155  /**
  5156   * Either remove or unlock any remaining ripples when the user mouses off of the element (either by
  5157   * mouseup, touchend or mouseleave event)
  5158   */
  5159  InkRippleCtrl.prototype.handleMouseup = function () {
  5160    autoCleanup(this, this.clearRipples);
  5161  };
  5162  
  5163  /**
  5164   * Either remove or unlock any remaining ripples when the user mouses off of the element (by
  5165   * touchmove)
  5166   */
  5167  InkRippleCtrl.prototype.handleTouchmove = function () {
  5168    autoCleanup(this, this.deleteRipples);
  5169  };
  5170  
  5171  /**
  5172   * Cycles through all ripples and attempts to remove them.
  5173   */
  5174  InkRippleCtrl.prototype.deleteRipples = function () {
  5175    for (var i = 0; i < this.ripples.length; i++) {
  5176      this.ripples[ i ].remove();
  5177    }
  5178  };
  5179  
  5180  /**
  5181   * Cycles through all ripples and attempts to remove them with fade.
  5182   * Depending on logic within `fadeInComplete`, some removals will be postponed.
  5183   */
  5184  InkRippleCtrl.prototype.clearRipples = function () {
  5185    for (var i = 0; i < this.ripples.length; i++) {
  5186      this.fadeInComplete(this.ripples[ i ]);
  5187    }
  5188  };
  5189  
  5190  /**
  5191   * Creates the ripple container element
  5192   * @returns {*}
  5193   */
  5194  InkRippleCtrl.prototype.createContainer = function () {
  5195    var container = angular.element('<div class="md-ripple-container"></div>');
  5196    this.$element.append(container);
  5197    return container;
  5198  };
  5199  
  5200  InkRippleCtrl.prototype.clearTimeout = function () {
  5201    if (this.timeout) {
  5202      this.$timeout.cancel(this.timeout);
  5203      this.timeout = null;
  5204    }
  5205  };
  5206  
  5207  InkRippleCtrl.prototype.isRippleAllowed = function () {
  5208    var element = this.$element[0];
  5209    do {
  5210      if (!element.tagName || element.tagName === 'BODY') break;
  5211  
  5212      if (element && angular.isFunction(element.hasAttribute)) {
  5213        if (element.hasAttribute('disabled')) return false;
  5214        if (this.inkRipple() === 'false' || this.inkRipple() === '0') return false;
  5215      }
  5216  
  5217    } while (element = element.parentNode);
  5218    return true;
  5219  };
  5220  
  5221  /**
  5222   * The attribute `md-ink-ripple` may be a static or interpolated
  5223   * color value OR a boolean indicator (used to disable ripples)
  5224   */
  5225  InkRippleCtrl.prototype.inkRipple = function () {
  5226    return this.$element.attr('md-ink-ripple');
  5227  };
  5228  
  5229  /**
  5230   * Creates a new ripple and adds it to the container.  Also tracks ripple in `this.ripples`.
  5231   * @param left
  5232   * @param top
  5233   */
  5234  InkRippleCtrl.prototype.createRipple = function (left, top) {
  5235    if (!this.isRippleAllowed()) return;
  5236  
  5237    var ctrl        = this;
  5238    var ripple      = angular.element('<div class="md-ripple"></div>');
  5239    var width       = this.$element.prop('clientWidth');
  5240    var height      = this.$element.prop('clientHeight');
  5241    var x           = Math.max(Math.abs(width - left), left) * 2;
  5242    var y           = Math.max(Math.abs(height - top), top) * 2;
  5243    var size        = getSize(this.options.fitRipple, x, y);
  5244    var color       = this.calculateColor();
  5245  
  5246    ripple.css({
  5247      left:            left + 'px',
  5248      top:             top + 'px',
  5249      background:      'black',
  5250      width:           size + 'px',
  5251      height:          size + 'px',
  5252      backgroundColor: rgbaToRGB(color),
  5253      borderColor:     rgbaToRGB(color)
  5254    });
  5255    this.lastRipple = ripple;
  5256  
  5257    // we only want one timeout to be running at a time
  5258    this.clearTimeout();
  5259    this.timeout    = this.$timeout(function () {
  5260      ctrl.clearTimeout();
  5261      if (!ctrl.mousedown) ctrl.fadeInComplete(ripple);
  5262    }, DURATION * 0.35, false);
  5263  
  5264    if (this.options.dimBackground) this.container.css({ backgroundColor: color });
  5265    this.container.append(ripple);
  5266    this.ripples.push(ripple);
  5267    ripple.addClass('md-ripple-placed');
  5268  
  5269    this.$mdUtil.nextTick(function () {
  5270  
  5271      ripple.addClass('md-ripple-scaled md-ripple-active');
  5272      ctrl.$timeout(function () {
  5273        ctrl.clearRipples();
  5274      }, DURATION, false);
  5275  
  5276    }, false);
  5277  
  5278    function rgbaToRGB (color) {
  5279      return color
  5280          ? color.replace('rgba', 'rgb').replace(/,[^\),]+\)/, ')')
  5281          : 'rgb(0,0,0)';
  5282    }
  5283  
  5284    function getSize (fit, x, y) {
  5285      return fit
  5286          ? Math.max(x, y)
  5287          : Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
  5288    }
  5289  };
  5290  
  5291  
  5292  
  5293  /**
  5294   * After fadeIn finishes, either kicks off the fade-out animation or queues the element for removal on mouseup
  5295   * @param ripple
  5296   */
  5297  InkRippleCtrl.prototype.fadeInComplete = function (ripple) {
  5298    if (this.lastRipple === ripple) {
  5299      if (!this.timeout && !this.mousedown) {
  5300        this.removeRipple(ripple);
  5301      }
  5302    } else {
  5303      this.removeRipple(ripple);
  5304    }
  5305  };
  5306  
  5307  /**
  5308   * Kicks off the animation for removing a ripple
  5309   * @param ripple {Element}
  5310   */
  5311  InkRippleCtrl.prototype.removeRipple = function (ripple) {
  5312    var ctrl  = this;
  5313    var index = this.ripples.indexOf(ripple);
  5314    if (index < 0) return;
  5315    this.ripples.splice(this.ripples.indexOf(ripple), 1);
  5316    ripple.removeClass('md-ripple-active');
  5317    if (this.ripples.length === 0) this.container.css({ backgroundColor: '' });
  5318    // use a 2-second timeout in order to allow for the animation to finish
  5319    // we don't actually care how long the animation takes
  5320    this.$timeout(function () {
  5321      ctrl.fadeOutComplete(ripple);
  5322    }, DURATION, false);
  5323  };
  5324  
  5325  /**
  5326   * Removes the provided ripple from the DOM
  5327   * @param ripple
  5328   */
  5329  InkRippleCtrl.prototype.fadeOutComplete = function (ripple) {
  5330    ripple.remove();
  5331    this.lastRipple = null;
  5332  };
  5333  
  5334  /**
  5335   * Used to create an empty directive.  This is used to track flag-directives whose children may have
  5336   * functionality based on them.
  5337   *
  5338   * Example: `md-no-ink` will potentially be used by all child directives.
  5339   */
  5340  function attrNoDirective () {
  5341    return { controller: angular.noop };
  5342  }
  5343  
  5344  })();
  5345  (function(){
  5346  "use strict";
  5347  
  5348  (function() {
  5349    'use strict';
  5350  
  5351      /**
  5352     * @ngdoc service
  5353     * @name $mdTabInkRipple
  5354     * @module material.core
  5355     *
  5356     * @description
  5357     * Provides ripple effects for md-tabs.  See $mdInkRipple service for all possible configuration options.
  5358     *
  5359     * @param {object=} scope Scope within the current context
  5360     * @param {object=} element The element the ripple effect should be applied to
  5361     * @param {object=} options (Optional) Configuration options to override the defaultripple configuration
  5362     */
  5363  
  5364    angular.module('material.core')
  5365      .factory('$mdTabInkRipple', MdTabInkRipple);
  5366  
  5367    function MdTabInkRipple($mdInkRipple) {
  5368      return {
  5369        attach: attach
  5370      };
  5371  
  5372      function attach(scope, element, options) {
  5373        return $mdInkRipple.attach(scope, element, angular.extend({
  5374          center: false,
  5375          dimBackground: true,
  5376          outline: false,
  5377          rippleSize: 'full'
  5378        }, options));
  5379      };
  5380    }
  5381    MdTabInkRipple.$inject = ["$mdInkRipple"];;
  5382  })();
  5383  
  5384  })();
  5385  (function(){
  5386  "use strict";
  5387  
  5388  // Polyfill angular < 1.4 (provide $animateCss)
  5389  angular
  5390    .module('material.core')
  5391    .factory('$$mdAnimate', ["$q", "$timeout", "$mdConstant", "$animateCss", function($q, $timeout, $mdConstant, $animateCss){
  5392  
  5393       // Since $$mdAnimate is injected into $mdUtil... use a wrapper function
  5394       // to subsequently inject $mdUtil as an argument to the AnimateDomUtils
  5395  
  5396       return function($mdUtil) {
  5397         return AnimateDomUtils( $mdUtil, $q, $timeout, $mdConstant, $animateCss);
  5398       };
  5399     }]);
  5400  
  5401  /**
  5402   * Factory function that requires special injections
  5403   */
  5404  function AnimateDomUtils($mdUtil, $q, $timeout, $mdConstant, $animateCss) {
  5405    var self;
  5406    return self = {
  5407      /**
  5408       *
  5409       */
  5410      translate3d : function( target, from, to, options ) {
  5411        return $animateCss(target,{
  5412          from:from,
  5413          to:to,
  5414          addClass:options.transitionInClass
  5415        })
  5416        .start()
  5417        .then(function(){
  5418            // Resolve with reverser function...
  5419            return reverseTranslate;
  5420        });
  5421  
  5422        /**
  5423         * Specific reversal of the request translate animation above...
  5424         */
  5425        function reverseTranslate (newFrom) {
  5426          return $animateCss(target, {
  5427             to: newFrom || from,
  5428             addClass: options.transitionOutClass,
  5429             removeClass: options.transitionInClass
  5430          }).start();
  5431  
  5432        }
  5433    },
  5434  
  5435      /**
  5436       * Listen for transitionEnd event (with optional timeout)
  5437       * Announce completion or failure via promise handlers
  5438       */
  5439      waitTransitionEnd: function (element, opts) {
  5440          var TIMEOUT = 3000; // fallback is 3 secs
  5441  
  5442          return $q(function(resolve, reject){
  5443            opts = opts || { };
  5444  
  5445            var timer = $timeout(finished, opts.timeout || TIMEOUT);
  5446            element.on($mdConstant.CSS.TRANSITIONEND, finished);
  5447  
  5448            /**
  5449             * Upon timeout or transitionEnd, reject or resolve (respectively) this promise.
  5450             * NOTE: Make sure this transitionEnd didn't bubble up from a child
  5451             */
  5452            function finished(ev) {
  5453              if ( ev && ev.target !== element[0]) return;
  5454  
  5455              if ( ev  ) $timeout.cancel(timer);
  5456              element.off($mdConstant.CSS.TRANSITIONEND, finished);
  5457  
  5458              // Never reject since ngAnimate may cause timeouts due missed transitionEnd events
  5459              resolve();
  5460  
  5461            }
  5462  
  5463          });
  5464        },
  5465  
  5466      /**
  5467       * Calculate the zoom transform from dialog to origin.
  5468       *
  5469       * We use this to set the dialog position immediately;
  5470       * then the md-transition-in actually translates back to
  5471       * `translate3d(0,0,0) scale(1.0)`...
  5472       *
  5473       * NOTE: all values are rounded to the nearest integer
  5474       */
  5475      calculateZoomToOrigin: function (element, originator) {
  5476        var origin = originator.element;
  5477        var bounds = originator.bounds;
  5478  
  5479        var zoomTemplate = "translate3d( {centerX}px, {centerY}px, 0 ) scale( {scaleX}, {scaleY} )";
  5480        var buildZoom = angular.bind(null, $mdUtil.supplant, zoomTemplate);
  5481        var zoomStyle = buildZoom({centerX: 0, centerY: 0, scaleX: 0.5, scaleY: 0.5});
  5482  
  5483        if (origin || bounds) {
  5484          var originBnds = origin ? self.clientRect(origin) || currentBounds() : self.copyRect(bounds);
  5485          var dialogRect = self.copyRect(element[0].getBoundingClientRect());
  5486          var dialogCenterPt = self.centerPointFor(dialogRect);
  5487          var originCenterPt = self.centerPointFor(originBnds);
  5488  
  5489          // Build the transform to zoom from the dialog center to the origin center
  5490  
  5491          zoomStyle = buildZoom({
  5492            centerX: originCenterPt.x - dialogCenterPt.x,
  5493            centerY: originCenterPt.y - dialogCenterPt.y,
  5494            scaleX: Math.round(100 * Math.min(0.5, originBnds.width / dialogRect.width))/100,
  5495            scaleY: Math.round(100 * Math.min(0.5, originBnds.height / dialogRect.height))/100
  5496          });
  5497        }
  5498  
  5499        return zoomStyle;
  5500  
  5501        /**
  5502         * This is a fallback if the origin information is no longer valid, then the
  5503         * origin bounds simply becomes the current bounds for the dialogContainer's parent
  5504         */
  5505        function currentBounds() {
  5506          var cntr = element ? element.parent() : null;
  5507          var parent = cntr ? cntr.parent() : null;
  5508  
  5509          return parent ? self.clientRect(parent) : null;
  5510        }
  5511      },
  5512  
  5513      /**
  5514       * Enhance raw values to represent valid css stylings...
  5515       */
  5516      toCss : function( raw ) {
  5517        var css = { };
  5518        var lookups = 'left top right bottom width height x y min-width min-height max-width max-height';
  5519  
  5520        angular.forEach(raw, function(value,key) {
  5521          if ( angular.isUndefined(value) ) return;
  5522  
  5523          if ( lookups.indexOf(key) >= 0 ) {
  5524            css[key] = value + 'px';
  5525          } else {
  5526            switch (key) {
  5527              case 'transition':
  5528                convertToVendor(key, $mdConstant.CSS.TRANSITION, value);
  5529                break;
  5530              case 'transform':
  5531                convertToVendor(key, $mdConstant.CSS.TRANSFORM, value);
  5532                break;
  5533              case 'transformOrigin':
  5534                convertToVendor(key, $mdConstant.CSS.TRANSFORM_ORIGIN, value);
  5535                break;
  5536            }
  5537          }
  5538        });
  5539  
  5540        return css;
  5541  
  5542        function convertToVendor(key, vendor, value) {
  5543          angular.forEach(vendor.split(' '), function (key) {
  5544            css[key] = value;
  5545          });
  5546        }
  5547      },
  5548  
  5549      /**
  5550       * Convert the translate CSS value to key/value pair(s).
  5551       */
  5552      toTransformCss: function (transform, addTransition, transition) {
  5553        var css = {};
  5554        angular.forEach($mdConstant.CSS.TRANSFORM.split(' '), function (key) {
  5555          css[key] = transform;
  5556        });
  5557  
  5558        if (addTransition) {
  5559          transition = transition || "all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1) !important";
  5560          css['transition'] = transition;
  5561        }
  5562  
  5563        return css;
  5564      },
  5565  
  5566      /**
  5567       *  Clone the Rect and calculate the height/width if needed
  5568       */
  5569      copyRect: function (source, destination) {
  5570        if (!source) return null;
  5571  
  5572        destination = destination || {};
  5573  
  5574        angular.forEach('left top right bottom width height'.split(' '), function (key) {
  5575          destination[key] = Math.round(source[key])
  5576        });
  5577  
  5578        destination.width = destination.width || (destination.right - destination.left);
  5579        destination.height = destination.height || (destination.bottom - destination.top);
  5580  
  5581        return destination;
  5582      },
  5583  
  5584      /**
  5585       * Calculate ClientRect of element; return null if hidden or zero size
  5586       */
  5587      clientRect: function (element) {
  5588        var bounds = angular.element(element)[0].getBoundingClientRect();
  5589        var isPositiveSizeClientRect = function (rect) {
  5590          return rect && (rect.width > 0) && (rect.height > 0);
  5591        };
  5592  
  5593        // If the event origin element has zero size, it has probably been hidden.
  5594        return isPositiveSizeClientRect(bounds) ? self.copyRect(bounds) : null;
  5595      },
  5596  
  5597      /**
  5598       *  Calculate 'rounded' center point of Rect
  5599       */
  5600      centerPointFor: function (targetRect) {
  5601        return targetRect ? {
  5602          x: Math.round(targetRect.left + (targetRect.width / 2)),
  5603          y: Math.round(targetRect.top + (targetRect.height / 2))
  5604        } : { x : 0, y : 0 };
  5605      }
  5606  
  5607    };
  5608  };
  5609  
  5610  
  5611  })();
  5612  (function(){
  5613  "use strict";
  5614  
  5615  "use strict";
  5616  
  5617  if (angular.version.minor >= 4) {
  5618    angular.module('material.core.animate', []);
  5619  } else {
  5620  (function() {
  5621  
  5622    var forEach = angular.forEach;
  5623  
  5624    var WEBKIT = angular.isDefined(document.documentElement.style.WebkitAppearance);
  5625    var TRANSITION_PROP = WEBKIT ? 'WebkitTransition' : 'transition';
  5626    var ANIMATION_PROP = WEBKIT ? 'WebkitAnimation' : 'animation';
  5627    var PREFIX = WEBKIT ? '-webkit-' : '';
  5628  
  5629    var TRANSITION_EVENTS = (WEBKIT ? 'webkitTransitionEnd ' : '') + 'transitionend';
  5630    var ANIMATION_EVENTS = (WEBKIT ? 'webkitAnimationEnd ' : '') + 'animationend';
  5631  
  5632    var $$ForceReflowFactory = ['$document', function($document) {
  5633      return function() {
  5634        return $document[0].body.clientWidth + 1;
  5635      }
  5636    }];
  5637  
  5638    var $$rAFMutexFactory = ['$$rAF', function($$rAF) {
  5639      return function() {
  5640        var passed = false;
  5641        $$rAF(function() {
  5642          passed = true;
  5643        });
  5644        return function(fn) {
  5645          passed ? fn() : $$rAF(fn);
  5646        };
  5647      };
  5648    }];
  5649  
  5650    var $$AnimateRunnerFactory = ['$q', '$$rAFMutex', function($q, $$rAFMutex) {
  5651      var INITIAL_STATE = 0;
  5652      var DONE_PENDING_STATE = 1;
  5653      var DONE_COMPLETE_STATE = 2;
  5654  
  5655      function AnimateRunner(host) {
  5656        this.setHost(host);
  5657  
  5658        this._doneCallbacks = [];
  5659        this._runInAnimationFrame = $$rAFMutex();
  5660        this._state = 0;
  5661      }
  5662  
  5663      AnimateRunner.prototype = {
  5664        setHost: function(host) {
  5665          this.host = host || {};
  5666        },
  5667  
  5668        done: function(fn) {
  5669          if (this._state === DONE_COMPLETE_STATE) {
  5670            fn();
  5671          } else {
  5672            this._doneCallbacks.push(fn);
  5673          }
  5674        },
  5675  
  5676        progress: angular.noop,
  5677  
  5678        getPromise: function() {
  5679          if (!this.promise) {
  5680            var self = this;
  5681            this.promise = $q(function(resolve, reject) {
  5682              self.done(function(status) {
  5683                status === false ? reject() : resolve();
  5684              });
  5685            });
  5686          }
  5687          return this.promise;
  5688        },
  5689  
  5690        then: function(resolveHandler, rejectHandler) {
  5691          return this.getPromise().then(resolveHandler, rejectHandler);
  5692        },
  5693  
  5694        'catch': function(handler) {
  5695          return this.getPromise()['catch'](handler);
  5696        },
  5697  
  5698        'finally': function(handler) {
  5699          return this.getPromise()['finally'](handler);
  5700        },
  5701  
  5702        pause: function() {
  5703          if (this.host.pause) {
  5704            this.host.pause();
  5705          }
  5706        },
  5707  
  5708        resume: function() {
  5709          if (this.host.resume) {
  5710            this.host.resume();
  5711          }
  5712        },
  5713  
  5714        end: function() {
  5715          if (this.host.end) {
  5716            this.host.end();
  5717          }
  5718          this._resolve(true);
  5719        },
  5720  
  5721        cancel: function() {
  5722          if (this.host.cancel) {
  5723            this.host.cancel();
  5724          }
  5725          this._resolve(false);
  5726        },
  5727  
  5728        complete: function(response) {
  5729          var self = this;
  5730          if (self._state === INITIAL_STATE) {
  5731            self._state = DONE_PENDING_STATE;
  5732            self._runInAnimationFrame(function() {
  5733              self._resolve(response);
  5734            });
  5735          }
  5736        },
  5737  
  5738        _resolve: function(response) {
  5739          if (this._state !== DONE_COMPLETE_STATE) {
  5740            forEach(this._doneCallbacks, function(fn) {
  5741              fn(response);
  5742            });
  5743            this._doneCallbacks.length = 0;
  5744            this._state = DONE_COMPLETE_STATE;
  5745          }
  5746        }
  5747      };
  5748  
  5749      return AnimateRunner;
  5750    }];
  5751  
  5752    angular
  5753      .module('material.core.animate', [])
  5754      .factory('$$forceReflow', $$ForceReflowFactory)
  5755      .factory('$$AnimateRunner', $$AnimateRunnerFactory)
  5756      .factory('$$rAFMutex', $$rAFMutexFactory)
  5757      .factory('$animateCss', ['$window', '$$rAF', '$$AnimateRunner', '$$forceReflow', '$$jqLite', '$timeout',
  5758                       function($window,   $$rAF,   $$AnimateRunner,   $$forceReflow,   $$jqLite,   $timeout) {
  5759  
  5760        function init(element, options) {
  5761  
  5762          var temporaryStyles = [];
  5763          var node = getDomNode(element);
  5764  
  5765          if (options.transitionStyle) {
  5766            temporaryStyles.push([PREFIX + 'transition', options.transitionStyle]);
  5767          }
  5768  
  5769          if (options.keyframeStyle) {
  5770            temporaryStyles.push([PREFIX + 'animation', options.keyframeStyle]);
  5771          }
  5772  
  5773          if (options.delay) {
  5774            temporaryStyles.push([PREFIX + 'transition-delay', options.delay + 's']);
  5775          }
  5776  
  5777          if (options.duration) {
  5778            temporaryStyles.push([PREFIX + 'transition-duration', options.duration + 's']);
  5779          }
  5780  
  5781          var hasCompleteStyles = options.keyframeStyle ||
  5782                                  (options.to && (options.duration > 0 || options.transitionStyle));
  5783          var hasCompleteClasses = !!options.addClass || !!options.removeClass;
  5784          var hasCompleteAnimation = hasCompleteStyles || hasCompleteClasses;
  5785  
  5786          blockTransition(element, true);
  5787          applyAnimationFromStyles(element, options);
  5788  
  5789          var animationClosed = false;
  5790          var events, eventFn;
  5791  
  5792          return {
  5793            close: $window.close,
  5794            start: function() {
  5795              var runner = new $$AnimateRunner();
  5796              waitUntilQuiet(function() {
  5797                blockTransition(element, false);
  5798                if (!hasCompleteAnimation) {
  5799                  return close();
  5800                }
  5801  
  5802                forEach(temporaryStyles, function(entry) {
  5803                  var key = entry[0];
  5804                  var value = entry[1];
  5805                  node.style[camelCase(key)] = value;
  5806                });
  5807  
  5808                applyClasses(element, options);
  5809  
  5810                var timings = computeTimings(element);
  5811                if (timings.duration === 0) {
  5812                  return close();
  5813                }
  5814  
  5815                var moreStyles = [];
  5816  
  5817                if (options.easing) {
  5818                  if (timings.transitionDuration) {
  5819                    moreStyles.push([PREFIX + 'transition-timing-function', options.easing]);
  5820                  }
  5821                  if (timings.animationDuration) {
  5822                    moreStyles.push([PREFIX + 'animation-timing-function', options.easing]);
  5823                  }
  5824                }
  5825  
  5826                if (options.delay && timings.animationDelay) {
  5827                  moreStyles.push([PREFIX + 'animation-delay', options.delay + 's']);
  5828                }
  5829  
  5830                if (options.duration && timings.animationDuration) {
  5831                  moreStyles.push([PREFIX + 'animation-duration', options.duration + 's']);
  5832                }
  5833  
  5834                forEach(moreStyles, function(entry) {
  5835                  var key = entry[0];
  5836                  var value = entry[1];
  5837                  node.style[camelCase(key)] = value;
  5838                  temporaryStyles.push(entry);
  5839                });
  5840  
  5841                var maxDelay = timings.delay;
  5842                var maxDelayTime = maxDelay * 1000;
  5843                var maxDuration = timings.duration;
  5844                var maxDurationTime = maxDuration * 1000;
  5845                var startTime = Date.now();
  5846  
  5847                events = [];
  5848                if (timings.transitionDuration) {
  5849                  events.push(TRANSITION_EVENTS);
  5850                }
  5851                if (timings.animationDuration) {
  5852                  events.push(ANIMATION_EVENTS);
  5853                }
  5854                events = events.join(' ');
  5855                eventFn = function(event) {
  5856                  event.stopPropagation();
  5857                  var ev = event.originalEvent || event;
  5858                  var timeStamp = ev.timeStamp || Date.now();
  5859                  var elapsedTime = parseFloat(ev.elapsedTime.toFixed(3));
  5860                  if (Math.max(timeStamp - startTime, 0) >= maxDelayTime && elapsedTime >= maxDuration) {
  5861                    close();
  5862                  }
  5863                };
  5864                element.on(events, eventFn);
  5865  
  5866                applyAnimationToStyles(element, options);
  5867  
  5868                $timeout(close, maxDelayTime + maxDurationTime * 1.5, false);
  5869              });
  5870  
  5871              return runner;
  5872  
  5873              function close() {
  5874                if (animationClosed) return;
  5875                animationClosed = true;
  5876  
  5877                if (events && eventFn) {
  5878                  element.off(events, eventFn);
  5879                }
  5880                applyClasses(element, options);
  5881                applyAnimationStyles(element, options);
  5882                forEach(temporaryStyles, function(entry) {
  5883                  node.style[camelCase(entry[0])] = '';
  5884                });
  5885                runner.complete(true);
  5886                return runner;
  5887              }
  5888            }
  5889          }
  5890        }
  5891  
  5892        function applyClasses(element, options) {
  5893          if (options.addClass) {
  5894            $$jqLite.addClass(element, options.addClass);
  5895            options.addClass = null;
  5896          }
  5897          if (options.removeClass) {
  5898            $$jqLite.removeClass(element, options.removeClass);
  5899            options.removeClass = null;
  5900          }
  5901        }
  5902  
  5903        function computeTimings(element) {
  5904          var node = getDomNode(element);
  5905          var cs = $window.getComputedStyle(node)
  5906          var tdr = parseMaxTime(cs[prop('transitionDuration')]);
  5907          var adr = parseMaxTime(cs[prop('animationDuration')]);
  5908          var tdy = parseMaxTime(cs[prop('transitionDelay')]);
  5909          var ady = parseMaxTime(cs[prop('animationDelay')]);
  5910  
  5911          adr *= (parseInt(cs[prop('animationIterationCount')], 10) || 1);
  5912          var duration = Math.max(adr, tdr);
  5913          var delay = Math.max(ady, tdy);
  5914  
  5915          return {
  5916            duration: duration,
  5917            delay: delay,
  5918            animationDuration: adr,
  5919            transitionDuration: tdr,
  5920            animationDelay: ady,
  5921            transitionDelay: tdy
  5922          };
  5923  
  5924          function prop(key) {
  5925            return WEBKIT ? 'Webkit' + key.charAt(0).toUpperCase() + key.substr(1)
  5926                          : key;
  5927          }
  5928        }
  5929  
  5930        function parseMaxTime(str) {
  5931          var maxValue = 0;
  5932          var values = (str || "").split(/\s*,\s*/);
  5933          forEach(values, function(value) {
  5934            // it's always safe to consider only second values and omit `ms` values since
  5935            // getComputedStyle will always handle the conversion for us
  5936            if (value.charAt(value.length - 1) == 's') {
  5937              value = value.substring(0, value.length - 1);
  5938            }
  5939            value = parseFloat(value) || 0;
  5940            maxValue = maxValue ? Math.max(value, maxValue) : value;
  5941          });
  5942          return maxValue;
  5943        }
  5944  
  5945        var cancelLastRAFRequest;
  5946        var rafWaitQueue = [];
  5947        function waitUntilQuiet(callback) {
  5948          if (cancelLastRAFRequest) {
  5949            cancelLastRAFRequest(); //cancels the request
  5950          }
  5951          rafWaitQueue.push(callback);
  5952          cancelLastRAFRequest = $$rAF(function() {
  5953            cancelLastRAFRequest = null;
  5954  
  5955            // DO NOT REMOVE THIS LINE OR REFACTOR OUT THE `pageWidth` variable.
  5956            // PLEASE EXAMINE THE `$$forceReflow` service to understand why.
  5957            var pageWidth = $$forceReflow();
  5958  
  5959            // we use a for loop to ensure that if the queue is changed
  5960            // during this looping then it will consider new requests
  5961            for (var i = 0; i < rafWaitQueue.length; i++) {
  5962              rafWaitQueue[i](pageWidth);
  5963            }
  5964            rafWaitQueue.length = 0;
  5965          });
  5966        }
  5967  
  5968        function applyAnimationStyles(element, options) {
  5969          applyAnimationFromStyles(element, options);
  5970          applyAnimationToStyles(element, options);
  5971        }
  5972  
  5973        function applyAnimationFromStyles(element, options) {
  5974          if (options.from) {
  5975            element.css(options.from);
  5976            options.from = null;
  5977          }
  5978        }
  5979  
  5980        function applyAnimationToStyles(element, options) {
  5981          if (options.to) {
  5982            element.css(options.to);
  5983            options.to = null;
  5984          }
  5985        }
  5986  
  5987        function getDomNode(element) {
  5988          for (var i = 0; i < element.length; i++) {
  5989            if (element[i].nodeType === 1) return element[i];
  5990          }
  5991        }
  5992  
  5993        function blockTransition(element, bool) {
  5994          var node = getDomNode(element);
  5995          var key = camelCase(PREFIX + 'transition-delay');
  5996          node.style[key] = bool ? '-9999s' : '';
  5997        }
  5998  
  5999        return init;
  6000      }]);
  6001  
  6002    /**
  6003     * Older browsers [FF31] expect camelCase
  6004     * property keys.
  6005     * e.g.
  6006     *  animation-duration --> animationDuration
  6007     */
  6008    function camelCase(str) {
  6009      return str.replace(/-[a-z]/g, function(str) {
  6010        return str.charAt(1).toUpperCase();
  6011      });
  6012    }
  6013  
  6014  })();
  6015  
  6016  }
  6017  
  6018  })();
  6019  (function(){
  6020  "use strict";
  6021  
  6022  /**
  6023   * @ngdoc module
  6024   * @name material.components.autocomplete
  6025   */
  6026  /*
  6027   * @see js folder for autocomplete implementation
  6028   */
  6029  angular.module('material.components.autocomplete', [
  6030    'material.core',
  6031    'material.components.icon',
  6032    'material.components.virtualRepeat'
  6033  ]);
  6034  
  6035  })();
  6036  (function(){
  6037  "use strict";
  6038  
  6039  /*
  6040   * @ngdoc module
  6041   * @name material.components.backdrop
  6042   * @description Backdrop
  6043   */
  6044  
  6045  /**
  6046   * @ngdoc directive
  6047   * @name mdBackdrop
  6048   * @module material.components.backdrop
  6049   *
  6050   * @restrict E
  6051   *
  6052   * @description
  6053   * `<md-backdrop>` is a backdrop element used by other components, such as dialog and bottom sheet.
  6054   * Apply class `opaque` to make the backdrop use the theme backdrop color.
  6055   *
  6056   */
  6057  
  6058  angular
  6059    .module('material.components.backdrop', ['material.core'])
  6060    .directive('mdBackdrop', ["$mdTheming", "$animate", "$rootElement", "$window", "$log", "$$rAF", "$document", function BackdropDirective($mdTheming, $animate, $rootElement, $window, $log, $$rAF, $document) {
  6061      var ERROR_CSS_POSITION = "<md-backdrop> may not work properly in a scrolled, static-positioned parent container.";
  6062  
  6063      return {
  6064        restrict: 'E',
  6065        link: postLink
  6066      };
  6067  
  6068      function postLink(scope, element, attrs) {
  6069  
  6070        // If body scrolling has been disabled using mdUtil.disableBodyScroll(),
  6071        // adjust the 'backdrop' height to account for the fixed 'body' top offset
  6072        var body = $window.getComputedStyle($document[0].body);
  6073        if (body.position == 'fixed') {
  6074          var hViewport = parseInt(body.height, 10) + Math.abs(parseInt(body.top, 10));
  6075          element.css({
  6076            height: hViewport + 'px'
  6077          });
  6078        }
  6079  
  6080        // backdrop may be outside the $rootElement, tell ngAnimate to animate regardless
  6081        if ($animate.pin) $animate.pin(element, $rootElement);
  6082  
  6083        $$rAF(function () {
  6084  
  6085          // Often $animate.enter() is used to append the backDrop element
  6086          // so let's wait until $animate is done...
  6087          var parent = element.parent()[0];
  6088          if (parent) {
  6089  
  6090            if ( parent.nodeName == 'BODY' ) {
  6091              element.css({position : 'fixed'});
  6092            }
  6093  
  6094            var styles = $window.getComputedStyle(parent);
  6095            if (styles.position == 'static') {
  6096              // backdrop uses position:absolute and will not work properly with parent position:static (default)
  6097              $log.warn(ERROR_CSS_POSITION);
  6098            }
  6099          }
  6100  
  6101          // Only inherit the parent if the backdrop has a parent.
  6102          if (element.parent().length) {
  6103            $mdTheming.inherit(element, element.parent());
  6104          }
  6105        });
  6106  
  6107      }
  6108  
  6109    }]);
  6110  
  6111  })();
  6112  (function(){
  6113  "use strict";
  6114  
  6115  /**
  6116   * @ngdoc module
  6117   * @name material.components.button
  6118   * @description
  6119   *
  6120   * Button
  6121   */
  6122  angular
  6123      .module('material.components.button', [ 'material.core' ])
  6124      .directive('mdButton', MdButtonDirective);
  6125  
  6126  /**
  6127   * @ngdoc directive
  6128   * @name mdButton
  6129   * @module material.components.button
  6130   *
  6131   * @restrict E
  6132   *
  6133   * @description
  6134   * `<md-button>` is a button directive with optional ink ripples (default enabled).
  6135   *
  6136   * If you supply a `href` or `ng-href` attribute, it will become an `<a>` element. Otherwise, it will
  6137   * become a `<button>` element. As per the [Material Design specifications](http://www.google.com/design/spec/style/color.html#color-ui-color-application)
  6138   * the FAB button background is filled with the accent color [by default]. The primary color palette may be used with
  6139   * the `md-primary` class.
  6140   *
  6141   * @param {boolean=} md-no-ink If present, disable ripple ink effects.
  6142   * @param {expression=} ng-disabled En/Disable based on the expression
  6143   * @param {string=} md-ripple-size Overrides the default ripple size logic. Options: `full`, `partial`, `auto`
  6144   * @param {string=} aria-label Adds alternative text to button for accessibility, useful for icon buttons.
  6145   * If no default text is found, a warning will be logged.
  6146   *
  6147   * @usage
  6148   *
  6149   * Regular buttons:
  6150   *
  6151   * <hljs lang="html">
  6152   *  <md-button> Flat Button </md-button>
  6153   *  <md-button href="http://google.com"> Flat link </md-button>
  6154   *  <md-button class="md-raised"> Raised Button </md-button>
  6155   *  <md-button ng-disabled="true"> Disabled Button </md-button>
  6156   *  <md-button>
  6157   *    <md-icon md-svg-src="your/icon.svg"></md-icon>
  6158   *    Register Now
  6159   *  </md-button>
  6160   * </hljs>
  6161   *
  6162   * FAB buttons:
  6163   *
  6164   * <hljs lang="html">
  6165   *  <md-button class="md-fab" aria-label="FAB">
  6166   *    <md-icon md-svg-src="your/icon.svg"></md-icon>
  6167   *  </md-button>
  6168   *  <!-- mini-FAB -->
  6169   *  <md-button class="md-fab md-mini" aria-label="Mini FAB">
  6170   *    <md-icon md-svg-src="your/icon.svg"></md-icon>
  6171   *  </md-button>
  6172   *  <!-- Button with SVG Icon -->
  6173   *  <md-button class="md-icon-button" aria-label="Custom Icon Button">
  6174   *    <md-icon md-svg-icon="path/to/your.svg"></md-icon>
  6175   *  </md-button>
  6176   * </hljs>
  6177   */
  6178  function MdButtonDirective($mdButtonInkRipple, $mdTheming, $mdAria, $timeout) {
  6179  
  6180    return {
  6181      restrict: 'EA',
  6182      replace: true,
  6183      transclude: true,
  6184      template: getTemplate,
  6185      link: postLink
  6186    };
  6187  
  6188    function isAnchor(attr) {
  6189      return angular.isDefined(attr.href) || angular.isDefined(attr.ngHref) || angular.isDefined(attr.ngLink) || angular.isDefined(attr.uiSref);
  6190    }
  6191  
  6192    function getTemplate(element, attr) {
  6193      if (isAnchor(attr)) {
  6194        return '<a class="md-button" ng-transclude></a>';
  6195      } else {
  6196        //If buttons don't have type="button", they will submit forms automatically.
  6197        var btnType = (typeof attr.type === 'undefined') ? 'button' : attr.type;
  6198        return '<button class="md-button" type="' + btnType + '" ng-transclude></button>';
  6199      }
  6200    }
  6201  
  6202    function postLink(scope, element, attr) {
  6203      $mdTheming(element);
  6204      $mdButtonInkRipple.attach(scope, element);
  6205  
  6206      // Use async expect to support possible bindings in the button label
  6207      $mdAria.expectWithText(element, 'aria-label');
  6208  
  6209      // For anchor elements, we have to set tabindex manually when the
  6210      // element is disabled
  6211      if (isAnchor(attr) && angular.isDefined(attr.ngDisabled) ) {
  6212        scope.$watch(attr.ngDisabled, function(isDisabled) {
  6213          element.attr('tabindex', isDisabled ? -1 : 0);
  6214        });
  6215      }
  6216  
  6217      // disabling click event when disabled is true
  6218      element.on('click', function(e){
  6219        if (attr.disabled === true) {
  6220          e.preventDefault();
  6221          e.stopImmediatePropagation();
  6222        }
  6223      });
  6224  
  6225      // restrict focus styles to the keyboard
  6226      scope.mouseActive = false;
  6227      element.on('mousedown', function() {
  6228          scope.mouseActive = true;
  6229          $timeout(function(){
  6230            scope.mouseActive = false;
  6231          }, 100);
  6232        })
  6233        .on('focus', function() {
  6234          if (scope.mouseActive === false) {
  6235            element.addClass('md-focused');
  6236          }
  6237        })
  6238        .on('blur', function(ev) {
  6239          element.removeClass('md-focused');
  6240        });
  6241    }
  6242  
  6243  }
  6244  MdButtonDirective.$inject = ["$mdButtonInkRipple", "$mdTheming", "$mdAria", "$timeout"];
  6245  
  6246  })();
  6247  (function(){
  6248  "use strict";
  6249  
  6250  /**
  6251   * @ngdoc module
  6252   * @name material.components.bottomSheet
  6253   * @description
  6254   * BottomSheet
  6255   */
  6256  angular
  6257    .module('material.components.bottomSheet', [
  6258      'material.core',
  6259      'material.components.backdrop'
  6260    ])
  6261    .directive('mdBottomSheet', MdBottomSheetDirective)
  6262    .provider('$mdBottomSheet', MdBottomSheetProvider);
  6263  
  6264  /* @ngInject */
  6265  function MdBottomSheetDirective($mdBottomSheet) {
  6266    return {
  6267      restrict: 'E',
  6268      link : function postLink(scope, element, attr) {
  6269        // When navigation force destroys an interimElement, then
  6270        // listen and $destroy() that interim instance...
  6271        scope.$on('$destroy', function() {
  6272          $mdBottomSheet.destroy();
  6273        });
  6274      }
  6275    };
  6276  }
  6277  MdBottomSheetDirective.$inject = ["$mdBottomSheet"];
  6278  
  6279  
  6280  /**
  6281   * @ngdoc service
  6282   * @name $mdBottomSheet
  6283   * @module material.components.bottomSheet
  6284   *
  6285   * @description
  6286   * `$mdBottomSheet` opens a bottom sheet over the app and provides a simple promise API.
  6287   *
  6288   * ## Restrictions
  6289   *
  6290   * - The bottom sheet's template must have an outer `<md-bottom-sheet>` element.
  6291   * - Add the `md-grid` class to the bottom sheet for a grid layout.
  6292   * - Add the `md-list` class to the bottom sheet for a list layout.
  6293   *
  6294   * @usage
  6295   * <hljs lang="html">
  6296   * <div ng-controller="MyController">
  6297   *   <md-button ng-click="openBottomSheet()">
  6298   *     Open a Bottom Sheet!
  6299   *   </md-button>
  6300   * </div>
  6301   * </hljs>
  6302   * <hljs lang="js">
  6303   * var app = angular.module('app', ['ngMaterial']);
  6304   * app.controller('MyController', function($scope, $mdBottomSheet) {
  6305   *   $scope.openBottomSheet = function() {
  6306   *     $mdBottomSheet.show({
  6307   *       template: '<md-bottom-sheet>Hello!</md-bottom-sheet>'
  6308   *     });
  6309   *   };
  6310   * });
  6311   * </hljs>
  6312   */
  6313  
  6314   /**
  6315   * @ngdoc method
  6316   * @name $mdBottomSheet#show
  6317   *
  6318   * @description
  6319   * Show a bottom sheet with the specified options.
  6320   *
  6321   * @param {object} options An options object, with the following properties:
  6322   *
  6323   *   - `templateUrl` - `{string=}`: The url of an html template file that will
  6324   *   be used as the content of the bottom sheet. Restrictions: the template must
  6325   *   have an outer `md-bottom-sheet` element.
  6326   *   - `template` - `{string=}`: Same as templateUrl, except this is an actual
  6327   *   template string.
  6328   *   - `scope` - `{object=}`: the scope to link the template / controller to. If none is specified, it will create a new child scope.
  6329   *     This scope will be destroyed when the bottom sheet is removed unless `preserveScope` is set to true.
  6330   *   - `preserveScope` - `{boolean=}`: whether to preserve the scope when the element is removed. Default is false
  6331   *   - `controller` - `{string=}`: The controller to associate with this bottom sheet.
  6332   *   - `locals` - `{string=}`: An object containing key/value pairs. The keys will
  6333   *   be used as names of values to inject into the controller. For example,
  6334   *   `locals: {three: 3}` would inject `three` into the controller with the value
  6335   *   of 3.
  6336   *   - `clickOutsideToClose` - `{boolean=}`: Whether the user can click outside the bottom sheet to
  6337   *     close it. Default true.
  6338   *   - `disableBackdrop` - `{boolean=}`: When set to true, the bottomsheet will not show a backdrop.
  6339   *   - `escapeToClose` - `{boolean=}`: Whether the user can press escape to close the bottom sheet.
  6340   *     Default true.
  6341   *   - `resolve` - `{object=}`: Similar to locals, except it takes promises as values
  6342   *   and the bottom sheet will not open until the promises resolve.
  6343   *   - `controllerAs` - `{string=}`: An alias to assign the controller to on the scope.
  6344   *   - `parent` - `{element=}`: The element to append the bottom sheet to. The `parent` may be a `function`, `string`,
  6345   *   `object`, or null. Defaults to appending to the body of the root element (or the root element) of the application.
  6346   *   e.g. angular.element(document.getElementById('content')) or "#content"
  6347   *   - `disableParentScroll` - `{boolean=}`: Whether to disable scrolling while the bottom sheet is open.
  6348   *     Default true.
  6349   *
  6350   * @returns {promise} A promise that can be resolved with `$mdBottomSheet.hide()` or
  6351   * rejected with `$mdBottomSheet.cancel()`.
  6352   */
  6353  
  6354  /**
  6355   * @ngdoc method
  6356   * @name $mdBottomSheet#hide
  6357   *
  6358   * @description
  6359   * Hide the existing bottom sheet and resolve the promise returned from
  6360   * `$mdBottomSheet.show()`. This call will close the most recently opened/current bottomsheet (if any).
  6361   *
  6362   * @param {*=} response An argument for the resolved promise.
  6363   *
  6364   */
  6365  
  6366  /**
  6367   * @ngdoc method
  6368   * @name $mdBottomSheet#cancel
  6369   *
  6370   * @description
  6371   * Hide the existing bottom sheet and reject the promise returned from
  6372   * `$mdBottomSheet.show()`.
  6373   *
  6374   * @param {*=} response An argument for the rejected promise.
  6375   *
  6376   */
  6377  
  6378  function MdBottomSheetProvider($$interimElementProvider) {
  6379    // how fast we need to flick down to close the sheet, pixels/ms
  6380    var CLOSING_VELOCITY = 0.5;
  6381    var PADDING = 80; // same as css
  6382  
  6383    bottomSheetDefaults.$inject = ["$animate", "$mdConstant", "$mdUtil", "$mdTheming", "$mdBottomSheet", "$rootElement", "$mdGesture"];
  6384    return $$interimElementProvider('$mdBottomSheet')
  6385      .setDefaults({
  6386        methods: ['disableParentScroll', 'escapeToClose', 'clickOutsideToClose'],
  6387        options: bottomSheetDefaults
  6388      });
  6389  
  6390    /* @ngInject */
  6391    function bottomSheetDefaults($animate, $mdConstant, $mdUtil, $mdTheming, $mdBottomSheet, $rootElement, $mdGesture) {
  6392      var backdrop;
  6393  
  6394      return {
  6395        themable: true,
  6396        onShow: onShow,
  6397        onRemove: onRemove,
  6398        disableBackdrop: false,
  6399        escapeToClose: true,
  6400        clickOutsideToClose: true,
  6401        disableParentScroll: true
  6402      };
  6403  
  6404  
  6405      function onShow(scope, element, options, controller) {
  6406  
  6407        element = $mdUtil.extractElementByName(element, 'md-bottom-sheet');
  6408  
  6409        // prevent tab focus or click focus on the bottom-sheet container
  6410        element.attr('tabindex',"-1");
  6411  
  6412        if (!options.disableBackdrop) {
  6413          // Add a backdrop that will close on click
  6414          backdrop = $mdUtil.createBackdrop(scope, "_md-bottom-sheet-backdrop md-opaque");
  6415  
  6416          // Prevent mouse focus on backdrop; ONLY programatic focus allowed.
  6417          // This allows clicks on backdrop to propagate to the $rootElement and
  6418          // ESC key events to be detected properly.
  6419          
  6420          backdrop[0].tabIndex = -1;
  6421  
  6422          if (options.clickOutsideToClose) {
  6423            backdrop.on('click', function() {
  6424              $mdUtil.nextTick($mdBottomSheet.cancel,true);
  6425            });
  6426          }
  6427  
  6428          $mdTheming.inherit(backdrop, options.parent);
  6429  
  6430          $animate.enter(backdrop, options.parent, null);
  6431        }
  6432  
  6433        var bottomSheet = new BottomSheet(element, options.parent);
  6434        options.bottomSheet = bottomSheet;
  6435  
  6436        $mdTheming.inherit(bottomSheet.element, options.parent);
  6437  
  6438        if (options.disableParentScroll) {
  6439          options.restoreScroll = $mdUtil.disableScrollAround(bottomSheet.element, options.parent);
  6440        }
  6441  
  6442        return $animate.enter(bottomSheet.element, options.parent, backdrop)
  6443          .then(function() {
  6444            var focusable = $mdUtil.findFocusTarget(element) || angular.element(
  6445              element[0].querySelector('button') ||
  6446              element[0].querySelector('a') ||
  6447              element[0].querySelector('[ng-click]')
  6448            ) || backdrop;
  6449  
  6450            if (options.escapeToClose) {
  6451              options.rootElementKeyupCallback = function(e) {
  6452                if (e.keyCode === $mdConstant.KEY_CODE.ESCAPE) {
  6453                  $mdUtil.nextTick($mdBottomSheet.cancel,true);
  6454                }
  6455              };
  6456  
  6457              $rootElement.on('keyup', options.rootElementKeyupCallback);
  6458              focusable && focusable.focus();
  6459            }
  6460          });
  6461  
  6462      }
  6463  
  6464      function onRemove(scope, element, options) {
  6465  
  6466        var bottomSheet = options.bottomSheet;
  6467  
  6468        if (!options.disableBackdrop) $animate.leave(backdrop);
  6469        return $animate.leave(bottomSheet.element).then(function() {
  6470          if (options.disableParentScroll) {
  6471            options.restoreScroll();
  6472            delete options.restoreScroll;
  6473          }
  6474  
  6475          bottomSheet.cleanup();
  6476        });
  6477      }
  6478  
  6479      /**
  6480       * BottomSheet class to apply bottom-sheet behavior to an element
  6481       */
  6482      function BottomSheet(element, parent) {
  6483        var deregister = $mdGesture.register(parent, 'drag', { horizontal: false });
  6484        parent.on('$md.dragstart', onDragStart)
  6485          .on('$md.drag', onDrag)
  6486          .on('$md.dragend', onDragEnd);
  6487  
  6488        return {
  6489          element: element,
  6490          cleanup: function cleanup() {
  6491            deregister();
  6492            parent.off('$md.dragstart', onDragStart);
  6493            parent.off('$md.drag', onDrag);
  6494            parent.off('$md.dragend', onDragEnd);
  6495          }
  6496        };
  6497  
  6498        function onDragStart(ev) {
  6499          // Disable transitions on transform so that it feels fast
  6500          element.css($mdConstant.CSS.TRANSITION_DURATION, '0ms');
  6501        }
  6502  
  6503        function onDrag(ev) {
  6504          var transform = ev.pointer.distanceY;
  6505          if (transform < 5) {
  6506            // Slow down drag when trying to drag up, and stop after PADDING
  6507            transform = Math.max(-PADDING, transform / 2);
  6508          }
  6509          element.css($mdConstant.CSS.TRANSFORM, 'translate3d(0,' + (PADDING + transform) + 'px,0)');
  6510        }
  6511  
  6512        function onDragEnd(ev) {
  6513          if (ev.pointer.distanceY > 0 &&
  6514              (ev.pointer.distanceY > 20 || Math.abs(ev.pointer.velocityY) > CLOSING_VELOCITY)) {
  6515            var distanceRemaining = element.prop('offsetHeight') - ev.pointer.distanceY;
  6516            var transitionDuration = Math.min(distanceRemaining / ev.pointer.velocityY * 0.75, 500);
  6517            element.css($mdConstant.CSS.TRANSITION_DURATION, transitionDuration + 'ms');
  6518            $mdUtil.nextTick($mdBottomSheet.cancel,true);
  6519          } else {
  6520            element.css($mdConstant.CSS.TRANSITION_DURATION, '');
  6521            element.css($mdConstant.CSS.TRANSFORM, '');
  6522          }
  6523        }
  6524      }
  6525  
  6526    }
  6527  
  6528  }
  6529  MdBottomSheetProvider.$inject = ["$$interimElementProvider"];
  6530  
  6531  })();
  6532  (function(){
  6533  "use strict";
  6534  
  6535  /**
  6536   * @ngdoc module
  6537   * @name material.components.card
  6538   *
  6539   * @description
  6540   * Card components.
  6541   */
  6542  angular.module('material.components.card', [
  6543      'material.core'
  6544    ])
  6545    .directive('mdCard', mdCardDirective);
  6546  
  6547  
  6548  /**
  6549   * @ngdoc directive
  6550   * @name mdCard
  6551   * @module material.components.card
  6552   *
  6553   * @restrict E
  6554   *
  6555   * @description
  6556   * The `<md-card>` directive is a container element used within `<md-content>` containers.
  6557   *
  6558   * An image included as a direct descendant will fill the card's width, while the `<md-card-content>`
  6559   * container will wrap text content and provide padding. An `<md-card-footer>` element can be
  6560   * optionally included to put content flush against the bottom edge of the card.
  6561   *
  6562   * Action buttons can be included in an `<md-card-actions>` element, similar to `<md-dialog-actions>`.
  6563   * You can then position buttons using layout attributes.
  6564   *
  6565   * Card is built with:
  6566   * * `<md-card-header>` - Header for the card, holds avatar, text and squared image
  6567   *  - `<md-card-avatar>` - Card avatar
  6568   *    - `md-user-avatar` - Class for user image
  6569   *    - `<md-icon>`
  6570   *  - `<md-card-header-text>` - Contains elements for the card description
  6571   *    - `md-title` - Class for the card title
  6572   *    - `md-subhead` - Class for the card sub header
  6573   * * `<img>` - Image for the card
  6574   * * `<md-card-title>` - Card content title
  6575   *  - `<md-card-title-text>`
  6576   *    - `md-headline` - Class for the card content title
  6577   *    - `md-subhead` - Class for the card content sub header
  6578   *  - `<md-card-title-media>` - Squared image within the title
  6579   *    - `md-media-sm` - Class for small image
  6580   *    - `md-media-md` - Class for medium image
  6581   *    - `md-media-lg` - Class for large image
  6582   * * `<md-card-content>` - Card content
  6583   *  - `md-media-xl` - Class for extra large image
  6584   * * `<md-card-actions>` - Card actions
  6585   *  - `<md-card-icon-actions>` - Icon actions
  6586   *
  6587   * Cards have constant width and variable heights; where the maximum height is limited to what can
  6588   * fit within a single view on a platform, but it can temporarily expand as needed.
  6589   *
  6590   * @usage
  6591   * ### Card with optional footer
  6592   * <hljs lang="html">
  6593   * <md-card>
  6594   *  <img src="card-image.png" class="md-card-image" alt="image caption">
  6595   *  <md-card-content>
  6596   *    <h2>Card headline</h2>
  6597   *    <p>Card content</p>
  6598   *  </md-card-content>
  6599   *  <md-card-footer>
  6600   *    Card footer
  6601   *  </md-card-footer>
  6602   * </md-card>
  6603   * </hljs>
  6604   *
  6605   * ### Card with actions
  6606   * <hljs lang="html">
  6607   * <md-card>
  6608   *  <img src="card-image.png" class="md-card-image" alt="image caption">
  6609   *  <md-card-content>
  6610   *    <h2>Card headline</h2>
  6611   *    <p>Card content</p>
  6612   *  </md-card-content>
  6613   *  <md-card-actions layout="row" layout-align="end center">
  6614   *    <md-button>Action 1</md-button>
  6615   *    <md-button>Action 2</md-button>
  6616   *  </md-card-actions>
  6617   * </md-card>
  6618   * </hljs>
  6619   *
  6620   * ### Card with header, image, title actions and content
  6621   * <hljs lang="html">
  6622   * <md-card>
  6623   *   <md-card-header>
  6624   *     <md-card-avatar>
  6625   *       <img class="md-user-avatar" src="avatar.png"/>
  6626   *     </md-card-avatar>
  6627   *     <md-card-header-text>
  6628   *       <span class="md-title">Title</span>
  6629   *       <span class="md-subhead">Sub header</span>
  6630   *     </md-card-header-text>
  6631   *   </md-card-header>
  6632   *   <img ng-src="card-image.png" class="md-card-image" alt="image caption">
  6633   *   <md-card-title>
  6634   *     <md-card-title-text>
  6635   *       <span class="md-headline">Card headline</span>
  6636   *       <span class="md-subhead">Card subheader</span>
  6637   *     </md-card-title-text>
  6638   *   </md-card-title>
  6639   *   <md-card-actions layout="row" layout-align="start center">
  6640   *     <md-button>Action 1</md-button>
  6641   *     <md-button>Action 2</md-button>
  6642   *     <md-card-icon-actions>
  6643   *       <md-button class="md-icon-button" aria-label="icon">
  6644   *         <md-icon md-svg-icon="icon"></md-icon>
  6645   *       </md-button>
  6646   *     </md-card-icon-actions>
  6647   *   </md-card-actions>
  6648   *   <md-card-content>
  6649   *     <p>
  6650   *      Card content
  6651   *     </p>
  6652   *   </md-card-content>
  6653   * </md-card>
  6654   * </hljs>
  6655   */
  6656  function mdCardDirective($mdTheming) {
  6657    return {
  6658      restrict: 'E',
  6659      link: function ($scope, $element) {
  6660        $mdTheming($element);
  6661      }
  6662    };
  6663  }
  6664  mdCardDirective.$inject = ["$mdTheming"];
  6665  
  6666  })();
  6667  (function(){
  6668  "use strict";
  6669  
  6670  /**
  6671   * @ngdoc module
  6672   * @name material.components.checkbox
  6673   * @description Checkbox module!
  6674   */
  6675  angular
  6676    .module('material.components.checkbox', ['material.core'])
  6677    .directive('mdCheckbox', MdCheckboxDirective);
  6678  
  6679  /**
  6680   * @ngdoc directive
  6681   * @name mdCheckbox
  6682   * @module material.components.checkbox
  6683   * @restrict E
  6684   *
  6685   * @description
  6686   * The checkbox directive is used like the normal [angular checkbox](https://docs.angularjs.org/api/ng/input/input%5Bcheckbox%5D).
  6687   *
  6688   * As per the [material design spec](http://www.google.com/design/spec/style/color.html#color-ui-color-application)
  6689   * the checkbox is in the accent color by default. The primary color palette may be used with
  6690   * the `md-primary` class.
  6691   *
  6692   * @param {string} ng-model Assignable angular expression to data-bind to.
  6693   * @param {string=} name Property name of the form under which the control is published.
  6694   * @param {expression=} ng-true-value The value to which the expression should be set when selected.
  6695   * @param {expression=} ng-false-value The value to which the expression should be set when not selected.
  6696   * @param {string=} ng-change Angular expression to be executed when input changes due to user interaction with the input element.
  6697   * @param {boolean=} md-no-ink Use of attribute indicates use of ripple ink effects
  6698   * @param {string=} aria-label Adds label to checkbox for accessibility.
  6699   * Defaults to checkbox's text. If no default text is found, a warning will be logged.
  6700   *
  6701   * @usage
  6702   * <hljs lang="html">
  6703   * <md-checkbox ng-model="isChecked" aria-label="Finished?">
  6704   *   Finished ?
  6705   * </md-checkbox>
  6706   *
  6707   * <md-checkbox md-no-ink ng-model="hasInk" aria-label="No Ink Effects">
  6708   *   No Ink Effects
  6709   * </md-checkbox>
  6710   *
  6711   * <md-checkbox ng-disabled="true" ng-model="isDisabled" aria-label="Disabled">
  6712   *   Disabled
  6713   * </md-checkbox>
  6714   *
  6715   * </hljs>
  6716   *
  6717   */
  6718  function MdCheckboxDirective(inputDirective, $mdAria, $mdConstant, $mdTheming, $mdUtil, $timeout) {
  6719    inputDirective = inputDirective[0];
  6720    var CHECKED_CSS = 'md-checked';
  6721  
  6722    return {
  6723      restrict: 'E',
  6724      transclude: true,
  6725      require: '?ngModel',
  6726      priority: 210, // Run before ngAria
  6727      template: 
  6728        '<div class="md-container" md-ink-ripple md-ink-ripple-checkbox>' +
  6729          '<div class="md-icon"></div>' +
  6730        '</div>' +
  6731        '<div ng-transclude class="md-label"></div>',
  6732      compile: compile
  6733    };
  6734  
  6735    // **********************************************************
  6736    // Private Methods
  6737    // **********************************************************
  6738  
  6739    function compile (tElement, tAttrs) {
  6740      var container = tElement.children();
  6741  
  6742      tAttrs.type = 'checkbox';
  6743      tAttrs.tabindex = tAttrs.tabindex || '0';
  6744      tElement.attr('role', tAttrs.type);
  6745  
  6746      // Attach a click handler in compile in order to immediately stop propagation
  6747      // (especially for ng-click) when the checkbox is disabled.
  6748      tElement.on('click', function(event) {
  6749        if (this.hasAttribute('disabled')) {
  6750          event.stopImmediatePropagation();
  6751        }
  6752      });
  6753  
  6754      // Redirect focus events to the root element, because IE11 is always focusing the container element instead
  6755      // of the md-checkbox element. This causes issues when using ngModelOptions: `updateOnBlur`
  6756      container.on('focus', function() {
  6757        tElement.focus();
  6758      });
  6759  
  6760      return function postLink(scope, element, attr, ngModelCtrl) {
  6761        ngModelCtrl = ngModelCtrl || $mdUtil.fakeNgModel();
  6762        $mdTheming(element);
  6763  
  6764        if (attr.ngChecked) {
  6765          scope.$watch(
  6766              scope.$eval.bind(scope, attr.ngChecked),
  6767              ngModelCtrl.$setViewValue.bind(ngModelCtrl)
  6768          );
  6769        }
  6770  
  6771        $$watchExpr('ngDisabled', 'tabindex', {
  6772          true: '-1',
  6773          false: attr.tabindex
  6774        });
  6775  
  6776        $mdAria.expectWithText(element, 'aria-label');
  6777  
  6778        // Reuse the original input[type=checkbox] directive from Angular core.
  6779        // This is a bit hacky as we need our own event listener and own render
  6780        // function.
  6781        inputDirective.link.pre(scope, {
  6782          on: angular.noop,
  6783          0: {}
  6784        }, attr, [ngModelCtrl]);
  6785  
  6786        scope.mouseActive = false;
  6787        element.on('click', listener)
  6788          .on('keypress', keypressHandler)
  6789          .on('mousedown', function() {
  6790            scope.mouseActive = true;
  6791            $timeout(function() {
  6792              scope.mouseActive = false;
  6793            }, 100);
  6794          })
  6795          .on('focus', function() {
  6796            if (scope.mouseActive === false) {
  6797              element.addClass('md-focused');
  6798            }
  6799          })
  6800          .on('blur', function() {
  6801            element.removeClass('md-focused');
  6802          });
  6803  
  6804        ngModelCtrl.$render = render;
  6805  
  6806        function $$watchExpr(expr, htmlAttr, valueOpts) {
  6807          if (attr[expr]) {
  6808            scope.$watch(attr[expr], function(val) {
  6809              if (valueOpts[val]) {
  6810                element.attr(htmlAttr, valueOpts[val]);
  6811              }
  6812            });
  6813          }
  6814        }
  6815  
  6816        function keypressHandler(ev) {
  6817          var keyCode = ev.which || ev.keyCode;
  6818          if (keyCode === $mdConstant.KEY_CODE.SPACE || keyCode === $mdConstant.KEY_CODE.ENTER) {
  6819            ev.preventDefault();
  6820  
  6821            if (!element.hasClass('md-focused')) {
  6822              element.addClass('md-focused');
  6823            }
  6824  
  6825            listener(ev);
  6826          }
  6827        }
  6828        function listener(ev) {
  6829          if (element[0].hasAttribute('disabled')) {
  6830            return;
  6831          }
  6832  
  6833          scope.$apply(function() {
  6834            // Toggle the checkbox value...
  6835            var viewValue = attr.ngChecked ? attr.checked : !ngModelCtrl.$viewValue;
  6836  
  6837            ngModelCtrl.$setViewValue( viewValue, ev && ev.type);
  6838            ngModelCtrl.$render();
  6839          });
  6840        }
  6841  
  6842        function render() {
  6843          if(ngModelCtrl.$viewValue) {
  6844            element.addClass(CHECKED_CSS);
  6845          } else {
  6846            element.removeClass(CHECKED_CSS);
  6847          }
  6848        }
  6849      };
  6850    }
  6851  }
  6852  MdCheckboxDirective.$inject = ["inputDirective", "$mdAria", "$mdConstant", "$mdTheming", "$mdUtil", "$timeout"];
  6853  
  6854  })();
  6855  (function(){
  6856  "use strict";
  6857  
  6858  /**
  6859   * @ngdoc module
  6860   * @name material.components.content
  6861   *
  6862   * @description
  6863   * Scrollable content
  6864   */
  6865  angular.module('material.components.content', [
  6866    'material.core'
  6867  ])
  6868    .directive('mdContent', mdContentDirective);
  6869  
  6870  /**
  6871   * @ngdoc directive
  6872   * @name mdContent
  6873   * @module material.components.content
  6874   *
  6875   * @restrict E
  6876   *
  6877   * @description
  6878   * The `<md-content>` directive is a container element useful for scrollable content
  6879   *
  6880   * @usage
  6881   *
  6882   * - Add the `[layout-padding]` attribute to make the content padded.
  6883   *
  6884   * <hljs lang="html">
  6885   *  <md-content layout-padding>
  6886   *      Lorem ipsum dolor sit amet, ne quod novum mei.
  6887   *  </md-content>
  6888   * </hljs>
  6889   *
  6890   */
  6891  
  6892  function mdContentDirective($mdTheming) {
  6893    return {
  6894      restrict: 'E',
  6895      controller: ['$scope', '$element', ContentController],
  6896      link: function(scope, element, attr) {
  6897        var node = element[0];
  6898  
  6899        $mdTheming(element);
  6900        scope.$broadcast('$mdContentLoaded', element);
  6901  
  6902        iosScrollFix(element[0]);
  6903      }
  6904    };
  6905  
  6906    function ContentController($scope, $element) {
  6907      this.$scope = $scope;
  6908      this.$element = $element;
  6909    }
  6910  }
  6911  mdContentDirective.$inject = ["$mdTheming"];
  6912  
  6913  function iosScrollFix(node) {
  6914    // IOS FIX:
  6915    // If we scroll where there is no more room for the webview to scroll,
  6916    // by default the webview itself will scroll up and down, this looks really
  6917    // bad.  So if we are scrolling to the very top or bottom, add/subtract one
  6918    angular.element(node).on('$md.pressdown', function(ev) {
  6919      // Only touch events
  6920      if (ev.pointer.type !== 't') return;
  6921      // Don't let a child content's touchstart ruin it for us.
  6922      if (ev.$materialScrollFixed) return;
  6923      ev.$materialScrollFixed = true;
  6924  
  6925      if (node.scrollTop === 0) {
  6926        node.scrollTop = 1;
  6927      } else if (node.scrollHeight === node.scrollTop + node.offsetHeight) {
  6928        node.scrollTop -= 1;
  6929      }
  6930    });
  6931  }
  6932  
  6933  })();
  6934  (function(){
  6935  "use strict";
  6936  
  6937  /**
  6938   * @ngdoc module
  6939   * @name material.components.chips
  6940   */
  6941  /*
  6942   * @see js folder for chips implementation
  6943   */
  6944  angular.module('material.components.chips', [
  6945    'material.core',
  6946    'material.components.autocomplete'
  6947  ]);
  6948  
  6949  })();
  6950  (function(){
  6951  "use strict";
  6952  
  6953  (function() {
  6954    'use strict';
  6955  
  6956    /**
  6957     * @ngdoc module
  6958     * @name material.components.datepicker
  6959     * @description Datepicker
  6960     */
  6961    angular.module('material.components.datepicker', [
  6962      'material.core',
  6963      'material.components.icon',
  6964      'material.components.virtualRepeat'
  6965    ]).directive('mdCalendar', calendarDirective);
  6966  
  6967  
  6968    // POST RELEASE
  6969    // TODO(jelbourn): Mac Cmd + left / right == Home / End
  6970    // TODO(jelbourn): Clicking on the month label opens the month-picker.
  6971    // TODO(jelbourn): Minimum and maximum date
  6972    // TODO(jelbourn): Refactor month element creation to use cloneNode (performance).
  6973    // TODO(jelbourn): Define virtual scrolling constants (compactness) users can override.
  6974    // TODO(jelbourn): Animated month transition on ng-model change (virtual-repeat)
  6975    // TODO(jelbourn): Scroll snapping (virtual repeat)
  6976    // TODO(jelbourn): Remove superfluous row from short months (virtual-repeat)
  6977    // TODO(jelbourn): Month headers stick to top when scrolling.
  6978    // TODO(jelbourn): Previous month opacity is lowered when partially scrolled out of view.
  6979    // TODO(jelbourn): Support md-calendar standalone on a page (as a tabstop w/ aria-live
  6980    //     announcement and key handling).
  6981    // Read-only calendar (not just date-picker).
  6982  
  6983    /**
  6984     * Height of one calendar month tbody. This must be made known to the virtual-repeat and is
  6985     * subsequently used for scrolling to specific months.
  6986     */
  6987    var TBODY_HEIGHT = 265;
  6988  
  6989    /**
  6990     * Height of a calendar month with a single row. This is needed to calculate the offset for
  6991     * rendering an extra month in virtual-repeat that only contains one row.
  6992     */
  6993    var TBODY_SINGLE_ROW_HEIGHT = 45;
  6994  
  6995    function calendarDirective() {
  6996      return {
  6997        template:
  6998            '<table aria-hidden="true" class="md-calendar-day-header"><thead></thead></table>' +
  6999            '<div class="md-calendar-scroll-mask">' +
  7000            '<md-virtual-repeat-container class="md-calendar-scroll-container" ' +
  7001                  'md-offset-size="' + (TBODY_SINGLE_ROW_HEIGHT - TBODY_HEIGHT) + '">' +
  7002                '<table role="grid" tabindex="0" class="md-calendar" aria-readonly="true">' +
  7003                  '<tbody role="rowgroup" md-virtual-repeat="i in ctrl.items" md-calendar-month ' +
  7004                      'md-month-offset="$index" class="md-calendar-month" ' +
  7005                      'md-start-index="ctrl.getSelectedMonthIndex()" ' +
  7006                      'md-item-size="' + TBODY_HEIGHT + '"></tbody>' +
  7007                '</table>' +
  7008              '</md-virtual-repeat-container>' +
  7009            '</div>',
  7010        scope: {
  7011          minDate: '=mdMinDate',
  7012          maxDate: '=mdMaxDate',
  7013          dateFilter: '=mdDateFilter',
  7014        },
  7015        require: ['ngModel', 'mdCalendar'],
  7016        controller: CalendarCtrl,
  7017        controllerAs: 'ctrl',
  7018        bindToController: true,
  7019        link: function(scope, element, attrs, controllers) {
  7020          var ngModelCtrl = controllers[0];
  7021          var mdCalendarCtrl = controllers[1];
  7022          mdCalendarCtrl.configureNgModel(ngModelCtrl);
  7023        }
  7024      };
  7025    }
  7026  
  7027    /** Class applied to the selected date cell/. */
  7028    var SELECTED_DATE_CLASS = 'md-calendar-selected-date';
  7029  
  7030    /** Class applied to the focused date cell/. */
  7031    var FOCUSED_DATE_CLASS = 'md-focus';
  7032  
  7033    /** Next identifier for calendar instance. */
  7034    var nextUniqueId = 0;
  7035  
  7036    /** The first renderable date in the virtual-scrolling calendar (for all instances). */
  7037    var firstRenderableDate = null;
  7038  
  7039    /**
  7040     * Controller for the mdCalendar component.
  7041     * @ngInject @constructor
  7042     */
  7043    function CalendarCtrl($element, $attrs, $scope, $animate, $q, $mdConstant,
  7044        $mdTheming, $$mdDateUtil, $mdDateLocale, $mdInkRipple, $mdUtil) {
  7045      $mdTheming($element);
  7046      /**
  7047       * Dummy array-like object for virtual-repeat to iterate over. The length is the total
  7048       * number of months that can be viewed. This is shorter than ideal because of (potential)
  7049       * Firefox bug https://bugzilla.mozilla.org/show_bug.cgi?id=1181658.
  7050       */
  7051      this.items = {length: 2000};
  7052  
  7053      if (this.maxDate && this.minDate) {
  7054        // Limit the number of months if min and max dates are set.
  7055        var numMonths = $$mdDateUtil.getMonthDistance(this.minDate, this.maxDate) + 1;
  7056        numMonths = Math.max(numMonths, 1);
  7057        // Add an additional month as the final dummy month for rendering purposes.
  7058        numMonths += 1;
  7059        this.items.length = numMonths;
  7060      }
  7061  
  7062      /** @final {!angular.$animate} */
  7063      this.$animate = $animate;
  7064  
  7065      /** @final {!angular.$q} */
  7066      this.$q = $q;
  7067  
  7068      /** @final */
  7069      this.$mdInkRipple = $mdInkRipple;
  7070  
  7071      /** @final */
  7072      this.$mdUtil = $mdUtil;
  7073  
  7074      /** @final */
  7075      this.keyCode = $mdConstant.KEY_CODE;
  7076  
  7077      /** @final */
  7078      this.dateUtil = $$mdDateUtil;
  7079  
  7080      /** @final */
  7081      this.dateLocale = $mdDateLocale;
  7082  
  7083      /** @final {!angular.JQLite} */
  7084      this.$element = $element;
  7085  
  7086      /** @final {!angular.Scope} */
  7087      this.$scope = $scope;
  7088  
  7089      /** @final {HTMLElement} */
  7090      this.calendarElement = $element[0].querySelector('.md-calendar');
  7091  
  7092      /** @final {HTMLElement} */
  7093      this.calendarScroller = $element[0].querySelector('.md-virtual-repeat-scroller');
  7094  
  7095      /** @final {Date} */
  7096      this.today = this.dateUtil.createDateAtMidnight();
  7097  
  7098      /** @type {Date} */
  7099      this.firstRenderableDate = this.dateUtil.incrementMonths(this.today, -this.items.length / 2);
  7100  
  7101      if (this.minDate && this.minDate > this.firstRenderableDate) {
  7102        this.firstRenderableDate = this.minDate;
  7103      } else if (this.maxDate) {
  7104        // Calculate the difference between the start date and max date.
  7105        // Subtract 1 because it's an inclusive difference and 1 for the final dummy month.
  7106        //
  7107        var monthDifference = this.items.length - 2;
  7108        this.firstRenderableDate = this.dateUtil.incrementMonths(this.maxDate, -(this.items.length - 2));
  7109      }
  7110  
  7111  
  7112      /** @final {number} Unique ID for this calendar instance. */
  7113      this.id = nextUniqueId++;
  7114  
  7115      /** @type {!angular.NgModelController} */
  7116      this.ngModelCtrl = null;
  7117  
  7118      /**
  7119       * The selected date. Keep track of this separately from the ng-model value so that we
  7120       * can know, when the ng-model value changes, what the previous value was before it's updated
  7121       * in the component's UI.
  7122       *
  7123       * @type {Date}
  7124       */
  7125      this.selectedDate = null;
  7126  
  7127      /**
  7128       * The date that is currently focused or showing in the calendar. This will initially be set
  7129       * to the ng-model value if set, otherwise to today. It will be updated as the user navigates
  7130       * to other months. The cell corresponding to the displayDate does not necessarily always have
  7131       * focus in the document (such as for cases when the user is scrolling the calendar).
  7132       * @type {Date}
  7133       */
  7134      this.displayDate = null;
  7135  
  7136      /**
  7137       * The date that has or should have focus.
  7138       * @type {Date}
  7139       */
  7140      this.focusDate = null;
  7141  
  7142      /** @type {boolean} */
  7143      this.isInitialized = false;
  7144  
  7145      /** @type {boolean} */
  7146      this.isMonthTransitionInProgress = false;
  7147  
  7148      // Unless the user specifies so, the calendar should not be a tab stop.
  7149      // This is necessary because ngAria might add a tabindex to anything with an ng-model
  7150      // (based on whether or not the user has turned that particular feature on/off).
  7151      if (!$attrs['tabindex']) {
  7152        $element.attr('tabindex', '-1');
  7153      }
  7154  
  7155      var self = this;
  7156  
  7157      /**
  7158       * Handles a click event on a date cell.
  7159       * Created here so that every cell can use the same function instance.
  7160       * @this {HTMLTableCellElement} The cell that was clicked.
  7161       */
  7162      this.cellClickHandler = function() {
  7163        var cellElement = this;
  7164        if (this.hasAttribute('data-timestamp')) {
  7165          $scope.$apply(function() {
  7166            var timestamp = Number(cellElement.getAttribute('data-timestamp'));
  7167            self.setNgModelValue(self.dateUtil.createDateAtMidnight(timestamp));
  7168          });
  7169        }
  7170      };
  7171  
  7172      this.attachCalendarEventListeners();
  7173    }
  7174    CalendarCtrl.$inject = ["$element", "$attrs", "$scope", "$animate", "$q", "$mdConstant", "$mdTheming", "$$mdDateUtil", "$mdDateLocale", "$mdInkRipple", "$mdUtil"];
  7175  
  7176  
  7177    /*** Initialization ***/
  7178  
  7179    /**
  7180     * Sets up the controller's reference to ngModelController.
  7181     * @param {!angular.NgModelController} ngModelCtrl
  7182     */
  7183    CalendarCtrl.prototype.configureNgModel = function(ngModelCtrl) {
  7184      this.ngModelCtrl = ngModelCtrl;
  7185  
  7186      var self = this;
  7187      ngModelCtrl.$render = function() {
  7188        self.changeSelectedDate(self.ngModelCtrl.$viewValue);
  7189      };
  7190    };
  7191  
  7192    /**
  7193     * Initialize the calendar by building the months that are initially visible.
  7194     * Initialization should occur after the ngModel value is known.
  7195     */
  7196    CalendarCtrl.prototype.buildInitialCalendarDisplay = function() {
  7197      this.buildWeekHeader();
  7198      this.hideVerticalScrollbar();
  7199  
  7200      this.displayDate = this.selectedDate || this.today;
  7201      this.isInitialized = true;
  7202    };
  7203  
  7204    /**
  7205     * Hides the vertical scrollbar on the calendar scroller by setting the width on the
  7206     * calendar scroller and the `overflow: hidden` wrapper around the scroller, and then setting
  7207     * a padding-right on the scroller equal to the width of the browser's scrollbar.
  7208     *
  7209     * This will cause a reflow.
  7210     */
  7211    CalendarCtrl.prototype.hideVerticalScrollbar = function() {
  7212      var element = this.$element[0];
  7213  
  7214      var scrollMask = element.querySelector('.md-calendar-scroll-mask');
  7215      var scroller = this.calendarScroller;
  7216  
  7217      var headerWidth = element.querySelector('.md-calendar-day-header').clientWidth;
  7218      var scrollbarWidth = scroller.offsetWidth - scroller.clientWidth;
  7219  
  7220      scrollMask.style.width = headerWidth + 'px';
  7221      scroller.style.width = (headerWidth + scrollbarWidth) + 'px';
  7222      scroller.style.paddingRight = scrollbarWidth + 'px';
  7223    };
  7224  
  7225  
  7226    /** Attach event listeners for the calendar. */
  7227    CalendarCtrl.prototype.attachCalendarEventListeners = function() {
  7228      // Keyboard interaction.
  7229      this.$element.on('keydown', angular.bind(this, this.handleKeyEvent));
  7230    };
  7231  
  7232    /*** User input handling ***/
  7233  
  7234    /**
  7235     * Handles a key event in the calendar with the appropriate action. The action will either
  7236     * be to select the focused date or to navigate to focus a new date.
  7237     * @param {KeyboardEvent} event
  7238     */
  7239    CalendarCtrl.prototype.handleKeyEvent = function(event) {
  7240      var self = this;
  7241      this.$scope.$apply(function() {
  7242        // Capture escape and emit back up so that a wrapping component
  7243        // (such as a date-picker) can decide to close.
  7244        if (event.which == self.keyCode.ESCAPE || event.which == self.keyCode.TAB) {
  7245          self.$scope.$emit('md-calendar-close');
  7246  
  7247          if (event.which == self.keyCode.TAB) {
  7248            event.preventDefault();
  7249          }
  7250  
  7251          return;
  7252        }
  7253  
  7254        // Remaining key events fall into two categories: selection and navigation.
  7255        // Start by checking if this is a selection event.
  7256        if (event.which === self.keyCode.ENTER) {
  7257          self.setNgModelValue(self.displayDate);
  7258          event.preventDefault();
  7259          return;
  7260        }
  7261  
  7262        // Selection isn't occurring, so the key event is either navigation or nothing.
  7263        var date = self.getFocusDateFromKeyEvent(event);
  7264        if (date) {
  7265          date = self.boundDateByMinAndMax(date);
  7266          event.preventDefault();
  7267          event.stopPropagation();
  7268  
  7269          // Since this is a keyboard interaction, actually give the newly focused date keyboard
  7270          // focus after the been brought into view.
  7271          self.changeDisplayDate(date).then(function () {
  7272            self.focus(date);
  7273          });
  7274        }
  7275      });
  7276    };
  7277  
  7278    /**
  7279     * Gets the date to focus as the result of a key event.
  7280     * @param {KeyboardEvent} event
  7281     * @returns {Date} Date to navigate to, or null if the key does not match a calendar shortcut.
  7282     */
  7283    CalendarCtrl.prototype.getFocusDateFromKeyEvent = function(event) {
  7284      var dateUtil = this.dateUtil;
  7285      var keyCode = this.keyCode;
  7286  
  7287      switch (event.which) {
  7288        case keyCode.RIGHT_ARROW: return dateUtil.incrementDays(this.displayDate, 1);
  7289        case keyCode.LEFT_ARROW: return dateUtil.incrementDays(this.displayDate, -1);
  7290        case keyCode.DOWN_ARROW:
  7291          return event.metaKey ?
  7292            dateUtil.incrementMonths(this.displayDate, 1) :
  7293            dateUtil.incrementDays(this.displayDate, 7);
  7294        case keyCode.UP_ARROW:
  7295          return event.metaKey ?
  7296            dateUtil.incrementMonths(this.displayDate, -1) :
  7297            dateUtil.incrementDays(this.displayDate, -7);
  7298        case keyCode.PAGE_DOWN: return dateUtil.incrementMonths(this.displayDate, 1);
  7299        case keyCode.PAGE_UP: return dateUtil.incrementMonths(this.displayDate, -1);
  7300        case keyCode.HOME: return dateUtil.getFirstDateOfMonth(this.displayDate);
  7301        case keyCode.END: return dateUtil.getLastDateOfMonth(this.displayDate);
  7302        default: return null;
  7303      }
  7304    };
  7305  
  7306    /**
  7307     * Gets the "index" of the currently selected date as it would be in the virtual-repeat.
  7308     * @returns {number}
  7309     */
  7310    CalendarCtrl.prototype.getSelectedMonthIndex = function() {
  7311      return this.dateUtil.getMonthDistance(this.firstRenderableDate,
  7312          this.selectedDate || this.today);
  7313    };
  7314  
  7315    /**
  7316     * Scrolls to the month of the given date.
  7317     * @param {Date} date
  7318     */
  7319    CalendarCtrl.prototype.scrollToMonth = function(date) {
  7320      if (!this.dateUtil.isValidDate(date)) {
  7321        return;
  7322      }
  7323  
  7324      var monthDistance = this.dateUtil.getMonthDistance(this.firstRenderableDate, date);
  7325      this.calendarScroller.scrollTop = monthDistance * TBODY_HEIGHT;
  7326    };
  7327  
  7328    /**
  7329     * Sets the ng-model value for the calendar and emits a change event.
  7330     * @param {Date} date
  7331     */
  7332    CalendarCtrl.prototype.setNgModelValue = function(date) {
  7333      this.$scope.$emit('md-calendar-change', date);
  7334      this.ngModelCtrl.$setViewValue(date);
  7335      this.ngModelCtrl.$render();
  7336    };
  7337  
  7338    /**
  7339     * Focus the cell corresponding to the given date.
  7340     * @param {Date=} opt_date
  7341     */
  7342    CalendarCtrl.prototype.focus = function(opt_date) {
  7343      var date = opt_date || this.selectedDate || this.today;
  7344  
  7345      var previousFocus = this.calendarElement.querySelector('.md-focus');
  7346      if (previousFocus) {
  7347        previousFocus.classList.remove(FOCUSED_DATE_CLASS);
  7348      }
  7349  
  7350      var cellId = this.getDateId(date);
  7351      var cell = document.getElementById(cellId);
  7352      if (cell) {
  7353        cell.classList.add(FOCUSED_DATE_CLASS);
  7354        cell.focus();
  7355      } else {
  7356        this.focusDate = date;
  7357      }
  7358    };
  7359  
  7360    /**
  7361     * If a date exceeds minDate or maxDate, returns date matching minDate or maxDate, respectively.
  7362     * Otherwise, returns the date.
  7363     * @param {Date} date
  7364     * @return {Date}
  7365     */
  7366    CalendarCtrl.prototype.boundDateByMinAndMax = function(date) {
  7367      var boundDate = date;
  7368      if (this.minDate && date < this.minDate) {
  7369        boundDate = new Date(this.minDate.getTime());
  7370      }
  7371      if (this.maxDate && date > this.maxDate) {
  7372        boundDate = new Date(this.maxDate.getTime());
  7373      }
  7374      return boundDate;
  7375    };
  7376  
  7377    /*** Updating the displayed / selected date ***/
  7378  
  7379    /**
  7380     * Change the selected date in the calendar (ngModel value has already been changed).
  7381     * @param {Date} date
  7382     */
  7383    CalendarCtrl.prototype.changeSelectedDate = function(date) {
  7384      var self = this;
  7385      var previousSelectedDate = this.selectedDate;
  7386      this.selectedDate = date;
  7387      this.changeDisplayDate(date).then(function() {
  7388  
  7389        // Remove the selected class from the previously selected date, if any.
  7390        if (previousSelectedDate) {
  7391          var prevDateCell =
  7392              document.getElementById(self.getDateId(previousSelectedDate));
  7393          if (prevDateCell) {
  7394            prevDateCell.classList.remove(SELECTED_DATE_CLASS);
  7395            prevDateCell.setAttribute('aria-selected', 'false');
  7396          }
  7397        }
  7398  
  7399        // Apply the select class to the new selected date if it is set.
  7400        if (date) {
  7401          var dateCell = document.getElementById(self.getDateId(date));
  7402          if (dateCell) {
  7403            dateCell.classList.add(SELECTED_DATE_CLASS);
  7404            dateCell.setAttribute('aria-selected', 'true');
  7405          }
  7406        }
  7407      });
  7408    };
  7409  
  7410  
  7411    /**
  7412     * Change the date that is being shown in the calendar. If the given date is in a different
  7413     * month, the displayed month will be transitioned.
  7414     * @param {Date} date
  7415     */
  7416    CalendarCtrl.prototype.changeDisplayDate = function(date) {
  7417      // Initialization is deferred until this function is called because we want to reflect
  7418      // the starting value of ngModel.
  7419      if (!this.isInitialized) {
  7420        this.buildInitialCalendarDisplay();
  7421        return this.$q.when();
  7422      }
  7423  
  7424      // If trying to show an invalid date or a transition is in progress, do nothing.
  7425      if (!this.dateUtil.isValidDate(date) || this.isMonthTransitionInProgress) {
  7426        return this.$q.when();
  7427      }
  7428  
  7429      this.isMonthTransitionInProgress = true;
  7430      var animationPromise = this.animateDateChange(date);
  7431  
  7432      this.displayDate = date;
  7433  
  7434      var self = this;
  7435      animationPromise.then(function() {
  7436        self.isMonthTransitionInProgress = false;
  7437      });
  7438  
  7439      return animationPromise;
  7440    };
  7441  
  7442    /**
  7443     * Animates the transition from the calendar's current month to the given month.
  7444     * @param {Date} date
  7445     * @returns {angular.$q.Promise} The animation promise.
  7446     */
  7447    CalendarCtrl.prototype.animateDateChange = function(date) {
  7448      this.scrollToMonth(date);
  7449      return this.$q.when();
  7450    };
  7451  
  7452    /*** Constructing the calendar table ***/
  7453  
  7454    /**
  7455     * Builds and appends a day-of-the-week header to the calendar.
  7456     * This should only need to be called once during initialization.
  7457     */
  7458    CalendarCtrl.prototype.buildWeekHeader = function() {
  7459      var firstDayOfWeek = this.dateLocale.firstDayOfWeek;
  7460      var shortDays = this.dateLocale.shortDays;
  7461  
  7462      var row = document.createElement('tr');
  7463      for (var i = 0; i < 7; i++) {
  7464        var th = document.createElement('th');
  7465        th.textContent = shortDays[(i + firstDayOfWeek) % 7];
  7466        row.appendChild(th);
  7467      }
  7468  
  7469      this.$element.find('thead').append(row);
  7470    };
  7471  
  7472      /**
  7473     * Gets an identifier for a date unique to the calendar instance for internal
  7474     * purposes. Not to be displayed.
  7475     * @param {Date} date
  7476     * @returns {string}
  7477     */
  7478    CalendarCtrl.prototype.getDateId = function(date) {
  7479      return [
  7480        'md',
  7481        this.id,
  7482        date.getFullYear(),
  7483        date.getMonth(),
  7484        date.getDate()
  7485      ].join('-');
  7486    };
  7487  })();
  7488  
  7489  })();
  7490  (function(){
  7491  "use strict";
  7492  
  7493  (function() {
  7494    'use strict';
  7495  
  7496  
  7497    angular.module('material.components.datepicker')
  7498        .directive('mdCalendarMonth', mdCalendarMonthDirective);
  7499  
  7500  
  7501    /**
  7502     * Private directive consumed by md-calendar. Having this directive lets the calender use
  7503     * md-virtual-repeat and also cleanly separates the month DOM construction functions from
  7504     * the rest of the calendar controller logic.
  7505     */
  7506    function mdCalendarMonthDirective() {
  7507      return {
  7508        require: ['^^mdCalendar', 'mdCalendarMonth'],
  7509        scope: {offset: '=mdMonthOffset'},
  7510        controller: CalendarMonthCtrl,
  7511        controllerAs: 'mdMonthCtrl',
  7512        bindToController: true,
  7513        link: function(scope, element, attrs, controllers) {
  7514          var calendarCtrl = controllers[0];
  7515          var monthCtrl = controllers[1];
  7516  
  7517          monthCtrl.calendarCtrl = calendarCtrl;
  7518          monthCtrl.generateContent();
  7519  
  7520          // The virtual-repeat re-uses the same DOM elements, so there are only a limited number
  7521          // of repeated items that are linked, and then those elements have their bindings updataed.
  7522          // Since the months are not generated by bindings, we simply regenerate the entire thing
  7523          // when the binding (offset) changes.
  7524          scope.$watch(function() { return monthCtrl.offset; }, function(offset, oldOffset) {
  7525            if (offset != oldOffset) {
  7526              monthCtrl.generateContent();
  7527            }
  7528          });
  7529        }
  7530      };
  7531    }
  7532  
  7533    /** Class applied to the cell for today. */
  7534    var TODAY_CLASS = 'md-calendar-date-today';
  7535  
  7536    /** Class applied to the selected date cell/. */
  7537    var SELECTED_DATE_CLASS = 'md-calendar-selected-date';
  7538  
  7539    /** Class applied to the focused date cell/. */
  7540    var FOCUSED_DATE_CLASS = 'md-focus';
  7541  
  7542    /**
  7543     * Controller for a single calendar month.
  7544     * @ngInject @constructor
  7545     */
  7546    function CalendarMonthCtrl($element, $$mdDateUtil, $mdDateLocale) {
  7547      this.dateUtil = $$mdDateUtil;
  7548      this.dateLocale = $mdDateLocale;
  7549      this.$element = $element;
  7550      this.calendarCtrl = null;
  7551  
  7552      /**
  7553       * Number of months from the start of the month "items" that the currently rendered month
  7554       * occurs. Set via angular data binding.
  7555       * @type {number}
  7556       */
  7557      this.offset;
  7558  
  7559      /**
  7560       * Date cell to focus after appending the month to the document.
  7561       * @type {HTMLElement}
  7562       */
  7563      this.focusAfterAppend = null;
  7564    }
  7565    CalendarMonthCtrl.$inject = ["$element", "$$mdDateUtil", "$mdDateLocale"];
  7566  
  7567    /** Generate and append the content for this month to the directive element. */
  7568    CalendarMonthCtrl.prototype.generateContent = function() {
  7569      var calendarCtrl = this.calendarCtrl;
  7570      var date = this.dateUtil.incrementMonths(calendarCtrl.firstRenderableDate, this.offset);
  7571  
  7572      this.$element.empty();
  7573      this.$element.append(this.buildCalendarForMonth(date));
  7574  
  7575      if (this.focusAfterAppend) {
  7576        this.focusAfterAppend.classList.add(FOCUSED_DATE_CLASS);
  7577        this.focusAfterAppend.focus();
  7578        this.focusAfterAppend = null;
  7579      }
  7580    };
  7581  
  7582    /**
  7583     * Creates a single cell to contain a date in the calendar with all appropriate
  7584     * attributes and classes added. If a date is given, the cell content will be set
  7585     * based on the date.
  7586     * @param {Date=} opt_date
  7587     * @returns {HTMLElement}
  7588     */
  7589    CalendarMonthCtrl.prototype.buildDateCell = function(opt_date) {
  7590      var calendarCtrl = this.calendarCtrl;
  7591  
  7592      // TODO(jelbourn): cloneNode is likely a faster way of doing this.
  7593      var cell = document.createElement('td');
  7594      cell.tabIndex = -1;
  7595      cell.classList.add('md-calendar-date');
  7596      cell.setAttribute('role', 'gridcell');
  7597  
  7598      if (opt_date) {
  7599        cell.setAttribute('tabindex', '-1');
  7600        cell.setAttribute('aria-label', this.dateLocale.longDateFormatter(opt_date));
  7601        cell.id = calendarCtrl.getDateId(opt_date);
  7602  
  7603        // Use `data-timestamp` attribute because IE10 does not support the `dataset` property.
  7604        cell.setAttribute('data-timestamp', opt_date.getTime());
  7605  
  7606        // TODO(jelourn): Doing these comparisons for class addition during generation might be slow.
  7607        // It may be better to finish the construction and then query the node and add the class.
  7608        if (this.dateUtil.isSameDay(opt_date, calendarCtrl.today)) {
  7609          cell.classList.add(TODAY_CLASS);
  7610        }
  7611  
  7612        if (this.dateUtil.isValidDate(calendarCtrl.selectedDate) &&
  7613            this.dateUtil.isSameDay(opt_date, calendarCtrl.selectedDate)) {
  7614          cell.classList.add(SELECTED_DATE_CLASS);
  7615          cell.setAttribute('aria-selected', 'true');
  7616        }
  7617  
  7618        var cellText = this.dateLocale.dates[opt_date.getDate()];
  7619  
  7620        if (this.isDateEnabled(opt_date)) {
  7621          // Add a indicator for select, hover, and focus states.
  7622          var selectionIndicator = document.createElement('span');
  7623          cell.appendChild(selectionIndicator);
  7624          selectionIndicator.classList.add('md-calendar-date-selection-indicator');
  7625          selectionIndicator.textContent = cellText;
  7626  
  7627          cell.addEventListener('click', calendarCtrl.cellClickHandler);
  7628  
  7629          if (calendarCtrl.focusDate && this.dateUtil.isSameDay(opt_date, calendarCtrl.focusDate)) {
  7630            this.focusAfterAppend = cell;
  7631          }
  7632        } else {
  7633          cell.classList.add('md-calendar-date-disabled');
  7634          cell.textContent = cellText;
  7635        }
  7636      }
  7637  
  7638      return cell;
  7639    };
  7640    
  7641    /**
  7642     * Check whether date is in range and enabled
  7643     * @param {Date=} opt_date
  7644     * @return {boolean} Whether the date is enabled.
  7645     */
  7646    CalendarMonthCtrl.prototype.isDateEnabled = function(opt_date) {
  7647      return this.dateUtil.isDateWithinRange(opt_date, 
  7648            this.calendarCtrl.minDate, this.calendarCtrl.maxDate) && 
  7649            (!angular.isFunction(this.calendarCtrl.dateFilter)
  7650             || this.calendarCtrl.dateFilter(opt_date));
  7651    }
  7652    
  7653    /**
  7654     * Builds a `tr` element for the calendar grid.
  7655     * @param rowNumber The week number within the month.
  7656     * @returns {HTMLElement}
  7657     */
  7658    CalendarMonthCtrl.prototype.buildDateRow = function(rowNumber) {
  7659      var row = document.createElement('tr');
  7660      row.setAttribute('role', 'row');
  7661  
  7662      // Because of an NVDA bug (with Firefox), the row needs an aria-label in order
  7663      // to prevent the entire row being read aloud when the user moves between rows.
  7664      // See http://community.nvda-project.org/ticket/4643.
  7665      row.setAttribute('aria-label', this.dateLocale.weekNumberFormatter(rowNumber));
  7666  
  7667      return row;
  7668    };
  7669  
  7670    /**
  7671     * Builds the <tbody> content for the given date's month.
  7672     * @param {Date=} opt_dateInMonth
  7673     * @returns {DocumentFragment} A document fragment containing the <tr> elements.
  7674     */
  7675    CalendarMonthCtrl.prototype.buildCalendarForMonth = function(opt_dateInMonth) {
  7676      var date = this.dateUtil.isValidDate(opt_dateInMonth) ? opt_dateInMonth : new Date();
  7677  
  7678      var firstDayOfMonth = this.dateUtil.getFirstDateOfMonth(date);
  7679      var firstDayOfTheWeek = this.getLocaleDay_(firstDayOfMonth);
  7680      var numberOfDaysInMonth = this.dateUtil.getNumberOfDaysInMonth(date);
  7681  
  7682      // Store rows for the month in a document fragment so that we can append them all at once.
  7683      var monthBody = document.createDocumentFragment();
  7684  
  7685      var rowNumber = 1;
  7686      var row = this.buildDateRow(rowNumber);
  7687      monthBody.appendChild(row);
  7688  
  7689      // If this is the final month in the list of items, only the first week should render,
  7690      // so we should return immediately after the first row is complete and has been
  7691      // attached to the body.
  7692      var isFinalMonth = this.offset === this.calendarCtrl.items.length - 1;
  7693  
  7694      // Add a label for the month. If the month starts on a Sun/Mon/Tues, the month label
  7695      // goes on a row above the first of the month. Otherwise, the month label takes up the first
  7696      // two cells of the first row.
  7697      var blankCellOffset = 0;
  7698      var monthLabelCell = document.createElement('td');
  7699      monthLabelCell.classList.add('md-calendar-month-label');
  7700      // If the entire month is after the max date, render the label as a disabled state.
  7701      if (this.calendarCtrl.maxDate && firstDayOfMonth > this.calendarCtrl.maxDate) {
  7702        monthLabelCell.classList.add('md-calendar-month-label-disabled');
  7703      }
  7704      monthLabelCell.textContent = this.dateLocale.monthHeaderFormatter(date);
  7705      if (firstDayOfTheWeek <= 2) {
  7706        monthLabelCell.setAttribute('colspan', '7');
  7707  
  7708        var monthLabelRow = this.buildDateRow();
  7709        monthLabelRow.appendChild(monthLabelCell);
  7710        monthBody.insertBefore(monthLabelRow, row);
  7711  
  7712        if (isFinalMonth) {
  7713          return monthBody;
  7714        }
  7715      } else {
  7716        blankCellOffset = 2;
  7717        monthLabelCell.setAttribute('colspan', '2');
  7718        row.appendChild(monthLabelCell);
  7719      }
  7720  
  7721      // Add a blank cell for each day of the week that occurs before the first of the month.
  7722      // For example, if the first day of the month is a Tuesday, add blank cells for Sun and Mon.
  7723      // The blankCellOffset is needed in cases where the first N cells are used by the month label.
  7724      for (var i = blankCellOffset; i < firstDayOfTheWeek; i++) {
  7725        row.appendChild(this.buildDateCell());
  7726      }
  7727  
  7728      // Add a cell for each day of the month, keeping track of the day of the week so that
  7729      // we know when to start a new row.
  7730      var dayOfWeek = firstDayOfTheWeek;
  7731      var iterationDate = firstDayOfMonth;
  7732      for (var d = 1; d <= numberOfDaysInMonth; d++) {
  7733        // If we've reached the end of the week, start a new row.
  7734        if (dayOfWeek === 7) {
  7735          // We've finished the first row, so we're done if this is the final month.
  7736          if (isFinalMonth) {
  7737            return monthBody;
  7738          }
  7739          dayOfWeek = 0;
  7740          rowNumber++;
  7741          row = this.buildDateRow(rowNumber);
  7742          monthBody.appendChild(row);
  7743        }
  7744  
  7745        iterationDate.setDate(d);
  7746        var cell = this.buildDateCell(iterationDate);
  7747        row.appendChild(cell);
  7748  
  7749        dayOfWeek++;
  7750      }
  7751  
  7752      // Ensure that the last row of the month has 7 cells.
  7753      while (row.childNodes.length < 7) {
  7754        row.appendChild(this.buildDateCell());
  7755      }
  7756  
  7757      // Ensure that all months have 6 rows. This is necessary for now because the virtual-repeat
  7758      // requires that all items have exactly the same height.
  7759      while (monthBody.childNodes.length < 6) {
  7760        var whitespaceRow = this.buildDateRow();
  7761        for (var i = 0; i < 7; i++) {
  7762          whitespaceRow.appendChild(this.buildDateCell());
  7763        }
  7764        monthBody.appendChild(whitespaceRow);
  7765      }
  7766  
  7767      return monthBody;
  7768    };
  7769  
  7770    /**
  7771     * Gets the day-of-the-week index for a date for the current locale.
  7772     * @private
  7773     * @param {Date} date
  7774     * @returns {number} The column index of the date in the calendar.
  7775     */
  7776    CalendarMonthCtrl.prototype.getLocaleDay_ = function(date) {
  7777      return (date.getDay() + (7 - this.dateLocale.firstDayOfWeek)) % 7
  7778    };
  7779  })();
  7780  
  7781  })();
  7782  (function(){
  7783  "use strict";
  7784  
  7785  (function() {
  7786    'use strict';
  7787  
  7788    /**
  7789     * @ngdoc service
  7790     * @name $mdDateLocaleProvider
  7791     * @module material.components.datepicker
  7792     *
  7793     * @description
  7794     * The `$mdDateLocaleProvider` is the provider that creates the `$mdDateLocale` service.
  7795     * This provider that allows the user to specify messages, formatters, and parsers for date
  7796     * internationalization. The `$mdDateLocale` service itself is consumed by Angular Material
  7797     * components that deal with dates.
  7798     *
  7799     * @property {(Array<string>)=} months Array of month names (in order).
  7800     * @property {(Array<string>)=} shortMonths Array of abbreviated month names.
  7801     * @property {(Array<string>)=} days Array of the days of the week (in order).
  7802     * @property {(Array<string>)=} shortDays Array of abbreviated dayes of the week.
  7803     * @property {(Array<string>)=} dates Array of dates of the month. Only necessary for locales
  7804     *     using a numeral system other than [1, 2, 3...].
  7805     * @property {(Array<string>)=} firstDayOfWeek The first day of the week. Sunday = 0, Monday = 1,
  7806     *    etc.
  7807     * @property {(function(string): Date)=} parseDate Function to parse a date object from a string.
  7808     * @property {(function(Date): string)=} formatDate Function to format a date object to a string.
  7809     * @property {(function(Date): string)=} monthHeaderFormatter Function that returns the label for
  7810     *     a month given a date.
  7811     * @property {(function(number): string)=} weekNumberFormatter Function that returns a label for
  7812     *     a week given the week number.
  7813     * @property {(string)=} msgCalendar Translation of the label "Calendar" for the current locale.
  7814     * @property {(string)=} msgOpenCalendar Translation of the button label "Open calendar" for the
  7815     *     current locale.
  7816     *
  7817     * @usage
  7818     * <hljs lang="js">
  7819     *   myAppModule.config(function($mdDateLocaleProvider) {
  7820     *
  7821     *     // Example of a French localization.
  7822     *     $mdDateLocaleProvider.months = ['janvier', 'février', 'mars', ...];
  7823     *     $mdDateLocaleProvider.shortMonths = ['janv', 'févr', 'mars', ...];
  7824     *     $mdDateLocaleProvider.days = ['dimanche', 'lundi', 'mardi', ...];
  7825     *     $mdDateLocaleProvider.shortDays = ['Di', 'Lu', 'Ma', ...];
  7826     *
  7827     *     // Can change week display to start on Monday.
  7828     *     $mdDateLocaleProvider.firstDayOfWeek = 1;
  7829     *
  7830     *     // Optional.
  7831     *     $mdDateLocaleProvider.dates = [1, 2, 3, 4, 5, 6, ...];
  7832     *
  7833     *     // Example uses moment.js to parse and format dates.
  7834     *     $mdDateLocaleProvider.parseDate = function(dateString) {
  7835     *       var m = moment(dateString, 'L', true);
  7836     *       return m.isValid() ? m.toDate() : new Date(NaN);
  7837     *     };
  7838     *
  7839     *     $mdDateLocaleProvider.formatDate = function(date) {
  7840     *       return moment(date).format('L');
  7841     *     };
  7842     *
  7843     *     $mdDateLocaleProvider.monthHeaderFormatter = function(date) {
  7844     *       return myShortMonths[date.getMonth()] + ' ' + date.getFullYear();
  7845     *     };
  7846     *
  7847     *     // In addition to date display, date components also need localized messages
  7848     *     // for aria-labels for screen-reader users.
  7849     *
  7850     *     $mdDateLocaleProvider.weekNumberFormatter = function(weekNumber) {
  7851     *       return 'Semaine ' + weekNumber;
  7852     *     };
  7853     *
  7854     *     $mdDateLocaleProvider.msgCalendar = 'Calendrier';
  7855     *     $mdDateLocaleProvider.msgOpenCalendar = 'Ouvrir le calendrier';
  7856     *
  7857     * });
  7858     * </hljs>
  7859     *
  7860     */
  7861  
  7862    angular.module('material.components.datepicker').config(["$provide", function($provide) {
  7863      // TODO(jelbourn): Assert provided values are correctly formatted. Need assertions.
  7864  
  7865      /** @constructor */
  7866      function DateLocaleProvider() {
  7867        /** Array of full month names. E.g., ['January', 'February', ...] */
  7868        this.months = null;
  7869  
  7870        /** Array of abbreviated month names. E.g., ['Jan', 'Feb', ...] */
  7871        this.shortMonths = null;
  7872  
  7873        /** Array of full day of the week names. E.g., ['Monday', 'Tuesday', ...] */
  7874        this.days = null;
  7875  
  7876        /** Array of abbreviated dat of the week names. E.g., ['M', 'T', ...] */
  7877        this.shortDays = null;
  7878  
  7879        /** Array of dates of a month (1 - 31). Characters might be different in some locales. */
  7880        this.dates = null;
  7881  
  7882        /** Index of the first day of the week. 0 = Sunday, 1 = Monday, etc. */
  7883        this.firstDayOfWeek = 0;
  7884  
  7885        /**
  7886         * Function that converts the date portion of a Date to a string.
  7887         * @type {(function(Date): string)}
  7888         */
  7889        this.formatDate = null;
  7890  
  7891        /**
  7892         * Function that converts a date string to a Date object (the date portion)
  7893         * @type {function(string): Date}
  7894         */
  7895        this.parseDate = null;
  7896  
  7897        /**
  7898         * Function that formats a Date into a month header string.
  7899         * @type {function(Date): string}
  7900         */
  7901        this.monthHeaderFormatter = null;
  7902  
  7903        /**
  7904         * Function that formats a week number into a label for the week.
  7905         * @type {function(number): string}
  7906         */
  7907        this.weekNumberFormatter = null;
  7908  
  7909        /**
  7910         * Function that formats a date into a long aria-label that is read
  7911         * when the focused date changes.
  7912         * @type {function(Date): string}
  7913         */
  7914        this.longDateFormatter = null;
  7915  
  7916        /**
  7917         * ARIA label for the calendar "dialog" used in the datepicker.
  7918         * @type {string}
  7919         */
  7920        this.msgCalendar = '';
  7921  
  7922        /**
  7923         * ARIA label for the datepicker's "Open calendar" buttons.
  7924         * @type {string}
  7925         */
  7926        this.msgOpenCalendar = '';
  7927      }
  7928  
  7929      /**
  7930       * Factory function that returns an instance of the dateLocale service.
  7931       * @ngInject
  7932       * @param $locale
  7933       * @returns {DateLocale}
  7934       */
  7935      DateLocaleProvider.prototype.$get = function($locale) {
  7936        /**
  7937         * Default date-to-string formatting function.
  7938         * @param {!Date} date
  7939         * @returns {string}
  7940         */
  7941        function defaultFormatDate(date) {
  7942          if (!date) {
  7943            return '';
  7944          }
  7945  
  7946          // All of the dates created through ng-material *should* be set to midnight.
  7947          // If we encounter a date where the localeTime shows at 11pm instead of midnight,
  7948          // we have run into an issue with DST where we need to increment the hour by one:
  7949          // var d = new Date(1992, 9, 8, 0, 0, 0);
  7950          // d.toLocaleString(); // == "10/7/1992, 11:00:00 PM"
  7951          var localeTime = date.toLocaleTimeString();
  7952          var formatDate = date;
  7953          if (date.getHours() == 0 &&
  7954              (localeTime.indexOf('11:') !== -1 || localeTime.indexOf('23:') !== -1)) {
  7955            formatDate = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 1, 0, 0);
  7956          }
  7957  
  7958          return formatDate.toLocaleDateString();
  7959        }
  7960  
  7961        /**
  7962         * Default string-to-date parsing function.
  7963         * @param {string} dateString
  7964         * @returns {!Date}
  7965         */
  7966        function defaultParseDate(dateString) {
  7967          return new Date(dateString);
  7968        }
  7969  
  7970        /**
  7971         * Default function to determine whether a string makes sense to be
  7972         * parsed to a Date object.
  7973         *
  7974         * This is very permissive and is just a basic sanity check to ensure that
  7975         * things like single integers aren't able to be parsed into dates.
  7976         * @param {string} dateString
  7977         * @returns {boolean}
  7978         */
  7979        function defaultIsDateComplete(dateString) {
  7980          dateString = dateString.trim();
  7981  
  7982          // Looks for three chunks of content (either numbers or text) separated
  7983          // by delimiters.
  7984          var re = /^(([a-zA-Z]{3,}|[0-9]{1,4})([ \.,]+|[\/\-])){2}([a-zA-Z]{3,}|[0-9]{1,4})$/;
  7985          return re.test(dateString);
  7986        }
  7987  
  7988        /**
  7989         * Default date-to-string formatter to get a month header.
  7990         * @param {!Date} date
  7991         * @returns {string}
  7992         */
  7993        function defaultMonthHeaderFormatter(date) {
  7994          return service.shortMonths[date.getMonth()] + ' ' + date.getFullYear();
  7995        }
  7996  
  7997        /**
  7998         * Default week number formatter.
  7999         * @param number
  8000         * @returns {string}
  8001         */
  8002        function defaultWeekNumberFormatter(number) {
  8003          return 'Week ' + number;
  8004        }
  8005  
  8006        /**
  8007         * Default formatter for date cell aria-labels.
  8008         * @param {!Date} date
  8009         * @returns {string}
  8010         */
  8011        function defaultLongDateFormatter(date) {
  8012          // Example: 'Thursday June 18 2015'
  8013          return [
  8014            service.days[date.getDay()],
  8015            service.months[date.getMonth()],
  8016            service.dates[date.getDate()],
  8017            date.getFullYear()
  8018          ].join(' ');
  8019        }
  8020  
  8021        // The default "short" day strings are the first character of each day,
  8022        // e.g., "Monday" => "M".
  8023        var defaultShortDays = $locale.DATETIME_FORMATS.DAY.map(function(day) {
  8024          return day[0];
  8025        });
  8026  
  8027        // The default dates are simply the numbers 1 through 31.
  8028        var defaultDates = Array(32);
  8029        for (var i = 1; i <= 31; i++) {
  8030          defaultDates[i] = i;
  8031        }
  8032  
  8033        // Default ARIA messages are in English (US).
  8034        var defaultMsgCalendar = 'Calendar';
  8035        var defaultMsgOpenCalendar = 'Open calendar';
  8036  
  8037        var service = {
  8038          months: this.months || $locale.DATETIME_FORMATS.MONTH,
  8039          shortMonths: this.shortMonths || $locale.DATETIME_FORMATS.SHORTMONTH,
  8040          days: this.days || $locale.DATETIME_FORMATS.DAY,
  8041          shortDays: this.shortDays || defaultShortDays,
  8042          dates: this.dates || defaultDates,
  8043          firstDayOfWeek: this.firstDayOfWeek || 0,
  8044          formatDate: this.formatDate || defaultFormatDate,
  8045          parseDate: this.parseDate || defaultParseDate,
  8046          isDateComplete: this.isDateComplete || defaultIsDateComplete,
  8047          monthHeaderFormatter: this.monthHeaderFormatter || defaultMonthHeaderFormatter,
  8048          weekNumberFormatter: this.weekNumberFormatter || defaultWeekNumberFormatter,
  8049          longDateFormatter: this.longDateFormatter || defaultLongDateFormatter,
  8050          msgCalendar: this.msgCalendar || defaultMsgCalendar,
  8051          msgOpenCalendar: this.msgOpenCalendar || defaultMsgOpenCalendar
  8052        };
  8053  
  8054        return service;
  8055      };
  8056      DateLocaleProvider.prototype.$get.$inject = ["$locale"];
  8057  
  8058      $provide.provider('$mdDateLocale', new DateLocaleProvider());
  8059    }]);
  8060  })();
  8061  
  8062  })();
  8063  (function(){
  8064  "use strict";
  8065  
  8066  (function() {
  8067    'use strict';
  8068  
  8069    // POST RELEASE
  8070    // TODO(jelbourn): Demo that uses moment.js
  8071    // TODO(jelbourn): make sure this plays well with validation and ngMessages.
  8072    // TODO(jelbourn): calendar pane doesn't open up outside of visible viewport.
  8073    // TODO(jelbourn): forward more attributes to the internal input (required, autofocus, etc.)
  8074    // TODO(jelbourn): something better for mobile (calendar panel takes up entire screen?)
  8075    // TODO(jelbourn): input behavior (masking? auto-complete?)
  8076    // TODO(jelbourn): UTC mode
  8077    // TODO(jelbourn): RTL
  8078  
  8079  
  8080    angular.module('material.components.datepicker')
  8081        .directive('mdDatepicker', datePickerDirective);
  8082  
  8083    /**
  8084     * @ngdoc directive
  8085     * @name mdDatepicker
  8086     * @module material.components.datepicker
  8087     *
  8088     * @param {Date} ng-model The component's model. Expects a JavaScript Date object.
  8089     * @param {expression=} ng-change Expression evaluated when the model value changes.
  8090     * @param {Date=} md-min-date Expression representing a min date (inclusive).
  8091     * @param {Date=} md-max-date Expression representing a max date (inclusive).
  8092     * @param {(function(Date): boolean)=} md-date-filter Function expecting a date and returning a boolean whether it can be selected or not.
  8093     * @param {String=} md-placeholder The date input placeholder value.
  8094     * @param {boolean=} ng-disabled Whether the datepicker is disabled.
  8095     * @param {boolean=} ng-required Whether a value is required for the datepicker.
  8096     *
  8097     * @description
  8098     * `<md-datepicker>` is a component used to select a single date.
  8099     * For information on how to configure internationalization for the date picker,
  8100     * see `$mdDateLocaleProvider`.
  8101     *
  8102     * This component supports [ngMessages](https://docs.angularjs.org/api/ngMessages/directive/ngMessages).
  8103     * Supported attributes are:
  8104     * * `required`: whether a required date is not set.
  8105     * * `mindate`: whether the selected date is before the minimum allowed date.
  8106     * * `maxdate`: whether the selected date is after the maximum allowed date.
  8107     *
  8108     * @usage
  8109     * <hljs lang="html">
  8110     *   <md-datepicker ng-model="birthday"></md-datepicker>
  8111     * </hljs>
  8112     *
  8113     */
  8114    function datePickerDirective() {
  8115      return {
  8116        template:
  8117            // Buttons are not in the tab order because users can open the calendar via keyboard
  8118            // interaction on the text input, and multiple tab stops for one component (picker)
  8119            // may be confusing.
  8120            '<md-button class="md-datepicker-button md-icon-button" type="button" ' +
  8121                'tabindex="-1" aria-hidden="true" ' +
  8122                'ng-click="ctrl.openCalendarPane($event)">' +
  8123              '<md-icon class="md-datepicker-calendar-icon" md-svg-icon="md-calendar"></md-icon>' +
  8124            '</md-button>' +
  8125            '<div class="md-datepicker-input-container" ' +
  8126                'ng-class="{\'md-datepicker-focused\': ctrl.isFocused}">' +
  8127              '<input class="md-datepicker-input" aria-haspopup="true" ' +
  8128                  'ng-focus="ctrl.setFocused(true)" ng-blur="ctrl.setFocused(false)">' +
  8129              '<md-button type="button" md-no-ink ' +
  8130                  'class="md-datepicker-triangle-button md-icon-button" ' +
  8131                  'ng-click="ctrl.openCalendarPane($event)" ' +
  8132                  'aria-label="{{::ctrl.dateLocale.msgOpenCalendar}}">' +
  8133                '<div class="md-datepicker-expand-triangle"></div>' +
  8134              '</md-button>' +
  8135            '</div>' +
  8136  
  8137            // This pane will be detached from here and re-attached to the document body.
  8138            '<div class="md-datepicker-calendar-pane md-whiteframe-z1">' +
  8139              '<div class="md-datepicker-input-mask">' +
  8140                '<div class="md-datepicker-input-mask-opaque"></div>' +
  8141              '</div>' +
  8142              '<div class="md-datepicker-calendar">' +
  8143                '<md-calendar role="dialog" aria-label="{{::ctrl.dateLocale.msgCalendar}}" ' +
  8144                    'md-min-date="ctrl.minDate" md-max-date="ctrl.maxDate"' +
  8145                    'md-date-filter="ctrl.dateFilter"' +
  8146                    'ng-model="ctrl.date" ng-if="ctrl.isCalendarOpen">' +
  8147                '</md-calendar>' +
  8148              '</div>' +
  8149            '</div>',
  8150        require: ['ngModel', 'mdDatepicker', '?^mdInputContainer'],
  8151        scope: {
  8152          minDate: '=mdMinDate',
  8153          maxDate: '=mdMaxDate',
  8154          placeholder: '@mdPlaceholder',
  8155          dateFilter: '=mdDateFilter'
  8156        },
  8157        controller: DatePickerCtrl,
  8158        controllerAs: 'ctrl',
  8159        bindToController: true,
  8160        link: function(scope, element, attr, controllers) {
  8161          var ngModelCtrl = controllers[0];
  8162          var mdDatePickerCtrl = controllers[1];
  8163  
  8164          var mdInputContainer = controllers[2];
  8165          if (mdInputContainer) {
  8166            throw Error('md-datepicker should not be placed inside md-input-container.');
  8167          }
  8168  
  8169          mdDatePickerCtrl.configureNgModel(ngModelCtrl);
  8170        }
  8171      };
  8172    }
  8173  
  8174    /** Additional offset for the input's `size` attribute, which is updated based on its content. */
  8175    var EXTRA_INPUT_SIZE = 3;
  8176  
  8177    /** Class applied to the container if the date is invalid. */
  8178    var INVALID_CLASS = 'md-datepicker-invalid';
  8179  
  8180    /** Default time in ms to debounce input event by. */
  8181    var DEFAULT_DEBOUNCE_INTERVAL = 500;
  8182  
  8183    /**
  8184     * Height of the calendar pane used to check if the pane is going outside the boundary of
  8185     * the viewport. See calendar.scss for how $md-calendar-height is computed; an extra 20px is
  8186     * also added to space the pane away from the exact edge of the screen.
  8187     *
  8188     *  This is computed statically now, but can be changed to be measured if the circumstances
  8189     *  of calendar sizing are changed.
  8190     */
  8191    var CALENDAR_PANE_HEIGHT = 368;
  8192  
  8193    /**
  8194     * Width of the calendar pane used to check if the pane is going outside the boundary of
  8195     * the viewport. See calendar.scss for how $md-calendar-width is computed; an extra 20px is
  8196     * also added to space the pane away from the exact edge of the screen.
  8197     *
  8198     *  This is computed statically now, but can be changed to be measured if the circumstances
  8199     *  of calendar sizing are changed.
  8200     */
  8201    var CALENDAR_PANE_WIDTH = 360;
  8202  
  8203    /**
  8204     * Controller for md-datepicker.
  8205     *
  8206     * @ngInject @constructor
  8207     */
  8208    function DatePickerCtrl($scope, $element, $attrs, $compile, $timeout, $window,
  8209        $mdConstant, $mdTheming, $mdUtil, $mdDateLocale, $$mdDateUtil, $$rAF) {
  8210      /** @final */
  8211      this.$compile = $compile;
  8212  
  8213      /** @final */
  8214      this.$timeout = $timeout;
  8215  
  8216      /** @final */
  8217      this.$window = $window;
  8218  
  8219      /** @final */
  8220      this.dateLocale = $mdDateLocale;
  8221  
  8222      /** @final */
  8223      this.dateUtil = $$mdDateUtil;
  8224  
  8225      /** @final */
  8226      this.$mdConstant = $mdConstant;
  8227  
  8228      /* @final */
  8229      this.$mdUtil = $mdUtil;
  8230  
  8231      /** @final */
  8232      this.$$rAF = $$rAF;
  8233  
  8234      /**
  8235       * The root document element. This is used for attaching a top-level click handler to
  8236       * close the calendar panel when a click outside said panel occurs. We use `documentElement`
  8237       * instead of body because, when scrolling is disabled, some browsers consider the body element
  8238       * to be completely off the screen and propagate events directly to the html element.
  8239       * @type {!angular.JQLite}
  8240       */
  8241      this.documentElement = angular.element(document.documentElement);
  8242  
  8243      /** @type {!angular.NgModelController} */
  8244      this.ngModelCtrl = null;
  8245  
  8246      /** @type {HTMLInputElement} */
  8247      this.inputElement = $element[0].querySelector('input');
  8248  
  8249      /** @final {!angular.JQLite} */
  8250      this.ngInputElement = angular.element(this.inputElement);
  8251  
  8252      /** @type {HTMLElement} */
  8253      this.inputContainer = $element[0].querySelector('.md-datepicker-input-container');
  8254  
  8255      /** @type {HTMLElement} Floating calendar pane. */
  8256      this.calendarPane = $element[0].querySelector('.md-datepicker-calendar-pane');
  8257  
  8258      /** @type {HTMLElement} Calendar icon button. */
  8259      this.calendarButton = $element[0].querySelector('.md-datepicker-button');
  8260  
  8261      /**
  8262       * Element covering everything but the input in the top of the floating calendar pane.
  8263       * @type {HTMLElement}
  8264       */
  8265      this.inputMask = $element[0].querySelector('.md-datepicker-input-mask-opaque');
  8266  
  8267      /** @final {!angular.JQLite} */
  8268      this.$element = $element;
  8269  
  8270      /** @final {!angular.Attributes} */
  8271      this.$attrs = $attrs;
  8272  
  8273      /** @final {!angular.Scope} */
  8274      this.$scope = $scope;
  8275  
  8276      /** @type {Date} */
  8277      this.date = null;
  8278  
  8279      /** @type {boolean} */
  8280      this.isFocused = false;
  8281  
  8282      /** @type {boolean} */
  8283      this.isDisabled;
  8284      this.setDisabled($element[0].disabled || angular.isString($attrs['disabled']));
  8285  
  8286      /** @type {boolean} Whether the date-picker's calendar pane is open. */
  8287      this.isCalendarOpen = false;
  8288  
  8289      /**
  8290       * Element from which the calendar pane was opened. Keep track of this so that we can return
  8291       * focus to it when the pane is closed.
  8292       * @type {HTMLElement}
  8293       */
  8294      this.calendarPaneOpenedFrom = null;
  8295  
  8296      this.calendarPane.id = 'md-date-pane' + $mdUtil.nextUid();
  8297  
  8298      $mdTheming($element);
  8299  
  8300      /** Pre-bound click handler is saved so that the event listener can be removed. */
  8301      this.bodyClickHandler = angular.bind(this, this.handleBodyClick);
  8302  
  8303      /** Pre-bound resize handler so that the event listener can be removed. */
  8304      this.windowResizeHandler = $mdUtil.debounce(angular.bind(this, this.closeCalendarPane), 100);
  8305  
  8306      // Unless the user specifies so, the datepicker should not be a tab stop.
  8307      // This is necessary because ngAria might add a tabindex to anything with an ng-model
  8308      // (based on whether or not the user has turned that particular feature on/off).
  8309      if (!$attrs['tabindex']) {
  8310        $element.attr('tabindex', '-1');
  8311      }
  8312  
  8313      this.installPropertyInterceptors();
  8314      this.attachChangeListeners();
  8315      this.attachInteractionListeners();
  8316  
  8317      var self = this;
  8318      $scope.$on('$destroy', function() {
  8319        self.detachCalendarPane();
  8320      });
  8321    }
  8322    DatePickerCtrl.$inject = ["$scope", "$element", "$attrs", "$compile", "$timeout", "$window", "$mdConstant", "$mdTheming", "$mdUtil", "$mdDateLocale", "$$mdDateUtil", "$$rAF"];
  8323  
  8324    /**
  8325     * Sets up the controller's reference to ngModelController.
  8326     * @param {!angular.NgModelController} ngModelCtrl
  8327     */
  8328    DatePickerCtrl.prototype.configureNgModel = function(ngModelCtrl) {
  8329      this.ngModelCtrl = ngModelCtrl;
  8330  
  8331      var self = this;
  8332      ngModelCtrl.$render = function() {
  8333        var value = self.ngModelCtrl.$viewValue;
  8334  
  8335        if (value && !(value instanceof Date)) {
  8336          throw Error('The ng-model for md-datepicker must be a Date instance. ' +
  8337              'Currently the model is a: ' + (typeof value));
  8338        }
  8339  
  8340        self.date = value;
  8341        self.inputElement.value = self.dateLocale.formatDate(value);
  8342        self.resizeInputElement();
  8343        self.updateErrorState();
  8344      };
  8345    };
  8346  
  8347    /**
  8348     * Attach event listeners for both the text input and the md-calendar.
  8349     * Events are used instead of ng-model so that updates don't infinitely update the other
  8350     * on a change. This should also be more performant than using a $watch.
  8351     */
  8352    DatePickerCtrl.prototype.attachChangeListeners = function() {
  8353      var self = this;
  8354  
  8355      self.$scope.$on('md-calendar-change', function(event, date) {
  8356        self.ngModelCtrl.$setViewValue(date);
  8357        self.date = date;
  8358        self.inputElement.value = self.dateLocale.formatDate(date);
  8359        self.closeCalendarPane();
  8360        self.resizeInputElement();
  8361        self.updateErrorState();
  8362      });
  8363  
  8364      self.ngInputElement.on('input', angular.bind(self, self.resizeInputElement));
  8365      // TODO(chenmike): Add ability for users to specify this interval.
  8366      self.ngInputElement.on('input', self.$mdUtil.debounce(self.handleInputEvent,
  8367          DEFAULT_DEBOUNCE_INTERVAL, self));
  8368    };
  8369  
  8370    /** Attach event listeners for user interaction. */
  8371    DatePickerCtrl.prototype.attachInteractionListeners = function() {
  8372      var self = this;
  8373      var $scope = this.$scope;
  8374      var keyCodes = this.$mdConstant.KEY_CODE;
  8375  
  8376      // Add event listener through angular so that we can triggerHandler in unit tests.
  8377      self.ngInputElement.on('keydown', function(event) {
  8378        if (event.altKey && event.keyCode == keyCodes.DOWN_ARROW) {
  8379          self.openCalendarPane(event);
  8380          $scope.$digest();
  8381        }
  8382      });
  8383  
  8384      $scope.$on('md-calendar-close', function() {
  8385        self.closeCalendarPane();
  8386      });
  8387    };
  8388  
  8389    /**
  8390     * Capture properties set to the date-picker and imperitively handle internal changes.
  8391     * This is done to avoid setting up additional $watches.
  8392     */
  8393    DatePickerCtrl.prototype.installPropertyInterceptors = function() {
  8394      var self = this;
  8395  
  8396      if (this.$attrs['ngDisabled']) {
  8397        // The expression is to be evaluated against the directive element's scope and not
  8398        // the directive's isolate scope.
  8399        var scope = this.$scope.$parent;
  8400  
  8401        if (scope) {
  8402          scope.$watch(this.$attrs['ngDisabled'], function(isDisabled) {
  8403            self.setDisabled(isDisabled);
  8404          });
  8405        }
  8406      }
  8407  
  8408      Object.defineProperty(this, 'placeholder', {
  8409        get: function() { return self.inputElement.placeholder; },
  8410        set: function(value) { self.inputElement.placeholder = value || ''; }
  8411      });
  8412    };
  8413  
  8414    /**
  8415     * Sets whether the date-picker is disabled.
  8416     * @param {boolean} isDisabled
  8417     */
  8418    DatePickerCtrl.prototype.setDisabled = function(isDisabled) {
  8419      this.isDisabled = isDisabled;
  8420      this.inputElement.disabled = isDisabled;
  8421      this.calendarButton.disabled = isDisabled;
  8422    };
  8423  
  8424    /**
  8425     * Sets the custom ngModel.$error flags to be consumed by ngMessages. Flags are:
  8426     *   - mindate: whether the selected date is before the minimum date.
  8427     *   - maxdate: whether the selected flag is after the maximum date.
  8428     *   - filtered: whether the selected date is allowed by the custom filtering function.
  8429     *   - valid: whether the entered text input is a valid date
  8430     *
  8431     * The 'required' flag is handled automatically by ngModel.
  8432     *
  8433     * @param {Date=} opt_date Date to check. If not given, defaults to the datepicker's model value.
  8434     */
  8435    DatePickerCtrl.prototype.updateErrorState = function(opt_date) {
  8436      var date = opt_date || this.date;
  8437  
  8438      // Clear any existing errors to get rid of anything that's no longer relevant.
  8439      this.clearErrorState();
  8440  
  8441      if (this.dateUtil.isValidDate(date)) {
  8442        // Force all dates to midnight in order to ignore the time portion.
  8443        date = this.dateUtil.createDateAtMidnight(date);
  8444  
  8445        if (this.dateUtil.isValidDate(this.minDate)) {
  8446          var minDate = this.dateUtil.createDateAtMidnight(this.minDate);
  8447          this.ngModelCtrl.$setValidity('mindate', date >= minDate);
  8448        }
  8449  
  8450        if (this.dateUtil.isValidDate(this.maxDate)) {
  8451          var maxDate = this.dateUtil.createDateAtMidnight(this.maxDate);
  8452          this.ngModelCtrl.$setValidity('maxdate', date <= maxDate);
  8453        }
  8454        
  8455        if (angular.isFunction(this.dateFilter)) {
  8456          this.ngModelCtrl.$setValidity('filtered', this.dateFilter(date));
  8457        }
  8458      } else {
  8459        // The date is seen as "not a valid date" if there is *something* set
  8460        // (i.e.., not null or undefined), but that something isn't a valid date.
  8461        this.ngModelCtrl.$setValidity('valid', date == null);
  8462      }
  8463  
  8464      // TODO(jelbourn): Change this to classList.toggle when we stop using PhantomJS in unit tests
  8465      // because it doesn't conform to the DOMTokenList spec.
  8466      // See https://github.com/ariya/phantomjs/issues/12782.
  8467      if (!this.ngModelCtrl.$valid) {
  8468        this.inputContainer.classList.add(INVALID_CLASS);
  8469      }
  8470    };
  8471  
  8472    /** Clears any error flags set by `updateErrorState`. */
  8473    DatePickerCtrl.prototype.clearErrorState = function() {
  8474      this.inputContainer.classList.remove(INVALID_CLASS);
  8475      ['mindate', 'maxdate', 'filtered', 'valid'].forEach(function(field) {
  8476        this.ngModelCtrl.$setValidity(field, true);
  8477      }, this);
  8478    };
  8479  
  8480    /** Resizes the input element based on the size of its content. */
  8481    DatePickerCtrl.prototype.resizeInputElement = function() {
  8482      this.inputElement.size = this.inputElement.value.length + EXTRA_INPUT_SIZE;
  8483    };
  8484  
  8485    /**
  8486     * Sets the model value if the user input is a valid date.
  8487     * Adds an invalid class to the input element if not.
  8488     */
  8489    DatePickerCtrl.prototype.handleInputEvent = function() {
  8490      var inputString = this.inputElement.value;
  8491      var parsedDate = inputString ? this.dateLocale.parseDate(inputString) : null;
  8492      this.dateUtil.setDateTimeToMidnight(parsedDate);
  8493  
  8494      // An input string is valid if it is either empty (representing no date)
  8495      // or if it parses to a valid date that the user is allowed to select.
  8496      var isValidInput = inputString == '' || (
  8497        this.dateUtil.isValidDate(parsedDate) &&
  8498        this.dateLocale.isDateComplete(inputString) &&
  8499        this.isDateEnabled(parsedDate)
  8500      );
  8501  
  8502      // The datepicker's model is only updated when there is a valid input.
  8503      if (isValidInput) {
  8504        this.ngModelCtrl.$setViewValue(parsedDate);
  8505        this.date = parsedDate;
  8506      }
  8507  
  8508      this.updateErrorState(parsedDate);
  8509    };
  8510    
  8511    /**
  8512     * Check whether date is in range and enabled
  8513     * @param {Date=} opt_date
  8514     * @return {boolean} Whether the date is enabled.
  8515     */
  8516    DatePickerCtrl.prototype.isDateEnabled = function(opt_date) {
  8517      return this.dateUtil.isDateWithinRange(opt_date, this.minDate, this.maxDate) && 
  8518            (!angular.isFunction(this.dateFilter) || this.dateFilter(opt_date));
  8519    };
  8520    
  8521    /** Position and attach the floating calendar to the document. */
  8522    DatePickerCtrl.prototype.attachCalendarPane = function() {
  8523      var calendarPane = this.calendarPane;
  8524      calendarPane.style.transform = '';
  8525      this.$element.addClass('md-datepicker-open');
  8526  
  8527      var elementRect = this.inputContainer.getBoundingClientRect();
  8528      var bodyRect = document.body.getBoundingClientRect();
  8529  
  8530      // Check to see if the calendar pane would go off the screen. If so, adjust position
  8531      // accordingly to keep it within the viewport.
  8532      var paneTop = elementRect.top - bodyRect.top;
  8533      var paneLeft = elementRect.left - bodyRect.left;
  8534  
  8535      // If ng-material has disabled body scrolling (for example, if a dialog is open),
  8536      // then it's possible that the already-scrolled body has a negative top/left. In this case,
  8537      // we want to treat the "real" top as (0 - bodyRect.top). In a normal scrolling situation,
  8538      // though, the top of the viewport should just be the body's scroll position.
  8539      var viewportTop = (bodyRect.top < 0 && document.body.scrollTop == 0) ?
  8540          -bodyRect.top :
  8541          document.body.scrollTop;
  8542  
  8543      var viewportLeft = (bodyRect.left < 0 && document.body.scrollLeft == 0) ?
  8544          -bodyRect.left :
  8545          document.body.scrollLeft;
  8546  
  8547      var viewportBottom = viewportTop + this.$window.innerHeight;
  8548      var viewportRight = viewportLeft + this.$window.innerWidth;
  8549  
  8550      // If the right edge of the pane would be off the screen and shifting it left by the
  8551      // difference would not go past the left edge of the screen. If the calendar pane is too
  8552      // big to fit on the screen at all, move it to the left of the screen and scale the entire
  8553      // element down to fit.
  8554      if (paneLeft + CALENDAR_PANE_WIDTH > viewportRight) {
  8555        if (viewportRight - CALENDAR_PANE_WIDTH > 0) {
  8556          paneLeft = viewportRight - CALENDAR_PANE_WIDTH;
  8557        } else {
  8558          paneLeft = viewportLeft;
  8559          var scale = this.$window.innerWidth / CALENDAR_PANE_WIDTH;
  8560          calendarPane.style.transform = 'scale(' + scale + ')';
  8561        }
  8562  
  8563        calendarPane.classList.add('md-datepicker-pos-adjusted');
  8564      }
  8565  
  8566      // If the bottom edge of the pane would be off the screen and shifting it up by the
  8567      // difference would not go past the top edge of the screen.
  8568      if (paneTop + CALENDAR_PANE_HEIGHT > viewportBottom &&
  8569          viewportBottom - CALENDAR_PANE_HEIGHT > viewportTop) {
  8570        paneTop = viewportBottom - CALENDAR_PANE_HEIGHT;
  8571        calendarPane.classList.add('md-datepicker-pos-adjusted');
  8572      }
  8573  
  8574      calendarPane.style.left = paneLeft + 'px';
  8575      calendarPane.style.top = paneTop + 'px';
  8576      document.body.appendChild(calendarPane);
  8577  
  8578      // The top of the calendar pane is a transparent box that shows the text input underneath.
  8579      // Since the pane is floating, though, the page underneath the pane *adjacent* to the input is
  8580      // also shown unless we cover it up. The inputMask does this by filling up the remaining space
  8581      // based on the width of the input.
  8582      this.inputMask.style.left = elementRect.width + 'px';
  8583  
  8584      // Add CSS class after one frame to trigger open animation.
  8585      this.$$rAF(function() {
  8586        calendarPane.classList.add('md-pane-open');
  8587      });
  8588    };
  8589  
  8590    /** Detach the floating calendar pane from the document. */
  8591    DatePickerCtrl.prototype.detachCalendarPane = function() {
  8592      this.$element.removeClass('md-datepicker-open');
  8593      this.calendarPane.classList.remove('md-pane-open');
  8594      this.calendarPane.classList.remove('md-datepicker-pos-adjusted');
  8595  
  8596      if (this.isCalendarOpen) {
  8597        this.$mdUtil.enableScrolling();
  8598      }
  8599  
  8600      if (this.calendarPane.parentNode) {
  8601        // Use native DOM removal because we do not want any of the angular state of this element
  8602        // to be disposed.
  8603        this.calendarPane.parentNode.removeChild(this.calendarPane);
  8604      }
  8605    };
  8606  
  8607    /**
  8608     * Open the floating calendar pane.
  8609     * @param {Event} event
  8610     */
  8611    DatePickerCtrl.prototype.openCalendarPane = function(event) {
  8612      if (!this.isCalendarOpen && !this.isDisabled) {
  8613        this.isCalendarOpen = true;
  8614        this.calendarPaneOpenedFrom = event.target;
  8615  
  8616        // Because the calendar pane is attached directly to the body, it is possible that the
  8617        // rest of the component (input, etc) is in a different scrolling container, such as
  8618        // an md-content. This means that, if the container is scrolled, the pane would remain
  8619        // stationary. To remedy this, we disable scrolling while the calendar pane is open, which
  8620        // also matches the native behavior for things like `<select>` on Mac and Windows.
  8621        this.$mdUtil.disableScrollAround(this.calendarPane);
  8622  
  8623        this.attachCalendarPane();
  8624        this.focusCalendar();
  8625  
  8626        // Attach click listener inside of a timeout because, if this open call was triggered by a
  8627        // click, we don't want it to be immediately propogated up to the body and handled.
  8628        var self = this;
  8629        this.$mdUtil.nextTick(function() {
  8630          // Use 'touchstart` in addition to click in order to work on iOS Safari, where click
  8631          // events aren't propogated under most circumstances.
  8632          // See http://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html
  8633          self.documentElement.on('click touchstart', self.bodyClickHandler);
  8634        }, false);
  8635  
  8636        window.addEventListener('resize', this.windowResizeHandler);
  8637      }
  8638    };
  8639  
  8640    /** Close the floating calendar pane. */
  8641    DatePickerCtrl.prototype.closeCalendarPane = function() {
  8642      if (this.isCalendarOpen) {
  8643        this.detachCalendarPane();
  8644        this.isCalendarOpen = false;
  8645        this.calendarPaneOpenedFrom.focus();
  8646        this.calendarPaneOpenedFrom = null;
  8647  
  8648        this.ngModelCtrl.$setTouched();
  8649  
  8650        this.documentElement.off('click touchstart', this.bodyClickHandler);
  8651        window.removeEventListener('resize', this.windowResizeHandler);
  8652      }
  8653    };
  8654  
  8655    /** Gets the controller instance for the calendar in the floating pane. */
  8656    DatePickerCtrl.prototype.getCalendarCtrl = function() {
  8657      return angular.element(this.calendarPane.querySelector('md-calendar')).controller('mdCalendar');
  8658    };
  8659  
  8660    /** Focus the calendar in the floating pane. */
  8661    DatePickerCtrl.prototype.focusCalendar = function() {
  8662      // Use a timeout in order to allow the calendar to be rendered, as it is gated behind an ng-if.
  8663      var self = this;
  8664      this.$mdUtil.nextTick(function() {
  8665        self.getCalendarCtrl().focus();
  8666      }, false);
  8667    };
  8668  
  8669    /**
  8670     * Sets whether the input is currently focused.
  8671     * @param {boolean} isFocused
  8672     */
  8673    DatePickerCtrl.prototype.setFocused = function(isFocused) {
  8674      if (!isFocused) {
  8675        this.ngModelCtrl.$setTouched();
  8676      }
  8677      this.isFocused = isFocused;
  8678    };
  8679  
  8680    /**
  8681     * Handles a click on the document body when the floating calendar pane is open.
  8682     * Closes the floating calendar pane if the click is not inside of it.
  8683     * @param {MouseEvent} event
  8684     */
  8685    DatePickerCtrl.prototype.handleBodyClick = function(event) {
  8686      if (this.isCalendarOpen) {
  8687        // TODO(jelbourn): way want to also include the md-datepicker itself in this check.
  8688        var isInCalendar = this.$mdUtil.getClosest(event.target, 'md-calendar');
  8689        if (!isInCalendar) {
  8690          this.closeCalendarPane();
  8691        }
  8692  
  8693        this.$scope.$digest();
  8694      }
  8695    };
  8696  })();
  8697  
  8698  })();
  8699  (function(){
  8700  "use strict";
  8701  
  8702  (function() {
  8703    'use strict';
  8704  
  8705    /**
  8706     * Utility for performing date calculations to facilitate operation of the calendar and
  8707     * datepicker.
  8708     */
  8709    angular.module('material.components.datepicker').factory('$$mdDateUtil', function() {
  8710      return {
  8711        getFirstDateOfMonth: getFirstDateOfMonth,
  8712        getNumberOfDaysInMonth: getNumberOfDaysInMonth,
  8713        getDateInNextMonth: getDateInNextMonth,
  8714        getDateInPreviousMonth: getDateInPreviousMonth,
  8715        isInNextMonth: isInNextMonth,
  8716        isInPreviousMonth: isInPreviousMonth,
  8717        getDateMidpoint: getDateMidpoint,
  8718        isSameMonthAndYear: isSameMonthAndYear,
  8719        getWeekOfMonth: getWeekOfMonth,
  8720        incrementDays: incrementDays,
  8721        incrementMonths: incrementMonths,
  8722        getLastDateOfMonth: getLastDateOfMonth,
  8723        isSameDay: isSameDay,
  8724        getMonthDistance: getMonthDistance,
  8725        isValidDate: isValidDate,
  8726        setDateTimeToMidnight: setDateTimeToMidnight,
  8727        createDateAtMidnight: createDateAtMidnight,
  8728        isDateWithinRange: isDateWithinRange
  8729      };
  8730  
  8731      /**
  8732       * Gets the first day of the month for the given date's month.
  8733       * @param {Date} date
  8734       * @returns {Date}
  8735       */
  8736      function getFirstDateOfMonth(date) {
  8737        return new Date(date.getFullYear(), date.getMonth(), 1);
  8738      }
  8739  
  8740      /**
  8741       * Gets the number of days in the month for the given date's month.
  8742       * @param date
  8743       * @returns {number}
  8744       */
  8745      function getNumberOfDaysInMonth(date) {
  8746        return new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate();
  8747      }
  8748  
  8749      /**
  8750       * Get an arbitrary date in the month after the given date's month.
  8751       * @param date
  8752       * @returns {Date}
  8753       */
  8754      function getDateInNextMonth(date) {
  8755        return new Date(date.getFullYear(), date.getMonth() + 1, 1);
  8756      }
  8757  
  8758      /**
  8759       * Get an arbitrary date in the month before the given date's month.
  8760       * @param date
  8761       * @returns {Date}
  8762       */
  8763      function getDateInPreviousMonth(date) {
  8764        return new Date(date.getFullYear(), date.getMonth() - 1, 1);
  8765      }
  8766  
  8767      /**
  8768       * Gets whether two dates have the same month and year.
  8769       * @param {Date} d1
  8770       * @param {Date} d2
  8771       * @returns {boolean}
  8772       */
  8773      function isSameMonthAndYear(d1, d2) {
  8774        return d1.getFullYear() === d2.getFullYear() && d1.getMonth() === d2.getMonth();
  8775      }
  8776  
  8777      /**
  8778       * Gets whether two dates are the same day (not not necessarily the same time).
  8779       * @param {Date} d1
  8780       * @param {Date} d2
  8781       * @returns {boolean}
  8782       */
  8783      function isSameDay(d1, d2) {
  8784        return d1.getDate() == d2.getDate() && isSameMonthAndYear(d1, d2);
  8785      }
  8786  
  8787      /**
  8788       * Gets whether a date is in the month immediately after some date.
  8789       * @param {Date} startDate The date from which to compare.
  8790       * @param {Date} endDate The date to check.
  8791       * @returns {boolean}
  8792       */
  8793      function isInNextMonth(startDate, endDate) {
  8794        var nextMonth = getDateInNextMonth(startDate);
  8795        return isSameMonthAndYear(nextMonth, endDate);
  8796      }
  8797  
  8798      /**
  8799       * Gets whether a date is in the month immediately before some date.
  8800       * @param {Date} startDate The date from which to compare.
  8801       * @param {Date} endDate The date to check.
  8802       * @returns {boolean}
  8803       */
  8804      function isInPreviousMonth(startDate, endDate) {
  8805        var previousMonth = getDateInPreviousMonth(startDate);
  8806        return isSameMonthAndYear(endDate, previousMonth);
  8807      }
  8808  
  8809      /**
  8810       * Gets the midpoint between two dates.
  8811       * @param {Date} d1
  8812       * @param {Date} d2
  8813       * @returns {Date}
  8814       */
  8815      function getDateMidpoint(d1, d2) {
  8816        return createDateAtMidnight((d1.getTime() + d2.getTime()) / 2);
  8817      }
  8818  
  8819      /**
  8820       * Gets the week of the month that a given date occurs in.
  8821       * @param {Date} date
  8822       * @returns {number} Index of the week of the month (zero-based).
  8823       */
  8824      function getWeekOfMonth(date) {
  8825        var firstDayOfMonth = getFirstDateOfMonth(date);
  8826        return Math.floor((firstDayOfMonth.getDay() + date.getDate() - 1) / 7);
  8827      }
  8828  
  8829      /**
  8830       * Gets a new date incremented by the given number of days. Number of days can be negative.
  8831       * @param {Date} date
  8832       * @param {number} numberOfDays
  8833       * @returns {Date}
  8834       */
  8835      function incrementDays(date, numberOfDays) {
  8836        return new Date(date.getFullYear(), date.getMonth(), date.getDate() + numberOfDays);
  8837      }
  8838  
  8839      /**
  8840       * Gets a new date incremented by the given number of months. Number of months can be negative.
  8841       * If the date of the given month does not match the target month, the date will be set to the
  8842       * last day of the month.
  8843       * @param {Date} date
  8844       * @param {number} numberOfMonths
  8845       * @returns {Date}
  8846       */
  8847      function incrementMonths(date, numberOfMonths) {
  8848        // If the same date in the target month does not actually exist, the Date object will
  8849        // automatically advance *another* month by the number of missing days.
  8850        // For example, if you try to go from Jan. 30 to Feb. 30, you'll end up on March 2.
  8851        // So, we check if the month overflowed and go to the last day of the target month instead.
  8852        var dateInTargetMonth = new Date(date.getFullYear(), date.getMonth() + numberOfMonths, 1);
  8853        var numberOfDaysInMonth = getNumberOfDaysInMonth(dateInTargetMonth);
  8854        if (numberOfDaysInMonth < date.getDate()) {
  8855          dateInTargetMonth.setDate(numberOfDaysInMonth);
  8856        } else {
  8857          dateInTargetMonth.setDate(date.getDate());
  8858        }
  8859  
  8860        return dateInTargetMonth;
  8861      }
  8862  
  8863      /**
  8864       * Get the integer distance between two months. This *only* considers the month and year
  8865       * portion of the Date instances.
  8866       *
  8867       * @param {Date} start
  8868       * @param {Date} end
  8869       * @returns {number} Number of months between `start` and `end`. If `end` is before `start`
  8870       *     chronologically, this number will be negative.
  8871       */
  8872      function getMonthDistance(start, end) {
  8873        return (12 * (end.getFullYear() - start.getFullYear())) + (end.getMonth() - start.getMonth());
  8874      }
  8875  
  8876      /**
  8877       * Gets the last day of the month for the given date.
  8878       * @param {Date} date
  8879       * @returns {Date}
  8880       */
  8881      function getLastDateOfMonth(date) {
  8882        return new Date(date.getFullYear(), date.getMonth(), getNumberOfDaysInMonth(date));
  8883      }
  8884  
  8885      /**
  8886       * Checks whether a date is valid.
  8887       * @param {Date} date
  8888       * @return {boolean} Whether the date is a valid Date.
  8889       */
  8890      function isValidDate(date) {
  8891        return date != null && date.getTime && !isNaN(date.getTime());
  8892      }
  8893  
  8894      /**
  8895       * Sets a date's time to midnight.
  8896       * @param {Date} date
  8897       */
  8898      function setDateTimeToMidnight(date) {
  8899        if (isValidDate(date)) {
  8900          date.setHours(0, 0, 0, 0);
  8901        }
  8902      }
  8903  
  8904      /**
  8905       * Creates a date with the time set to midnight.
  8906       * Drop-in replacement for two forms of the Date constructor:
  8907       * 1. No argument for Date representing now.
  8908       * 2. Single-argument value representing number of seconds since Unix Epoch
  8909       * or a Date object.
  8910       * @param {number|Date=} opt_value
  8911       * @return {Date} New date with time set to midnight.
  8912       */
  8913      function createDateAtMidnight(opt_value) {
  8914        var date;
  8915        if (angular.isUndefined(opt_value)) {
  8916          date = new Date();
  8917        } else {
  8918          date = new Date(opt_value);
  8919        }
  8920        setDateTimeToMidnight(date);
  8921        return date;
  8922      }
  8923  
  8924       /**
  8925        * Checks if a date is within a min and max range, ignoring the time component.
  8926        * If minDate or maxDate are not dates, they are ignored.
  8927        * @param {Date} date
  8928        * @param {Date} minDate
  8929        * @param {Date} maxDate
  8930        */
  8931       function isDateWithinRange(date, minDate, maxDate) {
  8932         var dateAtMidnight = createDateAtMidnight(date);
  8933         var minDateAtMidnight = isValidDate(minDate) ? createDateAtMidnight(minDate) : null;
  8934         var maxDateAtMidnight = isValidDate(maxDate) ? createDateAtMidnight(maxDate) : null;
  8935         return (!minDateAtMidnight || minDateAtMidnight <= dateAtMidnight) &&
  8936             (!maxDateAtMidnight || maxDateAtMidnight >= dateAtMidnight);
  8937       }
  8938    });
  8939  })();
  8940  
  8941  })();
  8942  (function(){
  8943  "use strict";
  8944  
  8945  /**
  8946   * @ngdoc module
  8947   * @name material.components.dialog
  8948   */
  8949  angular
  8950    .module('material.components.dialog', [
  8951      'material.core',
  8952      'material.components.backdrop'
  8953    ])
  8954    .directive('mdDialog', MdDialogDirective)
  8955    .provider('$mdDialog', MdDialogProvider);
  8956  
  8957  /**
  8958   * @ngdoc directive
  8959   * @name mdDialog
  8960   * @module material.components.dialog
  8961   *
  8962   * @restrict E
  8963   *
  8964   * @description
  8965   * `<md-dialog>` - The dialog's template must be inside this element.
  8966   *
  8967   * Inside, use an `<md-dialog-content>` element for the dialog's content, and use
  8968   * an `<md-dialog-actions>` element for the dialog's actions.
  8969   *
  8970   * ## CSS
  8971   * - `.md-dialog-content` - class that sets the padding on the content as the spec file
  8972   *
  8973   * ## Notes
  8974   * - If you specify an `id` for the `<md-dialog>`, the `<md-dialog-content>` will have the same `id`
  8975   * prefixed with `dialogContent_`.
  8976   *
  8977   * @usage
  8978   * ### Dialog template
  8979   * <hljs lang="html">
  8980   * <md-dialog aria-label="List dialog">
  8981   *   <md-dialog-content>
  8982   *     <md-list>
  8983   *       <md-list-item ng-repeat="item in items">
  8984   *         <p>Number {{item}}</p>
  8985   *       </md-list-item>
  8986   *     </md-list>
  8987   *   </md-dialog-content>
  8988   *   <md-dialog-actions>
  8989   *     <md-button ng-click="closeDialog()" class="md-primary">Close Dialog</md-button>
  8990   *   </md-dialog-actions>
  8991   * </md-dialog>
  8992   * </hljs>
  8993   */
  8994  function MdDialogDirective($$rAF, $mdTheming, $mdDialog) {
  8995    return {
  8996      restrict: 'E',
  8997      link: function(scope, element, attr) {
  8998        $mdTheming(element);
  8999        $$rAF(function() {
  9000          var images;
  9001          var content = element[0].querySelector('md-dialog-content');
  9002  
  9003          if (content) {
  9004            images = content.getElementsByTagName('img');
  9005            addOverflowClass();
  9006            //-- delayed image loading may impact scroll height, check after images are loaded
  9007            angular.element(images).on('load', addOverflowClass);
  9008          }
  9009  
  9010          scope.$on('$destroy', function() {
  9011            $mdDialog.destroy(element);
  9012          });
  9013  
  9014          /**
  9015           *
  9016           */
  9017          function addOverflowClass() {
  9018            element.toggleClass('md-content-overflow', content.scrollHeight > content.clientHeight);
  9019          }
  9020  
  9021  
  9022        });
  9023      }
  9024    };
  9025  }
  9026  MdDialogDirective.$inject = ["$$rAF", "$mdTheming", "$mdDialog"];
  9027  
  9028  /**
  9029   * @ngdoc service
  9030   * @name $mdDialog
  9031   * @module material.components.dialog
  9032   *
  9033   * @description
  9034   * `$mdDialog` opens a dialog over the app to inform users about critical information or require
  9035   *  them to make decisions. There are two approaches for setup: a simple promise API
  9036   *  and regular object syntax.
  9037   *
  9038   * ## Restrictions
  9039   *
  9040   * - The dialog is always given an isolate scope.
  9041   * - The dialog's template must have an outer `<md-dialog>` element.
  9042   *   Inside, use an `<md-dialog-content>` element for the dialog's content, and use
  9043   *   an `<md-dialog-actions>` element for the dialog's actions.
  9044   * - Dialogs must cover the entire application to keep interactions inside of them.
  9045   * Use the `parent` option to change where dialogs are appended.
  9046   *
  9047   * ## Sizing
  9048   * - Complex dialogs can be sized with `flex="percentage"`, i.e. `flex="66"`.
  9049   * - Default max-width is 80% of the `rootElement` or `parent`.
  9050   *
  9051   * ## CSS
  9052   * - `.md-dialog-content` - class that sets the padding on the content as the spec file
  9053   *
  9054   * @usage
  9055   * <hljs lang="html">
  9056   * <div  ng-app="demoApp" ng-controller="EmployeeController">
  9057   *   <div>
  9058   *     <md-button ng-click="showAlert()" class="md-raised md-warn">
  9059   *       Employee Alert!
  9060   *       </md-button>
  9061   *   </div>
  9062   *   <div>
  9063   *     <md-button ng-click="showDialog($event)" class="md-raised">
  9064   *       Custom Dialog
  9065   *       </md-button>
  9066   *   </div>
  9067   *   <div>
  9068   *     <md-button ng-click="closeAlert()" ng-disabled="!hasAlert()" class="md-raised">
  9069   *       Close Alert
  9070   *     </md-button>
  9071   *   </div>
  9072   *   <div>
  9073   *     <md-button ng-click="showGreeting($event)" class="md-raised md-primary" >
  9074   *       Greet Employee
  9075   *       </md-button>
  9076   *   </div>
  9077   * </div>
  9078   * </hljs>
  9079   *
  9080   * ### JavaScript: object syntax
  9081   * <hljs lang="js">
  9082   * (function(angular, undefined){
  9083   *   "use strict";
  9084   *
  9085   *   angular
  9086   *    .module('demoApp', ['ngMaterial'])
  9087   *    .controller('AppCtrl', AppController);
  9088   *
  9089   *   function AppController($scope, $mdDialog) {
  9090   *     var alert;
  9091   *     $scope.showAlert = showAlert;
  9092   *     $scope.showDialog = showDialog;
  9093   *     $scope.items = [1, 2, 3];
  9094   *
  9095   *     // Internal method
  9096   *     function showAlert() {
  9097   *       alert = $mdDialog.alert({
  9098   *         title: 'Attention',
  9099   *         textContent: 'This is an example of how easy dialogs can be!',
  9100   *         ok: 'Close'
  9101   *       });
  9102   *
  9103   *       $mdDialog
  9104   *         .show( alert )
  9105   *         .finally(function() {
  9106   *           alert = undefined;
  9107   *         });
  9108   *     }
  9109   *
  9110   *     function showDialog($event) {
  9111   *        var parentEl = angular.element(document.body);
  9112   *        $mdDialog.show({
  9113   *          parent: parentEl,
  9114   *          targetEvent: $event,
  9115   *          template:
  9116   *            '<md-dialog aria-label="List dialog">' +
  9117   *            '  <md-dialog-content>'+
  9118   *            '    <md-list>'+
  9119   *            '      <md-list-item ng-repeat="item in items">'+
  9120   *            '       <p>Number {{item}}</p>' +
  9121   *            '      </md-item>'+
  9122   *            '    </md-list>'+
  9123   *            '  </md-dialog-content>' +
  9124   *            '  <md-dialog-actions>' +
  9125   *            '    <md-button ng-click="closeDialog()" class="md-primary">' +
  9126   *            '      Close Dialog' +
  9127   *            '    </md-button>' +
  9128   *            '  </md-dialog-actions>' +
  9129   *            '</md-dialog>',
  9130   *          locals: {
  9131   *            items: $scope.items
  9132   *          },
  9133   *          controller: DialogController
  9134   *       });
  9135   *       function DialogController($scope, $mdDialog, items) {
  9136   *         $scope.items = items;
  9137   *         $scope.closeDialog = function() {
  9138   *           $mdDialog.hide();
  9139   *         }
  9140   *       }
  9141   *     }
  9142   *   }
  9143   * })(angular);
  9144   * </hljs>
  9145   *
  9146   * ### JavaScript: promise API syntax, custom dialog template
  9147   * <hljs lang="js">
  9148   * (function(angular, undefined){
  9149   *   "use strict";
  9150   *
  9151   *   angular
  9152   *     .module('demoApp', ['ngMaterial'])
  9153   *     .controller('EmployeeController', EmployeeEditor)
  9154   *     .controller('GreetingController', GreetingController);
  9155   *
  9156   *   // Fictitious Employee Editor to show how to use simple and complex dialogs.
  9157   *
  9158   *   function EmployeeEditor($scope, $mdDialog) {
  9159   *     var alert;
  9160   *
  9161   *     $scope.showAlert = showAlert;
  9162   *     $scope.closeAlert = closeAlert;
  9163   *     $scope.showGreeting = showCustomGreeting;
  9164   *
  9165   *     $scope.hasAlert = function() { return !!alert };
  9166   *     $scope.userName = $scope.userName || 'Bobby';
  9167   *
  9168   *     // Dialog #1 - Show simple alert dialog and cache
  9169   *     // reference to dialog instance
  9170   *
  9171   *     function showAlert() {
  9172   *       alert = $mdDialog.alert()
  9173   *         .title('Attention, ' + $scope.userName)
  9174   *         .textContent('This is an example of how easy dialogs can be!')
  9175   *         .ok('Close');
  9176   *
  9177   *       $mdDialog
  9178   *           .show( alert )
  9179   *           .finally(function() {
  9180   *             alert = undefined;
  9181   *           });
  9182   *     }
  9183   *
  9184   *     // Close the specified dialog instance and resolve with 'finished' flag
  9185   *     // Normally this is not needed, just use '$mdDialog.hide()' to close
  9186   *     // the most recent dialog popup.
  9187   *
  9188   *     function closeAlert() {
  9189   *       $mdDialog.hide( alert, "finished" );
  9190   *       alert = undefined;
  9191   *     }
  9192   *
  9193   *     // Dialog #2 - Demonstrate more complex dialogs construction and popup.
  9194   *
  9195   *     function showCustomGreeting($event) {
  9196   *         $mdDialog.show({
  9197   *           targetEvent: $event,
  9198   *           template:
  9199   *             '<md-dialog>' +
  9200   *
  9201   *             '  <md-dialog-content>Hello {{ employee }}!</md-dialog-content>' +
  9202   *
  9203   *             '  <md-dialog-actions>' +
  9204   *             '    <md-button ng-click="closeDialog()" class="md-primary">' +
  9205   *             '      Close Greeting' +
  9206   *             '    </md-button>' +
  9207   *             '  </md-dialog-actions>' +
  9208   *             '</md-dialog>',
  9209   *           controller: 'GreetingController',
  9210   *           onComplete: afterShowAnimation,
  9211   *           locals: { employee: $scope.userName }
  9212   *         });
  9213   *
  9214   *         // When the 'enter' animation finishes...
  9215   *
  9216   *         function afterShowAnimation(scope, element, options) {
  9217   *            // post-show code here: DOM element focus, etc.
  9218   *         }
  9219   *     }
  9220   *
  9221   *     // Dialog #3 - Demonstrate use of ControllerAs and passing $scope to dialog
  9222   *     //             Here we used ng-controller="GreetingController as vm" and
  9223   *     //             $scope.vm === <controller instance>
  9224   *
  9225   *     function showCustomGreeting() {
  9226   *
  9227   *        $mdDialog.show({
  9228   *           clickOutsideToClose: true,
  9229   *
  9230   *           scope: $scope,        // use parent scope in template
  9231   *           preserveScope: true,  // do not forget this if use parent scope
  9232  
  9233   *           // Since GreetingController is instantiated with ControllerAs syntax
  9234   *           // AND we are passing the parent '$scope' to the dialog, we MUST
  9235   *           // use 'vm.<xxx>' in the template markup
  9236   *
  9237   *           template: '<md-dialog>' +
  9238   *                     '  <md-dialog-content>' +
  9239   *                     '     Hi There {{vm.employee}}' +
  9240   *                     '  </md-dialog-content>' +
  9241   *                     '</md-dialog>',
  9242   *
  9243   *           controller: function DialogController($scope, $mdDialog) {
  9244   *             $scope.closeDialog = function() {
  9245   *               $mdDialog.hide();
  9246   *             }
  9247   *           }
  9248   *        });
  9249   *     }
  9250   *
  9251   *   }
  9252   *
  9253   *   // Greeting controller used with the more complex 'showCustomGreeting()' custom dialog
  9254   *
  9255   *   function GreetingController($scope, $mdDialog, employee) {
  9256   *     // Assigned from construction <code>locals</code> options...
  9257   *     $scope.employee = employee;
  9258   *
  9259   *     $scope.closeDialog = function() {
  9260   *       // Easily hides most recent dialog shown...
  9261   *       // no specific instance reference is needed.
  9262   *       $mdDialog.hide();
  9263   *     };
  9264   *   }
  9265   *
  9266   * })(angular);
  9267   * </hljs>
  9268   */
  9269  
  9270  /**
  9271   * @ngdoc method
  9272   * @name $mdDialog#alert
  9273   *
  9274   * @description
  9275   * Builds a preconfigured dialog with the specified message.
  9276   *
  9277   * @returns {obj} an `$mdDialogPreset` with the chainable configuration methods:
  9278   *
  9279   * - $mdDialogPreset#title(string) - Sets the alert title.
  9280   * - $mdDialogPreset#textContent(string) - Sets the alert message.
  9281   * - $mdDialogPreset#htmlContent(string) - Sets the alert message as HTML. Requires ngSanitize
  9282   *     module to be loaded. HTML is not run through Angular's compiler.
  9283   * - $mdDialogPreset#ok(string) - Sets the alert "Okay" button text.
  9284   * - $mdDialogPreset#theme(string) - Sets the theme of the alert dialog.
  9285   * - $mdDialogPreset#targetEvent(DOMClickEvent=) - A click's event object. When passed in as an option,
  9286   *     the location of the click will be used as the starting point for the opening animation
  9287   *     of the the dialog.
  9288   *
  9289   */
  9290  
  9291  /**
  9292   * @ngdoc method
  9293   * @name $mdDialog#confirm
  9294   *
  9295   * @description
  9296   * Builds a preconfigured dialog with the specified message. You can call show and the promise returned
  9297   * will be resolved only if the user clicks the confirm action on the dialog.
  9298   *
  9299   * @returns {obj} an `$mdDialogPreset` with the chainable configuration methods:
  9300   *
  9301   * Additionally, it supports the following methods:
  9302   *
  9303   * - $mdDialogPreset#title(string) - Sets the confirm title.
  9304   * - $mdDialogPreset#textContent(string) - Sets the confirm message.
  9305   * - $mdDialogPreset#htmlContent(string) - Sets the confirm message as HTML. Requires ngSanitize
  9306   *     module to be loaded. HTML is not run through Angular's compiler.
  9307   * - $mdDialogPreset#ok(string) - Sets the confirm "Okay" button text.
  9308   * - $mdDialogPreset#cancel(string) - Sets the confirm "Cancel" button text.
  9309   * - $mdDialogPreset#theme(string) - Sets the theme of the confirm dialog.
  9310   * - $mdDialogPreset#targetEvent(DOMClickEvent=) - A click's event object. When passed in as an option,
  9311   *     the location of the click will be used as the starting point for the opening animation
  9312   *     of the the dialog.
  9313   *
  9314   */
  9315  
  9316  /**
  9317   * @ngdoc method
  9318   * @name $mdDialog#prompt
  9319   *
  9320   * @description
  9321   * Builds a preconfigured dialog with the specified message and input box. You can call show and the promise returned
  9322   * will be resolved only if the user clicks the prompt action on the dialog, passing the input value as the first argument.
  9323   *
  9324   * @returns {obj} an `$mdDialogPreset` with the chainable configuration methods:
  9325   *
  9326   * Additionally, it supports the following methods:
  9327   *
  9328   * - $mdDialogPreset#title(string) - Sets the prompt title.
  9329   * - $mdDialogPreset#textContent(string) - Sets the prompt message.
  9330   * - $mdDialogPreset#htmlContent(string) - Sets the prompt message as HTML. Requires ngSanitize
  9331   *     module to be loaded. HTML is not run through Angular's compiler.
  9332   * - $mdDialogPreset#placeholder(string) - Sets the placeholder text for the input.
  9333   * - $mdDialogPreset#ok(string) - Sets the prompt "Okay" button text.
  9334   * - $mdDialogPreset#cancel(string) - Sets the prompt "Cancel" button text.
  9335   * - $mdDialogPreset#theme(string) - Sets the theme of the prompt dialog.
  9336   * - $mdDialogPreset#targetEvent(DOMClickEvent=) - A click's event object. When passed in as an option,
  9337   *     the location of the click will be used as the starting point for the opening animation
  9338   *     of the the dialog.
  9339   *
  9340   */
  9341  
  9342  /**
  9343   * @ngdoc method
  9344   * @name $mdDialog#show
  9345   *
  9346   * @description
  9347   * Show a dialog with the specified options.
  9348   *
  9349   * @param {object} optionsOrPreset Either provide an `$mdDialogPreset` returned from `alert()`, and
  9350   * `confirm()`, or an options object with the following properties:
  9351   *   - `templateUrl` - `{string=}`: The url of a template that will be used as the content
  9352   *   of the dialog.
  9353   *   - `template` - `{string=}`: HTML template to show in the dialog. This **must** be trusted HTML
  9354   *      with respect to Angular's [$sce service](https://docs.angularjs.org/api/ng/service/$sce).
  9355   *      This template should **never** be constructed with any kind of user input or user data.
  9356   *   - `autoWrap` - `{boolean=}`: Whether or not to automatically wrap the template with a
  9357   *     `<md-dialog>` tag if one is not provided. Defaults to true. Can be disabled if you provide a
  9358   *     custom dialog directive.
  9359   *   - `targetEvent` - `{DOMClickEvent=}`: A click's event object. When passed in as an option,
  9360   *     the location of the click will be used as the starting point for the opening animation
  9361   *     of the the dialog.
  9362   *   - `openFrom` - `{string|Element|object}`: The query selector, DOM element or the Rect object
  9363   *     that is used to determine the bounds (top, left, height, width) from which the Dialog will
  9364   *     originate.
  9365   *   - `closeTo` - `{string|Element|object}`: The query selector, DOM element or the Rect object
  9366   *     that is used to determine the bounds (top, left, height, width) to which the Dialog will
  9367   *     target.
  9368   *   - `scope` - `{object=}`: the scope to link the template / controller to. If none is specified,
  9369   *     it will create a new isolate scope.
  9370   *     This scope will be destroyed when the dialog is removed unless `preserveScope` is set to true.
  9371   *   - `preserveScope` - `{boolean=}`: whether to preserve the scope when the element is removed. Default is false
  9372   *   - `disableParentScroll` - `{boolean=}`: Whether to disable scrolling while the dialog is open.
  9373   *     Default true.
  9374   *   - `hasBackdrop` - `{boolean=}`: Whether there should be an opaque backdrop behind the dialog.
  9375   *     Default true.
  9376   *   - `clickOutsideToClose` - `{boolean=}`: Whether the user can click outside the dialog to
  9377   *     close it. Default false.
  9378   *   - `escapeToClose` - `{boolean=}`: Whether the user can press escape to close the dialog.
  9379   *     Default true.
  9380   *   - `focusOnOpen` - `{boolean=}`: An option to override focus behavior on open. Only disable if
  9381   *     focusing some other way, as focus management is required for dialogs to be accessible.
  9382   *     Defaults to true.
  9383   *   - `controller` - `{function|string=}`: The controller to associate with the dialog. The controller
  9384   *     will be injected with the local `$mdDialog`, which passes along a scope for the dialog.
  9385   *   - `locals` - `{object=}`: An object containing key/value pairs. The keys will be used as names
  9386   *     of values to inject into the controller. For example, `locals: {three: 3}` would inject
  9387   *     `three` into the controller, with the value 3. If `bindToController` is true, they will be
  9388   *     copied to the controller instead.
  9389   *   - `bindToController` - `bool`: bind the locals to the controller, instead of passing them in.
  9390   *   - `resolve` - `{object=}`: Similar to locals, except it takes promises as values, and the
  9391   *     dialog will not open until all of the promises resolve.
  9392   *   - `controllerAs` - `{string=}`: An alias to assign the controller to on the scope.
  9393   *   - `parent` - `{element=}`: The element to append the dialog to. Defaults to appending
  9394   *     to the root element of the application.
  9395   *   - `onShowing` `{function=} Callback function used to announce the show() action is
  9396   *     starting.
  9397   *   - `onComplete` `{function=}`: Callback function used to announce when the show() action is
  9398   *     finished.
  9399   *   - `onRemoving` `{function=}`: Callback function used to announce the close/hide() action is
  9400   *     starting. This allows developers to run custom animations in parallel the close animations.
  9401   *   - `fullscreen` `{boolean=}`: An option to apply `.md-dialog-fullscreen` class on open.
  9402   * @returns {promise} A promise that can be resolved with `$mdDialog.hide()` or
  9403   * rejected with `$mdDialog.cancel()`.
  9404   */
  9405  
  9406  /**
  9407   * @ngdoc method
  9408   * @name $mdDialog#hide
  9409   *
  9410   * @description
  9411   * Hide an existing dialog and resolve the promise returned from `$mdDialog.show()`.
  9412   *
  9413   * @param {*=} response An argument for the resolved promise.
  9414   *
  9415   * @returns {promise} A promise that is resolved when the dialog has been closed.
  9416   */
  9417  
  9418  /**
  9419   * @ngdoc method
  9420   * @name $mdDialog#cancel
  9421   *
  9422   * @description
  9423   * Hide an existing dialog and reject the promise returned from `$mdDialog.show()`.
  9424   *
  9425   * @param {*=} response An argument for the rejected promise.
  9426   *
  9427   * @returns {promise} A promise that is resolved when the dialog has been closed.
  9428   */
  9429  
  9430  function MdDialogProvider($$interimElementProvider) {
  9431    // Elements to capture and redirect focus when the user presses tab at the dialog boundary.
  9432    var topFocusTrap, bottomFocusTrap;
  9433  
  9434    advancedDialogOptions.$inject = ["$mdDialog", "$mdTheming", "$mdConstant"];
  9435    dialogDefaultOptions.$inject = ["$mdDialog", "$mdAria", "$mdUtil", "$mdConstant", "$animate", "$document", "$window", "$rootElement", "$log", "$injector"];
  9436    return $$interimElementProvider('$mdDialog')
  9437      .setDefaults({
  9438        methods: ['disableParentScroll', 'hasBackdrop', 'clickOutsideToClose', 'escapeToClose',
  9439            'targetEvent', 'closeTo', 'openFrom', 'parent', 'fullscreen'],
  9440        options: dialogDefaultOptions
  9441      })
  9442      .addPreset('alert', {
  9443        methods: ['title', 'htmlContent', 'textContent', 'content', 'ariaLabel', 'ok', 'theme',
  9444            'css'],
  9445        options: advancedDialogOptions
  9446      })
  9447      .addPreset('confirm', {
  9448        methods: ['title', 'htmlContent', 'textContent', 'content', 'ariaLabel', 'ok', 'cancel',
  9449            'theme', 'css'],
  9450        options: advancedDialogOptions
  9451      })
  9452      .addPreset('prompt', {
  9453        methods: ['title', 'htmlContent', 'textContent', 'content', 'placeholder', 'ariaLabel',
  9454            'ok', 'cancel', 'theme', 'css'],
  9455        options: advancedDialogOptions
  9456      });
  9457  
  9458    /* @ngInject */
  9459    function advancedDialogOptions($mdDialog, $mdTheming, $mdConstant) {
  9460      return {
  9461        template: [
  9462          '<md-dialog md-theme="{{ dialog.theme }}" aria-label="{{ dialog.ariaLabel }}" ng-class="dialog.css">',
  9463          '  <md-dialog-content class="md-dialog-content" role="document" tabIndex="-1">',
  9464          '    <h2 class="md-title">{{ dialog.title }}</h2>',
  9465          '    <div ng-if="::dialog.mdHtmlContent" class="md-dialog-content-body" ',
  9466          '        ng-bind-html="::dialog.mdHtmlContent"></div>',
  9467          '    <div ng-if="::!dialog.mdHtmlContent" class="md-dialog-content-body">',
  9468          '      <p>{{::dialog.mdTextContent}}</p>',
  9469          '    </div>',
  9470          '    <md-input-container md-no-float ng-if="::dialog.$type == \'prompt\'" class="md-prompt-input-container">',
  9471          '      <input ng-keypress="dialog.keypress($event)" md-autofocus ng-model="dialog.result" placeholder="{{::dialog.placeholder}}">',
  9472          '    </md-input-container>',
  9473          '  </md-dialog-content>',
  9474          '  <md-dialog-actions>',
  9475          '    <md-button ng-if="dialog.$type === \'confirm\' || dialog.$type === \'prompt\'"' +
  9476          '               ng-click="dialog.abort()" class="md-primary">',
  9477          '      {{ dialog.cancel }}',
  9478          '    </md-button>',
  9479          '    <md-button ng-click="dialog.hide()" class="md-primary" md-autofocus="dialog.$type===\'alert\'">',
  9480          '      {{ dialog.ok }}',
  9481          '    </md-button>',
  9482          '  </md-dialog-actions>',
  9483          '</md-dialog>'
  9484        ].join('').replace(/\s\s+/g, ''),
  9485        controller: function mdDialogCtrl() {
  9486          this.hide = function() {
  9487            $mdDialog.hide(this.$type === 'prompt' ? this.result : true);
  9488          };
  9489          this.abort = function() {
  9490            $mdDialog.cancel();
  9491          };
  9492          this.keypress = function($event) {
  9493            if ($event.keyCode === $mdConstant.KEY_CODE.ENTER) {
  9494              $mdDialog.hide(this.result)
  9495            }
  9496          }
  9497        },
  9498        controllerAs: 'dialog',
  9499        bindToController: true,
  9500        theme: $mdTheming.defaultTheme()
  9501      };
  9502    }
  9503  
  9504    /* @ngInject */
  9505    function dialogDefaultOptions($mdDialog, $mdAria, $mdUtil, $mdConstant, $animate, $document, $window, $rootElement, $log, $injector) {
  9506      return {
  9507        hasBackdrop: true,
  9508        isolateScope: true,
  9509        onShow: onShow,
  9510        onShowing: beforeShow,
  9511        onRemove: onRemove,
  9512        clickOutsideToClose: false,
  9513        escapeToClose: true,
  9514        targetEvent: null,
  9515        closeTo: null,
  9516        openFrom: null,
  9517        focusOnOpen: true,
  9518        disableParentScroll: true,
  9519        autoWrap: true,
  9520        fullscreen: false,
  9521        transformTemplate: function(template, options) {
  9522          // Make the dialog container focusable, because otherwise the focus will be always redirected to
  9523          // an element outside of the container, and the focus trap won't work probably..
  9524          // Also the tabindex is needed for the `escapeToClose` functionality, because
  9525          // the keyDown event can't be triggered when the focus is outside of the container.
  9526          return '<div class="md-dialog-container" tabindex="-1">' + validatedTemplate(template) + '</div>';
  9527  
  9528          /**
  9529           * The specified template should contain a <md-dialog> wrapper element....
  9530           */
  9531          function validatedTemplate(template) {
  9532            if (options.autoWrap && !/<\/md-dialog>/g.test(template)) {
  9533              return '<md-dialog>' + (template || '') + '</md-dialog>';
  9534            } else {
  9535              return template || '';
  9536            }
  9537          }
  9538        }
  9539      };
  9540  
  9541      function beforeShow(scope, element, options, controller) {
  9542        if (controller) {
  9543          controller.mdHtmlContent = controller.htmlContent || options.htmlContent || '';
  9544          controller.mdTextContent = controller.textContent || options.textContent ||
  9545              controller.content || options.content || '';
  9546  
  9547          if (controller.mdHtmlContent && !$injector.has('$sanitize')) {
  9548            throw Error('The ngSanitize module must be loaded in order to use htmlContent.');
  9549          }
  9550  
  9551          if (controller.mdHtmlContent && controller.mdTextContent) {
  9552            throw Error('md-dialog cannot have both `htmlContent` and `textContent`');
  9553          }
  9554        }
  9555      }
  9556  
  9557      /** Show method for dialogs */
  9558      function onShow(scope, element, options, controller) {
  9559        angular.element($document[0].body).addClass('md-dialog-is-showing');
  9560  
  9561        captureParentAndFromToElements(options);
  9562        configureAria(element.find('md-dialog'), options);
  9563        showBackdrop(scope, element, options);
  9564  
  9565        return dialogPopIn(element, options)
  9566          .then(function() {
  9567            activateListeners(element, options);
  9568            lockScreenReader(element, options);
  9569            warnDeprecatedActions();
  9570            focusOnOpen();
  9571          });
  9572  
  9573        /**
  9574         * Check to see if they used the deprecated .md-actions class and log a warning
  9575         */
  9576        function warnDeprecatedActions() {
  9577          var badActions = element[0].querySelectorAll('.md-actions');
  9578  
  9579          if (badActions.length > 0) {
  9580            $log.warn('Using a class of md-actions is deprecated, please use <md-dialog-actions>.');
  9581          }
  9582        }
  9583  
  9584        /**
  9585         * For alerts, focus on content... otherwise focus on
  9586         * the close button (or equivalent)
  9587         */
  9588        function focusOnOpen() {
  9589          if (options.focusOnOpen) {
  9590            var target = $mdUtil.findFocusTarget(element) || findCloseButton();
  9591            target.focus();
  9592          }
  9593  
  9594          /**
  9595           * If no element with class dialog-close, try to find the last
  9596           * button child in md-actions and assume it is a close button.
  9597           *
  9598           * If we find no actions at all, log a warning to the console.
  9599           */
  9600          function findCloseButton() {
  9601            var closeButton = element[0].querySelector('.dialog-close');
  9602            if (!closeButton) {
  9603              var actionButtons = element[0].querySelectorAll('.md-actions button, md-dialog-actions button');
  9604              closeButton = actionButtons[actionButtons.length - 1];
  9605            }
  9606            return angular.element(closeButton);
  9607          }
  9608        }
  9609      }
  9610  
  9611      /**
  9612       * Remove function for all dialogs
  9613       */
  9614      function onRemove(scope, element, options) {
  9615        options.deactivateListeners();
  9616        options.unlockScreenReader();
  9617        options.hideBackdrop(options.$destroy);
  9618  
  9619        // Remove the focus traps that we added earlier for keeping focus within the dialog.
  9620        if (topFocusTrap && topFocusTrap.parentNode) {
  9621          topFocusTrap.parentNode.removeChild(topFocusTrap);
  9622        }
  9623  
  9624        if (bottomFocusTrap && bottomFocusTrap.parentNode) {
  9625          bottomFocusTrap.parentNode.removeChild(bottomFocusTrap);
  9626        }
  9627  
  9628        // For navigation $destroy events, do a quick, non-animated removal,
  9629        // but for normal closes (from clicks, etc) animate the removal
  9630        return !!options.$destroy ? detachAndClean() : animateRemoval().then( detachAndClean );
  9631  
  9632        /**
  9633         * For normal closes, animate the removal.
  9634         * For forced closes (like $destroy events), skip the animations
  9635         */
  9636        function animateRemoval() {
  9637          return dialogPopOut(element, options);
  9638        }
  9639  
  9640        /**
  9641         * Detach the element
  9642         */
  9643        function detachAndClean() {
  9644          angular.element($document[0].body).removeClass('md-dialog-is-showing');
  9645          element.remove();
  9646  
  9647          if (!options.$destroy) options.origin.focus();
  9648        }
  9649      }
  9650  
  9651      /**
  9652       * Capture originator/trigger/from/to element information (if available)
  9653       * and the parent container for the dialog; defaults to the $rootElement
  9654       * unless overridden in the options.parent
  9655       */
  9656      function captureParentAndFromToElements(options) {
  9657            options.origin = angular.extend({
  9658              element: null,
  9659              bounds: null,
  9660              focus: angular.noop
  9661            }, options.origin || {});
  9662  
  9663            options.parent   = getDomElement(options.parent, $rootElement);
  9664            options.closeTo  = getBoundingClientRect(getDomElement(options.closeTo));
  9665            options.openFrom = getBoundingClientRect(getDomElement(options.openFrom));
  9666  
  9667            if ( options.targetEvent ) {
  9668              options.origin   = getBoundingClientRect(options.targetEvent.target, options.origin);
  9669            }
  9670  
  9671            /**
  9672             * Identify the bounding RECT for the target element
  9673             *
  9674             */
  9675            function getBoundingClientRect (element, orig) {
  9676              var source = angular.element((element || {}));
  9677              if (source && source.length) {
  9678                // Compute and save the target element's bounding rect, so that if the
  9679                // element is hidden when the dialog closes, we can shrink the dialog
  9680                // back to the same position it expanded from.
  9681                //
  9682                // Checking if the source is a rect object or a DOM element
  9683                var bounds = {top:0,left:0,height:0,width:0};
  9684                var hasFn = angular.isFunction(source[0].getBoundingClientRect);
  9685  
  9686                return angular.extend(orig || {}, {
  9687                    element : hasFn ? source : undefined,
  9688                    bounds  : hasFn ? source[0].getBoundingClientRect() : angular.extend({}, bounds, source[0]),
  9689                    focus   : angular.bind(source, source.focus),
  9690                });
  9691              }
  9692            }
  9693  
  9694            /**
  9695             * If the specifier is a simple string selector, then query for
  9696             * the DOM element.
  9697             */
  9698            function getDomElement(element, defaultElement) {
  9699              if (angular.isString(element)) {
  9700                var simpleSelector = element,
  9701                  container = $document[0].querySelectorAll(simpleSelector);
  9702                  element = container.length ? container[0] : null;
  9703              }
  9704  
  9705              // If we have a reference to a raw dom element, always wrap it in jqLite
  9706              return angular.element(element || defaultElement);
  9707            }
  9708  
  9709          }
  9710  
  9711      /**
  9712       * Listen for escape keys and outside clicks to auto close
  9713       */
  9714      function activateListeners(element, options) {
  9715        var window = angular.element($window);
  9716        var onWindowResize = $mdUtil.debounce(function(){
  9717          stretchDialogContainerToViewport(element, options);
  9718        }, 60);
  9719  
  9720        var removeListeners = [];
  9721        var smartClose = function() {
  9722          // Only 'confirm' dialogs have a cancel button... escape/clickOutside will
  9723          // cancel or fallback to hide.
  9724          var closeFn = ( options.$type == 'alert' ) ? $mdDialog.hide : $mdDialog.cancel;
  9725          $mdUtil.nextTick(closeFn, true);
  9726        };
  9727  
  9728        if (options.escapeToClose) {
  9729          var parentTarget = options.parent;
  9730          var keyHandlerFn = function(ev) {
  9731            if (ev.keyCode === $mdConstant.KEY_CODE.ESCAPE) {
  9732              ev.stopPropagation();
  9733              ev.preventDefault();
  9734  
  9735              smartClose();
  9736            }
  9737          };
  9738  
  9739          // Add keydown listeners
  9740          element.on('keydown', keyHandlerFn);
  9741          parentTarget.on('keydown', keyHandlerFn);
  9742  
  9743          // Queue remove listeners function
  9744          removeListeners.push(function() {
  9745  
  9746            element.off('keydown', keyHandlerFn);
  9747            parentTarget.off('keydown', keyHandlerFn);
  9748  
  9749          });
  9750        }
  9751  
  9752        // Register listener to update dialog on window resize
  9753        window.on('resize', onWindowResize);
  9754  
  9755        removeListeners.push(function() {
  9756          window.off('resize', onWindowResize);
  9757        });
  9758  
  9759        if (options.clickOutsideToClose) {
  9760          var target = element;
  9761          var sourceElem;
  9762  
  9763          // Keep track of the element on which the mouse originally went down
  9764          // so that we can only close the backdrop when the 'click' started on it.
  9765          // A simple 'click' handler does not work,
  9766          // it sets the target object as the element the mouse went down on.
  9767          var mousedownHandler = function(ev) {
  9768            sourceElem = ev.target;
  9769          };
  9770  
  9771          // We check if our original element and the target is the backdrop
  9772          // because if the original was the backdrop and the target was inside the dialog
  9773          // we don't want to dialog to close.
  9774          var mouseupHandler = function(ev) {
  9775            if (sourceElem === target[0] && ev.target === target[0]) {
  9776              ev.stopPropagation();
  9777              ev.preventDefault();
  9778  
  9779              smartClose();
  9780            }
  9781          };
  9782  
  9783          // Add listeners
  9784          target.on('mousedown', mousedownHandler);
  9785          target.on('mouseup', mouseupHandler);
  9786  
  9787          // Queue remove listeners function
  9788          removeListeners.push(function() {
  9789            target.off('mousedown', mousedownHandler);
  9790            target.off('mouseup', mouseupHandler);
  9791          });
  9792        }
  9793  
  9794        // Attach specific `remove` listener handler
  9795        options.deactivateListeners = function() {
  9796          removeListeners.forEach(function(removeFn) {
  9797            removeFn();
  9798          });
  9799          options.deactivateListeners = null;
  9800        };
  9801      }
  9802  
  9803      /**
  9804       * Show modal backdrop element...
  9805       */
  9806      function showBackdrop(scope, element, options) {
  9807  
  9808        if (options.disableParentScroll) {
  9809          // !! DO this before creating the backdrop; since disableScrollAround()
  9810          //    configures the scroll offset; which is used by mdBackDrop postLink()
  9811          options.restoreScroll = $mdUtil.disableScrollAround(element, options.parent);
  9812        }
  9813  
  9814        if (options.hasBackdrop) {
  9815          options.backdrop = $mdUtil.createBackdrop(scope, "md-dialog-backdrop md-opaque");
  9816          $animate.enter(options.backdrop, options.parent);
  9817        }
  9818  
  9819        /**
  9820         * Hide modal backdrop element...
  9821         */
  9822        options.hideBackdrop = function hideBackdrop($destroy) {
  9823          if (options.backdrop) {
  9824            if ( !!$destroy ) options.backdrop.remove();
  9825            else              $animate.leave(options.backdrop);
  9826          }
  9827  
  9828          if (options.disableParentScroll) {
  9829            options.restoreScroll();
  9830            delete options.restoreScroll;
  9831          }
  9832  
  9833          options.hideBackdrop = null;
  9834        }
  9835      }
  9836  
  9837      /**
  9838       * Inject ARIA-specific attributes appropriate for Dialogs
  9839       */
  9840      function configureAria(element, options) {
  9841  
  9842        var role = (options.$type === 'alert') ? 'alertdialog' : 'dialog';
  9843        var dialogContent = element.find('md-dialog-content');
  9844        var dialogContentId = 'dialogContent_' + (element.attr('id') || $mdUtil.nextUid());
  9845  
  9846        element.attr({
  9847          'role': role,
  9848          'tabIndex': '-1'
  9849        });
  9850  
  9851        if (dialogContent.length === 0) {
  9852          dialogContent = element;
  9853        }
  9854  
  9855        dialogContent.attr('id', dialogContentId);
  9856        element.attr('aria-describedby', dialogContentId);
  9857  
  9858        if (options.ariaLabel) {
  9859          $mdAria.expect(element, 'aria-label', options.ariaLabel);
  9860        }
  9861        else {
  9862          $mdAria.expectAsync(element, 'aria-label', function() {
  9863            var words = dialogContent.text().split(/\s+/);
  9864            if (words.length > 3) words = words.slice(0, 3).concat('...');
  9865            return words.join(' ');
  9866          });
  9867        }
  9868  
  9869        // Set up elements before and after the dialog content to capture focus and
  9870        // redirect back into the dialog.
  9871        topFocusTrap = document.createElement('div');
  9872        topFocusTrap.classList.add('md-dialog-focus-trap');
  9873        topFocusTrap.tabIndex = 0;
  9874  
  9875        bottomFocusTrap = topFocusTrap.cloneNode(false);
  9876  
  9877        // When focus is about to move out of the dialog, we want to intercept it and redirect it
  9878        // back to the dialog element.
  9879        var focusHandler = function() {
  9880          element.focus();
  9881        };
  9882        topFocusTrap.addEventListener('focus', focusHandler);
  9883        bottomFocusTrap.addEventListener('focus', focusHandler);
  9884  
  9885        // The top focus trap inserted immeidately before the md-dialog element (as a sibling).
  9886        // The bottom focus trap is inserted at the very end of the md-dialog element (as a child).
  9887        element[0].parentNode.insertBefore(topFocusTrap, element[0]);
  9888        element.after(bottomFocusTrap);
  9889      }
  9890  
  9891      /**
  9892       * Prevents screen reader interaction behind modal window
  9893       * on swipe interfaces
  9894       */
  9895      function lockScreenReader(element, options) {
  9896        var isHidden = true;
  9897  
  9898        // get raw DOM node
  9899        walkDOM(element[0]);
  9900  
  9901        options.unlockScreenReader = function() {
  9902          isHidden = false;
  9903          walkDOM(element[0]);
  9904  
  9905          options.unlockScreenReader = null;
  9906        };
  9907  
  9908        /**
  9909         * Walk DOM to apply or remove aria-hidden on sibling nodes
  9910         * and parent sibling nodes
  9911         *
  9912         */
  9913        function walkDOM(element) {
  9914          while (element.parentNode) {
  9915            if (element === document.body) {
  9916              return;
  9917            }
  9918            var children = element.parentNode.children;
  9919            for (var i = 0; i < children.length; i++) {
  9920              // skip over child if it is an ascendant of the dialog
  9921              // or a script or style tag
  9922              if (element !== children[i] && !isNodeOneOf(children[i], ['SCRIPT', 'STYLE'])) {
  9923                children[i].setAttribute('aria-hidden', isHidden);
  9924              }
  9925            }
  9926  
  9927            walkDOM(element = element.parentNode);
  9928          }
  9929        }
  9930      }
  9931  
  9932      /**
  9933       * Ensure the dialog container fill-stretches to the viewport
  9934       */
  9935      function stretchDialogContainerToViewport(container, options) {
  9936        var isFixed = $window.getComputedStyle($document[0].body).position == 'fixed';
  9937        var backdrop = options.backdrop ? $window.getComputedStyle(options.backdrop[0]) : null;
  9938        var height = backdrop ? Math.min($document[0].body.clientHeight, Math.ceil(Math.abs(parseInt(backdrop.height, 10)))) : 0;
  9939  
  9940        container.css({
  9941          top: (isFixed ? $mdUtil.scrollTop(options.parent) : 0) + 'px',
  9942          height: height ? height + 'px' : '100%'
  9943        });
  9944  
  9945        return container;
  9946      }
  9947  
  9948      /**
  9949       *  Dialog open and pop-in animation
  9950       */
  9951      function dialogPopIn(container, options) {
  9952        // Add the `md-dialog-container` to the DOM
  9953        options.parent.append(container);
  9954        stretchDialogContainerToViewport(container, options);
  9955  
  9956        var dialogEl = container.find('md-dialog');
  9957        var animator = $mdUtil.dom.animator;
  9958        var buildTranslateToOrigin = animator.calculateZoomToOrigin;
  9959        var translateOptions = {transitionInClass: 'md-transition-in', transitionOutClass: 'md-transition-out'};
  9960        var from = animator.toTransformCss(buildTranslateToOrigin(dialogEl, options.openFrom || options.origin));
  9961        var to = animator.toTransformCss("");  // defaults to center display (or parent or $rootElement)
  9962  
  9963        if (options.fullscreen) {
  9964          dialogEl.addClass('md-dialog-fullscreen');
  9965        }
  9966  
  9967        return animator
  9968          .translate3d(dialogEl, from, to, translateOptions)
  9969          .then(function(animateReversal) {
  9970            // Build a reversal translate function synched to this translation...
  9971            options.reverseAnimate = function() {
  9972              delete options.reverseAnimate;
  9973  
  9974              if (options.closeTo) {
  9975                // Using the opposite classes to create a close animation to the closeTo element
  9976                translateOptions = {transitionInClass: 'md-transition-out', transitionOutClass: 'md-transition-in'};
  9977                from = to;
  9978                to = animator.toTransformCss(buildTranslateToOrigin(dialogEl, options.closeTo));
  9979  
  9980                return animator
  9981                  .translate3d(dialogEl, from, to,translateOptions);
  9982              }
  9983  
  9984              return animateReversal(
  9985                animator.toTransformCss(
  9986                  // in case the origin element has moved or is hidden,
  9987                  // let's recalculate the translateCSS
  9988                  buildTranslateToOrigin(dialogEl, options.origin)
  9989                )
  9990              );
  9991  
  9992            };
  9993            return true;
  9994          });
  9995      }
  9996  
  9997      /**
  9998       * Dialog close and pop-out animation
  9999       */
 10000      function dialogPopOut(container, options) {
 10001        return options.reverseAnimate();
 10002      }
 10003  
 10004      /**
 10005       * Utility function to filter out raw DOM nodes
 10006       */
 10007      function isNodeOneOf(elem, nodeTypeArray) {
 10008        if (nodeTypeArray.indexOf(elem.nodeName) !== -1) {
 10009          return true;
 10010        }
 10011      }
 10012  
 10013    }
 10014  }
 10015  MdDialogProvider.$inject = ["$$interimElementProvider"];
 10016  
 10017  })();
 10018  (function(){
 10019  "use strict";
 10020  
 10021  /**
 10022   * @ngdoc module
 10023   * @name material.components.divider
 10024   * @description Divider module!
 10025   */
 10026  angular.module('material.components.divider', [
 10027    'material.core'
 10028  ])
 10029    .directive('mdDivider', MdDividerDirective);
 10030  
 10031  /**
 10032   * @ngdoc directive
 10033   * @name mdDivider
 10034   * @module material.components.divider
 10035   * @restrict E
 10036   *
 10037   * @description
 10038   * Dividers group and separate content within lists and page layouts using strong visual and spatial distinctions. This divider is a thin rule, lightweight enough to not distract the user from content.
 10039   *
 10040   * @param {boolean=} md-inset Add this attribute to activate the inset divider style.
 10041   * @usage
 10042   * <hljs lang="html">
 10043   * <md-divider></md-divider>
 10044   *
 10045   * <md-divider md-inset></md-divider>
 10046   * </hljs>
 10047   *
 10048   */
 10049  function MdDividerDirective($mdTheming) {
 10050    return {
 10051      restrict: 'E',
 10052      link: $mdTheming
 10053    };
 10054  }
 10055  MdDividerDirective.$inject = ["$mdTheming"];
 10056  
 10057  })();
 10058  (function(){
 10059  "use strict";
 10060  
 10061  (function() {
 10062    'use strict';
 10063  
 10064    /**
 10065     * @ngdoc module
 10066     * @name material.components.fabActions
 10067     */
 10068    angular
 10069      .module('material.components.fabActions', ['material.core'])
 10070      .directive('mdFabActions', MdFabActionsDirective);
 10071  
 10072    /**
 10073     * @ngdoc directive
 10074     * @name mdFabActions
 10075     * @module material.components.fabActions
 10076     *
 10077     * @restrict E
 10078     *
 10079     * @description
 10080     * The `<md-fab-actions>` directive is used inside of a `<md-fab-speed-dial>` or
 10081     * `<md-fab-toolbar>` directive to mark an element (or elements) as the actions and setup the
 10082     * proper event listeners.
 10083     *
 10084     * @usage
 10085     * See the `<md-fab-speed-dial>` or `<md-fab-toolbar>` directives for example usage.
 10086     */
 10087    function MdFabActionsDirective() {
 10088      return {
 10089        restrict: 'E',
 10090  
 10091        require: ['^?mdFabSpeedDial', '^?mdFabToolbar'],
 10092  
 10093        compile: function(element, attributes) {
 10094          var children = element.children();
 10095  
 10096          var hasNgRepeat = false;
 10097  
 10098          angular.forEach(['', 'data-', 'x-'], function(prefix) {
 10099            hasNgRepeat = hasNgRepeat || (children.attr(prefix + 'ng-repeat') ? true : false);
 10100          });
 10101  
 10102          // Support both ng-repeat and static content
 10103          if (hasNgRepeat) {
 10104            children.addClass('md-fab-action-item');
 10105          } else {
 10106            // Wrap every child in a new div and add a class that we can scale/fling independently
 10107            children.wrap('<div class="md-fab-action-item">');
 10108          }
 10109        }
 10110      }
 10111    }
 10112  
 10113  })();
 10114  
 10115  })();
 10116  (function(){
 10117  "use strict";
 10118  
 10119  (function() {
 10120    'use strict';
 10121  
 10122    angular.module('material.components.fabShared', ['material.core'])
 10123      .controller('FabController', FabController);
 10124  
 10125    function FabController($scope, $element, $animate, $mdUtil, $mdConstant, $timeout) {
 10126      var vm = this;
 10127  
 10128      // NOTE: We use async eval(s) below to avoid conflicts with any existing digest loops
 10129  
 10130      vm.open = function() {
 10131        $scope.$evalAsync("vm.isOpen = true");
 10132      };
 10133  
 10134      vm.close = function() {
 10135        // Async eval to avoid conflicts with existing digest loops
 10136        $scope.$evalAsync("vm.isOpen = false");
 10137  
 10138        // Focus the trigger when the element closes so users can still tab to the next item
 10139        $element.find('md-fab-trigger')[0].focus();
 10140      };
 10141  
 10142      // Toggle the open/close state when the trigger is clicked
 10143      vm.toggle = function() {
 10144        $scope.$evalAsync("vm.isOpen = !vm.isOpen");
 10145      };
 10146  
 10147      setupDefaults();
 10148      setupListeners();
 10149      setupWatchers();
 10150  
 10151      var initialAnimationAttempts = 0;
 10152      fireInitialAnimations();
 10153  
 10154      function setupDefaults() {
 10155        // Set the default direction to 'down' if none is specified
 10156        vm.direction = vm.direction || 'down';
 10157  
 10158        // Set the default to be closed
 10159        vm.isOpen = vm.isOpen || false;
 10160  
 10161        // Start the keyboard interaction at the first action
 10162        resetActionIndex();
 10163  
 10164        // Add an animations waiting class so we know not to run
 10165        $element.addClass('md-animations-waiting');
 10166      }
 10167  
 10168      function setupListeners() {
 10169        var eventTypes = [
 10170          'click', 'focusin', 'focusout'
 10171        ];
 10172  
 10173        // Add our listeners
 10174        angular.forEach(eventTypes, function(eventType) {
 10175          $element.on(eventType, parseEvents);
 10176        });
 10177  
 10178        // Remove our listeners when destroyed
 10179        $scope.$on('$destroy', function() {
 10180          angular.forEach(eventTypes, function(eventType) {
 10181            $element.off(eventType, parseEvents);
 10182          });
 10183  
 10184          // remove any attached keyboard handlers in case element is removed while
 10185          // speed dial is open
 10186          disableKeyboard();
 10187        });
 10188      }
 10189  
 10190      var closeTimeout;
 10191      function parseEvents(event) {
 10192        // If the event is a click, just handle it
 10193        if (event.type == 'click') {
 10194          handleItemClick(event);
 10195        }
 10196  
 10197        // If we focusout, set a timeout to close the element
 10198        if (event.type == 'focusout' && !closeTimeout) {
 10199          closeTimeout = $timeout(function() {
 10200            vm.close();
 10201          }, 100, false);
 10202        }
 10203  
 10204        // If we see a focusin and there is a timeout about to run, cancel it so we stay open
 10205        if (event.type == 'focusin' && closeTimeout) {
 10206          $timeout.cancel(closeTimeout);
 10207          closeTimeout = null;
 10208        }
 10209      }
 10210  
 10211      function resetActionIndex() {
 10212        vm.currentActionIndex = -1;
 10213      }
 10214  
 10215      function setupWatchers() {
 10216        // Watch for changes to the direction and update classes/attributes
 10217        $scope.$watch('vm.direction', function(newDir, oldDir) {
 10218          // Add the appropriate classes so we can target the direction in the CSS
 10219          $animate.removeClass($element, 'md-' + oldDir);
 10220          $animate.addClass($element, 'md-' + newDir);
 10221  
 10222          // Reset the action index since it may have changed
 10223          resetActionIndex();
 10224        });
 10225  
 10226        var trigger, actions;
 10227  
 10228        // Watch for changes to md-open
 10229        $scope.$watch('vm.isOpen', function(isOpen) {
 10230          // Reset the action index since it may have changed
 10231          resetActionIndex();
 10232  
 10233          // We can't get the trigger/actions outside of the watch because the component hasn't been
 10234          // linked yet, so we wait until the first watch fires to cache them.
 10235          if (!trigger || !actions) {
 10236            trigger = getTriggerElement();
 10237            actions = getActionsElement();
 10238          }
 10239  
 10240          if (isOpen) {
 10241            enableKeyboard();
 10242          } else {
 10243            disableKeyboard();
 10244          }
 10245  
 10246          var toAdd = isOpen ? 'md-is-open' : '';
 10247          var toRemove = isOpen ? '' : 'md-is-open';
 10248  
 10249          // Set the proper ARIA attributes
 10250          trigger.attr('aria-haspopup', true);
 10251          trigger.attr('aria-expanded', isOpen);
 10252          actions.attr('aria-hidden', !isOpen);
 10253  
 10254          // Animate the CSS classes
 10255          $animate.setClass($element, toAdd, toRemove);
 10256        });
 10257      }
 10258  
 10259      function fireInitialAnimations() {
 10260        // If the element is actually visible on the screen
 10261        if ($element[0].scrollHeight > 0) {
 10262          // Fire our animation
 10263          $animate.addClass($element, 'md-animations-ready').then(function() {
 10264            // Remove the waiting class
 10265            $element.removeClass('md-animations-waiting');
 10266          });
 10267        }
 10268  
 10269        // Otherwise, try for up to 1 second before giving up
 10270        else if (initialAnimationAttempts < 10) {
 10271          $timeout(fireInitialAnimations, 100);
 10272  
 10273          // Increment our counter
 10274          initialAnimationAttempts = initialAnimationAttempts + 1;
 10275        }
 10276      }
 10277  
 10278      function enableKeyboard() {
 10279        $element.on('keydown', keyPressed);
 10280  
 10281        // On the next tick, setup a check for outside clicks; we do this on the next tick to avoid
 10282        // clicks/touches that result in the isOpen attribute changing (e.g. a bound radio button)
 10283        $mdUtil.nextTick(function() {
 10284          angular.element(document).on('click touchend', checkForOutsideClick);
 10285        });
 10286  
 10287        // TODO: On desktop, we should be able to reset the indexes so you cannot tab through, but
 10288        // this breaks accessibility, especially on mobile, since you have no arrow keys to press
 10289        //resetActionTabIndexes();
 10290      }
 10291  
 10292      function disableKeyboard() {
 10293        $element.off('keydown', keyPressed);
 10294        angular.element(document).off('click touchend', checkForOutsideClick);
 10295      }
 10296  
 10297      function checkForOutsideClick(event) {
 10298        if (event.target) {
 10299          var closestTrigger = $mdUtil.getClosest(event.target, 'md-fab-trigger');
 10300          var closestActions = $mdUtil.getClosest(event.target, 'md-fab-actions');
 10301  
 10302          if (!closestTrigger && !closestActions) {
 10303            vm.close();
 10304          }
 10305        }
 10306      }
 10307  
 10308      function keyPressed(event) {
 10309        switch (event.which) {
 10310          case $mdConstant.KEY_CODE.ESCAPE: vm.close(); event.preventDefault(); return false;
 10311          case $mdConstant.KEY_CODE.LEFT_ARROW: doKeyLeft(event); return false;
 10312          case $mdConstant.KEY_CODE.UP_ARROW: doKeyUp(event); return false;
 10313          case $mdConstant.KEY_CODE.RIGHT_ARROW: doKeyRight(event); return false;
 10314          case $mdConstant.KEY_CODE.DOWN_ARROW: doKeyDown(event); return false;
 10315        }
 10316      }
 10317  
 10318      function doActionPrev(event) {
 10319        focusAction(event, -1);
 10320      }
 10321  
 10322      function doActionNext(event) {
 10323        focusAction(event, 1);
 10324      }
 10325  
 10326      function focusAction(event, direction) {
 10327        var actions = resetActionTabIndexes();
 10328  
 10329        // Increment/decrement the counter with restrictions
 10330        vm.currentActionIndex = vm.currentActionIndex + direction;
 10331        vm.currentActionIndex = Math.min(actions.length - 1, vm.currentActionIndex);
 10332        vm.currentActionIndex = Math.max(0, vm.currentActionIndex);
 10333  
 10334        // Focus the element
 10335        var focusElement =  angular.element(actions[vm.currentActionIndex]).children()[0];
 10336        angular.element(focusElement).attr('tabindex', 0);
 10337        focusElement.focus();
 10338  
 10339        // Make sure the event doesn't bubble and cause something else
 10340        event.preventDefault();
 10341        event.stopImmediatePropagation();
 10342      }
 10343  
 10344      function resetActionTabIndexes() {
 10345        // Grab all of the actions
 10346        var actions = getActionsElement()[0].querySelectorAll('.md-fab-action-item');
 10347  
 10348        // Disable all other actions for tabbing
 10349        angular.forEach(actions, function(action) {
 10350          angular.element(angular.element(action).children()[0]).attr('tabindex', -1);
 10351        });
 10352  
 10353        return actions;
 10354      }
 10355  
 10356      function doKeyLeft(event) {
 10357        if (vm.direction === 'left') {
 10358          doActionNext(event);
 10359        } else {
 10360          doActionPrev(event);
 10361        }
 10362      }
 10363  
 10364      function doKeyUp(event) {
 10365        if (vm.direction === 'down') {
 10366          doActionPrev(event);
 10367        } else {
 10368          doActionNext(event);
 10369        }
 10370      }
 10371  
 10372      function doKeyRight(event) {
 10373        if (vm.direction === 'left') {
 10374          doActionPrev(event);
 10375        } else {
 10376          doActionNext(event);
 10377        }
 10378      }
 10379  
 10380      function doKeyDown(event) {
 10381        if (vm.direction === 'up') {
 10382          doActionPrev(event);
 10383        } else {
 10384          doActionNext(event);
 10385        }
 10386      }
 10387  
 10388      function isTrigger(element) {
 10389        return $mdUtil.getClosest(element, 'md-fab-trigger');
 10390      }
 10391  
 10392      function isAction(element) {
 10393        return $mdUtil.getClosest(element, 'md-fab-actions');
 10394      }
 10395  
 10396      function handleItemClick(event) {
 10397        if (isTrigger(event.target)) {
 10398          vm.toggle();
 10399        }
 10400  
 10401        if (isAction(event.target)) {
 10402          vm.close();
 10403        }
 10404      }
 10405  
 10406      function getTriggerElement() {
 10407        return $element.find('md-fab-trigger');
 10408      }
 10409  
 10410      function getActionsElement() {
 10411        return $element.find('md-fab-actions');
 10412      }
 10413    }
 10414    FabController.$inject = ["$scope", "$element", "$animate", "$mdUtil", "$mdConstant", "$timeout"];
 10415  })();
 10416  
 10417  })();
 10418  (function(){
 10419  "use strict";
 10420  
 10421  (function() {
 10422    'use strict';
 10423  
 10424    /**
 10425     * The duration of the CSS animation in milliseconds.
 10426     *
 10427     * @type {number}
 10428     */
 10429    var cssAnimationDuration = 300;
 10430  
 10431    /**
 10432     * @ngdoc module
 10433     * @name material.components.fabSpeedDial
 10434     */
 10435    angular
 10436      // Declare our module
 10437      .module('material.components.fabSpeedDial', [
 10438        'material.core',
 10439        'material.components.fabShared',
 10440        'material.components.fabTrigger',
 10441        'material.components.fabActions'
 10442      ])
 10443  
 10444      // Register our directive
 10445      .directive('mdFabSpeedDial', MdFabSpeedDialDirective)
 10446  
 10447      // Register our custom animations
 10448      .animation('.md-fling', MdFabSpeedDialFlingAnimation)
 10449      .animation('.md-scale', MdFabSpeedDialScaleAnimation)
 10450  
 10451      // Register a service for each animation so that we can easily inject them into unit tests
 10452      .service('mdFabSpeedDialFlingAnimation', MdFabSpeedDialFlingAnimation)
 10453      .service('mdFabSpeedDialScaleAnimation', MdFabSpeedDialScaleAnimation);
 10454  
 10455    /**
 10456     * @ngdoc directive
 10457     * @name mdFabSpeedDial
 10458     * @module material.components.fabSpeedDial
 10459     *
 10460     * @restrict E
 10461     *
 10462     * @description
 10463     * The `<md-fab-speed-dial>` directive is used to present a series of popup elements (usually
 10464     * `<md-button>`s) for quick access to common actions.
 10465     *
 10466     * There are currently two animations available by applying one of the following classes to
 10467     * the component:
 10468     *
 10469     *  - `md-fling` - The speed dial items appear from underneath the trigger and move into their
 10470     *    appropriate positions.
 10471     *  - `md-scale` - The speed dial items appear in their proper places by scaling from 0% to 100%.
 10472     *
 10473     * You may also easily position the trigger by applying one one of the following classes to the
 10474     * `<md-fab-speed-dial>` element:
 10475     *  - `md-fab-top-left`
 10476     *  - `md-fab-top-right`
 10477     *  - `md-fab-bottom-left`
 10478     *  - `md-fab-bottom-right`
 10479     *
 10480     * These CSS classes use `position: absolute`, so you need to ensure that the container element
 10481     * also uses `position: absolute` or `position: relative` in order for them to work.
 10482     *
 10483     * Additionally, you may use the standard `ng-mouseenter` and `ng-mouseleave` directives to
 10484     * open or close the speed dial. However, if you wish to allow users to hover over the empty
 10485     * space where the actions will appear, you must also add the `md-hover-full` class to the speed
 10486     * dial element. Without this, the hover effect will only occur on top of the trigger.
 10487     *
 10488     * See the demos for more information.
 10489     *
 10490     * ## Troubleshooting
 10491     *
 10492     * If your speed dial shows the closing animation upon launch, you may need to use `ng-cloak` on
 10493     * the parent container to ensure that it is only visible once ready. We have plans to remove this
 10494     * necessity in the future.
 10495     *
 10496     * @usage
 10497     * <hljs lang="html">
 10498     * <md-fab-speed-dial md-direction="up" class="md-fling">
 10499     *   <md-fab-trigger>
 10500     *     <md-button aria-label="Add..."><md-icon icon="/img/icons/plus.svg"></md-icon></md-button>
 10501     *   </md-fab-trigger>
 10502     *
 10503     *   <md-fab-actions>
 10504     *     <md-button aria-label="Add User">
 10505     *       <md-icon icon="/img/icons/user.svg"></md-icon>
 10506     *     </md-button>
 10507     *
 10508     *     <md-button aria-label="Add Group">
 10509     *       <md-icon icon="/img/icons/group.svg"></md-icon>
 10510     *     </md-button>
 10511     *   </md-fab-actions>
 10512     * </md-fab-speed-dial>
 10513     * </hljs>
 10514     *
 10515     * @param {string} md-direction From which direction you would like the speed dial to appear
 10516     * relative to the trigger element.
 10517     * @param {expression=} md-open Programmatically control whether or not the speed-dial is visible.
 10518     */
 10519    function MdFabSpeedDialDirective() {
 10520      return {
 10521        restrict: 'E',
 10522  
 10523        scope: {
 10524          direction: '@?mdDirection',
 10525          isOpen: '=?mdOpen'
 10526        },
 10527  
 10528        bindToController: true,
 10529        controller: 'FabController',
 10530        controllerAs: 'vm',
 10531  
 10532        link: FabSpeedDialLink
 10533      };
 10534  
 10535      function FabSpeedDialLink(scope, element) {
 10536        // Prepend an element to hold our CSS variables so we can use them in the animations below
 10537        element.prepend('<div class="md-css-variables"></div>');
 10538      }
 10539    }
 10540  
 10541    function MdFabSpeedDialFlingAnimation($timeout) {
 10542      function delayDone(done) { $timeout(done, cssAnimationDuration, false); }
 10543  
 10544      function runAnimation(element) {
 10545        // Don't run if we are still waiting and we are not ready
 10546        if (element.hasClass('md-animations-waiting') && !element.hasClass('md-animations-ready')) {
 10547          return;
 10548        }
 10549  
 10550        var el = element[0];
 10551        var ctrl = element.controller('mdFabSpeedDial');
 10552        var items = el.querySelectorAll('.md-fab-action-item');
 10553  
 10554        // Grab our trigger element
 10555        var triggerElement = el.querySelector('md-fab-trigger');
 10556  
 10557        // Grab our element which stores CSS variables
 10558        var variablesElement = el.querySelector('.md-css-variables');
 10559  
 10560        // Setup JS variables based on our CSS variables
 10561        var startZIndex = parseInt(window.getComputedStyle(variablesElement).zIndex);
 10562  
 10563        // Always reset the items to their natural position/state
 10564        angular.forEach(items, function(item, index) {
 10565          var styles = item.style;
 10566  
 10567          styles.transform = styles.webkitTransform = '';
 10568          styles.transitionDelay = '';
 10569          styles.opacity = 1;
 10570  
 10571          // Make the items closest to the trigger have the highest z-index
 10572          styles.zIndex = (items.length - index) + startZIndex;
 10573        });
 10574  
 10575        // Set the trigger to be above all of the actions so they disappear behind it.
 10576        triggerElement.style.zIndex = startZIndex + items.length + 1;
 10577  
 10578        // If the control is closed, hide the items behind the trigger
 10579        if (!ctrl.isOpen) {
 10580          angular.forEach(items, function(item, index) {
 10581            var newPosition, axis;
 10582            var styles = item.style;
 10583  
 10584            // Make sure to account for differences in the dimensions of the trigger verses the items
 10585            // so that we can properly center everything; this helps hide the item's shadows behind
 10586            // the trigger.
 10587            var triggerItemHeightOffset = (triggerElement.clientHeight - item.clientHeight) / 2;
 10588            var triggerItemWidthOffset = (triggerElement.clientWidth - item.clientWidth) / 2;
 10589  
 10590            switch (ctrl.direction) {
 10591              case 'up':
 10592                newPosition = (item.scrollHeight * (index + 1) + triggerItemHeightOffset);
 10593                axis = 'Y';
 10594                break;
 10595              case 'down':
 10596                newPosition = -(item.scrollHeight * (index + 1) + triggerItemHeightOffset);
 10597                axis = 'Y';
 10598                break;
 10599              case 'left':
 10600                newPosition = (item.scrollWidth * (index + 1) + triggerItemWidthOffset);
 10601                axis = 'X';
 10602                break;
 10603              case 'right':
 10604                newPosition = -(item.scrollWidth * (index + 1) + triggerItemWidthOffset);
 10605                axis = 'X';
 10606                break;
 10607            }
 10608  
 10609            var newTranslate = 'translate' + axis + '(' + newPosition + 'px)';
 10610  
 10611            styles.transform = styles.webkitTransform = newTranslate;
 10612          });
 10613        }
 10614      }
 10615  
 10616      return {
 10617        addClass: function(element, className, done) {
 10618          if (element.hasClass('md-fling')) {
 10619            runAnimation(element);
 10620            delayDone(done);
 10621          } else {
 10622            done();
 10623          }
 10624        },
 10625        removeClass: function(element, className, done) {
 10626          runAnimation(element);
 10627          delayDone(done);
 10628        }
 10629      }
 10630    }
 10631    MdFabSpeedDialFlingAnimation.$inject = ["$timeout"];
 10632  
 10633    function MdFabSpeedDialScaleAnimation($timeout) {
 10634      function delayDone(done) { $timeout(done, cssAnimationDuration, false); }
 10635  
 10636      var delay = 65;
 10637  
 10638      function runAnimation(element) {
 10639        var el = element[0];
 10640        var ctrl = element.controller('mdFabSpeedDial');
 10641        var items = el.querySelectorAll('.md-fab-action-item');
 10642  
 10643        // Grab our element which stores CSS variables
 10644        var variablesElement = el.querySelector('.md-css-variables');
 10645  
 10646        // Setup JS variables based on our CSS variables
 10647        var startZIndex = parseInt(window.getComputedStyle(variablesElement).zIndex);
 10648  
 10649        // Always reset the items to their natural position/state
 10650        angular.forEach(items, function(item, index) {
 10651          var styles = item.style,
 10652            offsetDelay = index * delay;
 10653  
 10654          styles.opacity = ctrl.isOpen ? 1 : 0;
 10655          styles.transform = styles.webkitTransform = ctrl.isOpen ? 'scale(1)' : 'scale(0)';
 10656          styles.transitionDelay = (ctrl.isOpen ? offsetDelay : (items.length - offsetDelay)) + 'ms';
 10657  
 10658          // Make the items closest to the trigger have the highest z-index
 10659          styles.zIndex = (items.length - index) + startZIndex;
 10660        });
 10661      }
 10662  
 10663      return {
 10664        addClass: function(element, className, done) {
 10665          runAnimation(element);
 10666          delayDone(done);
 10667        },
 10668  
 10669        removeClass: function(element, className, done) {
 10670          runAnimation(element);
 10671          delayDone(done);
 10672        }
 10673      }
 10674    }
 10675    MdFabSpeedDialScaleAnimation.$inject = ["$timeout"];
 10676  })();
 10677  
 10678  })();
 10679  (function(){
 10680  "use strict";
 10681  
 10682  (function() {
 10683    'use strict';
 10684  
 10685    /**
 10686     * @ngdoc module
 10687     * @name material.components.fabToolbar
 10688     */
 10689    angular
 10690      // Declare our module
 10691      .module('material.components.fabToolbar', [
 10692        'material.core',
 10693        'material.components.fabShared',
 10694        'material.components.fabTrigger',
 10695        'material.components.fabActions'
 10696      ])
 10697  
 10698      // Register our directive
 10699      .directive('mdFabToolbar', MdFabToolbarDirective)
 10700  
 10701      // Register our custom animations
 10702      .animation('.md-fab-toolbar', MdFabToolbarAnimation)
 10703  
 10704      // Register a service for the animation so that we can easily inject it into unit tests
 10705      .service('mdFabToolbarAnimation', MdFabToolbarAnimation);
 10706  
 10707    /**
 10708     * @ngdoc directive
 10709     * @name mdFabToolbar
 10710     * @module material.components.fabToolbar
 10711     *
 10712     * @restrict E
 10713     *
 10714     * @description
 10715     *
 10716     * The `<md-fab-toolbar>` directive is used present a toolbar of elements (usually `<md-button>`s)
 10717     * for quick access to common actions when a floating action button is activated (via click or
 10718     * keyboard navigation).
 10719     *
 10720     * You may also easily position the trigger by applying one one of the following classes to the
 10721     * `<md-fab-toolbar>` element:
 10722     *  - `md-fab-top-left`
 10723     *  - `md-fab-top-right`
 10724     *  - `md-fab-bottom-left`
 10725     *  - `md-fab-bottom-right`
 10726     *
 10727     * These CSS classes use `position: absolute`, so you need to ensure that the container element
 10728     * also uses `position: absolute` or `position: relative` in order for them to work.
 10729     *
 10730     * @usage
 10731     *
 10732     * <hljs lang="html">
 10733     * <md-fab-toolbar md-direction='left'>
 10734     *   <md-fab-trigger>
 10735     *     <md-button aria-label="Add..."><md-icon icon="/img/icons/plus.svg"></md-icon></md-button>
 10736     *   </md-fab-trigger>
 10737     *
 10738     *   <md-fab-actions>
 10739     *     <md-button aria-label="Add User">
 10740     *       <md-icon icon="/img/icons/user.svg"></md-icon>
 10741     *     </md-button>
 10742     *
 10743     *     <md-button aria-label="Add Group">
 10744     *       <md-icon icon="/img/icons/group.svg"></md-icon>
 10745     *     </md-button>
 10746     *   </md-fab-actions>
 10747     * </md-fab-toolbar>
 10748     * </hljs>
 10749     *
 10750     * @param {string} md-direction From which direction you would like the toolbar items to appear
 10751     * relative to the trigger element. Supports `left` and `right` directions.
 10752     * @param {expression=} md-open Programmatically control whether or not the toolbar is visible.
 10753     */
 10754    function MdFabToolbarDirective() {
 10755      return {
 10756        restrict: 'E',
 10757        transclude: true,
 10758        template: '<div class="md-fab-toolbar-wrapper">' +
 10759        '  <div class="md-fab-toolbar-content" ng-transclude></div>' +
 10760        '</div>',
 10761  
 10762        scope: {
 10763          direction: '@?mdDirection',
 10764          isOpen: '=?mdOpen'
 10765        },
 10766  
 10767        bindToController: true,
 10768        controller: 'FabController',
 10769        controllerAs: 'vm',
 10770  
 10771        link: link
 10772      };
 10773  
 10774      function link(scope, element, attributes) {
 10775        // Add the base class for animations
 10776        element.addClass('md-fab-toolbar');
 10777  
 10778        // Prepend the background element to the trigger's button
 10779        element.find('md-fab-trigger').find('button')
 10780          .prepend('<div class="md-fab-toolbar-background"></div>');
 10781      }
 10782    }
 10783  
 10784    function MdFabToolbarAnimation() {
 10785  
 10786      function runAnimation(element, className, done) {
 10787        // If no className was specified, don't do anything
 10788        if (!className) {
 10789          return;
 10790        }
 10791  
 10792        var el = element[0];
 10793        var ctrl = element.controller('mdFabToolbar');
 10794  
 10795        // Grab the relevant child elements
 10796        var backgroundElement = el.querySelector('.md-fab-toolbar-background');
 10797        var triggerElement = el.querySelector('md-fab-trigger button');
 10798        var toolbarElement = el.querySelector('md-toolbar');
 10799        var iconElement = el.querySelector('md-fab-trigger button md-icon');
 10800        var actions = element.find('md-fab-actions').children();
 10801  
 10802        // If we have both elements, use them to position the new background
 10803        if (triggerElement && backgroundElement) {
 10804          // Get our variables
 10805          var color = window.getComputedStyle(triggerElement).getPropertyValue('background-color');
 10806          var width = el.offsetWidth;
 10807          var height = el.offsetHeight;
 10808  
 10809          // Make it twice as big as it should be since we scale from the center
 10810          var scale = 2 * (width / triggerElement.offsetWidth);
 10811  
 10812          // Set some basic styles no matter what animation we're doing
 10813          backgroundElement.style.backgroundColor = color;
 10814          backgroundElement.style.borderRadius = width + 'px';
 10815  
 10816          // If we're open
 10817          if (ctrl.isOpen) {
 10818            // Turn on toolbar pointer events when closed
 10819            toolbarElement.style.pointerEvents = 'initial';
 10820  
 10821            backgroundElement.style.width = triggerElement.offsetWidth + 'px';
 10822            backgroundElement.style.height = triggerElement.offsetHeight + 'px';
 10823            backgroundElement.style.transform = 'scale(' + scale + ')';
 10824  
 10825            // Set the next close animation to have the proper delays
 10826            backgroundElement.style.transitionDelay = '0ms';
 10827            iconElement && (iconElement.style.transitionDelay = '.3s');
 10828  
 10829            // Apply a transition delay to actions
 10830            angular.forEach(actions, function(action, index) {
 10831              action.style.transitionDelay = (actions.length - index) * 25 + 'ms';
 10832            });
 10833          } else {
 10834            // Turn off toolbar pointer events when closed
 10835            toolbarElement.style.pointerEvents = 'none';
 10836  
 10837            // Scale it back down to the trigger's size
 10838            backgroundElement.style.transform = 'scale(1)';
 10839  
 10840            // Reset the position
 10841            backgroundElement.style.top = '0';
 10842  
 10843            if (element.hasClass('md-right')) {
 10844              backgroundElement.style.left = '0';
 10845              backgroundElement.style.right = null;
 10846            }
 10847  
 10848            if (element.hasClass('md-left')) {
 10849              backgroundElement.style.right = '0';
 10850              backgroundElement.style.left = null;
 10851            }
 10852  
 10853            // Set the next open animation to have the proper delays
 10854            backgroundElement.style.transitionDelay = '200ms';
 10855            iconElement && (iconElement.style.transitionDelay = '0ms');
 10856  
 10857            // Apply a transition delay to actions
 10858            angular.forEach(actions, function(action, index) {
 10859              action.style.transitionDelay = 200 + (index * 25) + 'ms';
 10860            });
 10861          }
 10862        }
 10863      }
 10864  
 10865      return {
 10866        addClass: function(element, className, done) {
 10867          runAnimation(element, className, done);
 10868          done();
 10869        },
 10870  
 10871        removeClass: function(element, className, done) {
 10872          runAnimation(element, className, done);
 10873          done();
 10874        }
 10875      }
 10876    }
 10877  })();
 10878  })();
 10879  (function(){
 10880  "use strict";
 10881  
 10882  (function() {
 10883    'use strict';
 10884  
 10885    /**
 10886     * @ngdoc module
 10887     * @name material.components.fabTrigger
 10888     */
 10889    angular
 10890      .module('material.components.fabTrigger', ['material.core'])
 10891      .directive('mdFabTrigger', MdFabTriggerDirective);
 10892  
 10893    /**
 10894     * @ngdoc directive
 10895     * @name mdFabTrigger
 10896     * @module material.components.fabSpeedDial
 10897     *
 10898     * @restrict E
 10899     *
 10900     * @description
 10901     * The `<md-fab-trigger>` directive is used inside of a `<md-fab-speed-dial>` or
 10902     * `<md-fab-toolbar>` directive to mark an element (or elements) as the trigger and setup the
 10903     * proper event listeners.
 10904     *
 10905     * @usage
 10906     * See the `<md-fab-speed-dial>` or `<md-fab-toolbar>` directives for example usage.
 10907     */
 10908    function MdFabTriggerDirective() {
 10909      // TODO: Remove this completely?
 10910      return {
 10911        restrict: 'E',
 10912  
 10913        require: ['^?mdFabSpeedDial', '^?mdFabToolbar']
 10914      };
 10915    }
 10916  })();
 10917  
 10918  
 10919  })();
 10920  (function(){
 10921  "use strict";
 10922  
 10923  /**
 10924   * @ngdoc module
 10925   * @name material.components.gridList
 10926   */
 10927  angular.module('material.components.gridList', ['material.core'])
 10928         .directive('mdGridList', GridListDirective)
 10929         .directive('mdGridTile', GridTileDirective)
 10930         .directive('mdGridTileFooter', GridTileCaptionDirective)
 10931         .directive('mdGridTileHeader', GridTileCaptionDirective)
 10932         .factory('$mdGridLayout', GridLayoutFactory);
 10933  
 10934  /**
 10935   * @ngdoc directive
 10936   * @name mdGridList
 10937   * @module material.components.gridList
 10938   * @restrict E
 10939   * @description
 10940   * Grid lists are an alternative to standard list views. Grid lists are distinct
 10941   * from grids used for layouts and other visual presentations.
 10942   *
 10943   * A grid list is best suited to presenting a homogenous data type, typically
 10944   * images, and is optimized for visual comprehension and differentiating between
 10945   * like data types.
 10946   *
 10947   * A grid list is a continuous element consisting of tessellated, regular
 10948   * subdivisions called cells that contain tiles (`md-grid-tile`).
 10949   *
 10950   * <img src="//material-design.storage.googleapis.com/publish/v_2/material_ext_publish/0Bx4BSt6jniD7OVlEaXZ5YmU1Xzg/components_grids_usage2.png"
 10951   *    style="width: 300px; height: auto; margin-right: 16px;" alt="Concept of grid explained visually">
 10952   * <img src="//material-design.storage.googleapis.com/publish/v_2/material_ext_publish/0Bx4BSt6jniD7VGhsOE5idWlJWXM/components_grids_usage3.png"
 10953   *    style="width: 300px; height: auto;" alt="Grid concepts legend">
 10954   *
 10955   * Cells are arrayed vertically and horizontally within the grid.
 10956   *
 10957   * Tiles hold content and can span one or more cells vertically or horizontally.
 10958   *
 10959   * ### Responsive Attributes
 10960   *
 10961   * The `md-grid-list` directive supports "responsive" attributes, which allow
 10962   * different `md-cols`, `md-gutter` and `md-row-height` values depending on the
 10963   * currently matching media query.
 10964   *
 10965   * In order to set a responsive attribute, first define the fallback value with
 10966   * the standard attribute name, then add additional attributes with the
 10967   * following convention: `{base-attribute-name}-{media-query-name}="{value}"`
 10968   * (ie. `md-cols-lg="8"`)
 10969   *
 10970   * @param {number} md-cols Number of columns in the grid.
 10971   * @param {string} md-row-height One of
 10972   * <ul>
 10973   *   <li>CSS length - Fixed height rows (eg. `8px` or `1rem`)</li>
 10974   *   <li>`{width}:{height}` - Ratio of width to height (eg.
 10975   *   `md-row-height="16:9"`)</li>
 10976   *   <li>`"fit"` - Height will be determined by subdividing the available
 10977   *   height by the number of rows</li>
 10978   * </ul>
 10979   * @param {string=} md-gutter The amount of space between tiles in CSS units
 10980   *     (default 1px)
 10981   * @param {expression=} md-on-layout Expression to evaluate after layout. Event
 10982   *     object is available as `$event`, and contains performance information.
 10983   *
 10984   * @usage
 10985   * Basic:
 10986   * <hljs lang="html">
 10987   * <md-grid-list md-cols="5" md-gutter="1em" md-row-height="4:3">
 10988   *   <md-grid-tile></md-grid-tile>
 10989   * </md-grid-list>
 10990   * </hljs>
 10991   *
 10992   * Fixed-height rows:
 10993   * <hljs lang="html">
 10994   * <md-grid-list md-cols="4" md-row-height="200px" ...>
 10995   *   <md-grid-tile></md-grid-tile>
 10996   * </md-grid-list>
 10997   * </hljs>
 10998   *
 10999   * Fit rows:
 11000   * <hljs lang="html">
 11001   * <md-grid-list md-cols="4" md-row-height="fit" style="height: 400px;" ...>
 11002   *   <md-grid-tile></md-grid-tile>
 11003   * </md-grid-list>
 11004   * </hljs>
 11005   *
 11006   * Using responsive attributes:
 11007   * <hljs lang="html">
 11008   * <md-grid-list
 11009   *     md-cols-sm="2"
 11010   *     md-cols-md="4"
 11011   *     md-cols-lg="8"
 11012   *     md-cols-gt-lg="12"
 11013   *     ...>
 11014   *   <md-grid-tile></md-grid-tile>
 11015   * </md-grid-list>
 11016   * </hljs>
 11017   */
 11018  function GridListDirective($interpolate, $mdConstant, $mdGridLayout, $mdMedia) {
 11019    return {
 11020      restrict: 'E',
 11021      controller: GridListController,
 11022      scope: {
 11023        mdOnLayout: '&'
 11024      },
 11025      link: postLink
 11026    };
 11027  
 11028    function postLink(scope, element, attrs, ctrl) {
 11029      // Apply semantics
 11030      element.attr('role', 'list');
 11031  
 11032      // Provide the controller with a way to trigger layouts.
 11033      ctrl.layoutDelegate = layoutDelegate;
 11034  
 11035      var invalidateLayout = angular.bind(ctrl, ctrl.invalidateLayout),
 11036          unwatchAttrs = watchMedia();
 11037        scope.$on('$destroy', unwatchMedia);
 11038  
 11039      /**
 11040       * Watches for changes in media, invalidating layout as necessary.
 11041       */
 11042      function watchMedia() {
 11043        for (var mediaName in $mdConstant.MEDIA) {
 11044          $mdMedia(mediaName); // initialize
 11045          $mdMedia.getQuery($mdConstant.MEDIA[mediaName])
 11046              .addListener(invalidateLayout);
 11047        }
 11048        return $mdMedia.watchResponsiveAttributes(
 11049            ['md-cols', 'md-row-height', 'md-gutter'], attrs, layoutIfMediaMatch);
 11050      }
 11051  
 11052      function unwatchMedia() {
 11053        ctrl.layoutDelegate = angular.noop;
 11054  
 11055        unwatchAttrs();
 11056        for (var mediaName in $mdConstant.MEDIA) {
 11057          $mdMedia.getQuery($mdConstant.MEDIA[mediaName])
 11058              .removeListener(invalidateLayout);
 11059        }
 11060      }
 11061  
 11062      /**
 11063       * Performs grid layout if the provided mediaName matches the currently
 11064       * active media type.
 11065       */
 11066      function layoutIfMediaMatch(mediaName) {
 11067        if (mediaName == null) {
 11068          // TODO(shyndman): It would be nice to only layout if we have
 11069          // instances of attributes using this media type
 11070          ctrl.invalidateLayout();
 11071        } else if ($mdMedia(mediaName)) {
 11072          ctrl.invalidateLayout();
 11073        }
 11074      }
 11075  
 11076      var lastLayoutProps;
 11077  
 11078      /**
 11079       * Invokes the layout engine, and uses its results to lay out our
 11080       * tile elements.
 11081       *
 11082       * @param {boolean} tilesInvalidated Whether tiles have been
 11083       *    added/removed/moved since the last layout. This is to avoid situations
 11084       *    where tiles are replaced with properties identical to their removed
 11085       *    counterparts.
 11086       */
 11087      function layoutDelegate(tilesInvalidated) {
 11088        var tiles = getTileElements();
 11089        var props = {
 11090          tileSpans: getTileSpans(tiles),
 11091          colCount: getColumnCount(),
 11092          rowMode: getRowMode(),
 11093          rowHeight: getRowHeight(),
 11094          gutter: getGutter()
 11095        };
 11096  
 11097        if (!tilesInvalidated && angular.equals(props, lastLayoutProps)) {
 11098          return;
 11099        }
 11100  
 11101        var performance =
 11102          $mdGridLayout(props.colCount, props.tileSpans, tiles)
 11103            .map(function(tilePositions, rowCount) {
 11104              return {
 11105                grid: {
 11106                  element: element,
 11107                  style: getGridStyle(props.colCount, rowCount,
 11108                      props.gutter, props.rowMode, props.rowHeight)
 11109                },
 11110                tiles: tilePositions.map(function(ps, i) {
 11111                  return {
 11112                    element: angular.element(tiles[i]),
 11113                    style: getTileStyle(ps.position, ps.spans,
 11114                        props.colCount, rowCount,
 11115                        props.gutter, props.rowMode, props.rowHeight)
 11116                  }
 11117                })
 11118              }
 11119            })
 11120            .reflow()
 11121            .performance();
 11122  
 11123        // Report layout
 11124        scope.mdOnLayout({
 11125          $event: {
 11126            performance: performance
 11127          }
 11128        });
 11129  
 11130        lastLayoutProps = props;
 11131      }
 11132  
 11133      // Use $interpolate to do some simple string interpolation as a convenience.
 11134  
 11135      var startSymbol = $interpolate.startSymbol();
 11136      var endSymbol = $interpolate.endSymbol();
 11137  
 11138      // Returns an expression wrapped in the interpolator's start and end symbols.
 11139      function expr(exprStr) {
 11140        return startSymbol + exprStr + endSymbol;
 11141      }
 11142  
 11143      // The amount of space a single 1x1 tile would take up (either width or height), used as
 11144      // a basis for other calculations. This consists of taking the base size percent (as would be
 11145      // if evenly dividing the size between cells), and then subtracting the size of one gutter.
 11146      // However, since there are no gutters on the edges, each tile only uses a fration
 11147      // (gutterShare = numGutters / numCells) of the gutter size. (Imagine having one gutter per
 11148      // tile, and then breaking up the extra gutter on the edge evenly among the cells).
 11149      var UNIT = $interpolate(expr('share') + '% - (' + expr('gutter') + ' * ' + expr('gutterShare') + ')');
 11150  
 11151      // The horizontal or vertical position of a tile, e.g., the 'top' or 'left' property value.
 11152      // The position comes the size of a 1x1 tile plus gutter for each previous tile in the
 11153      // row/column (offset).
 11154      var POSITION  = $interpolate('calc((' + expr('unit') + ' + ' + expr('gutter') + ') * ' + expr('offset') + ')');
 11155  
 11156      // The actual size of a tile, e.g., width or height, taking rowSpan or colSpan into account.
 11157      // This is computed by multiplying the base unit by the rowSpan/colSpan, and then adding back
 11158      // in the space that the gutter would normally have used (which was already accounted for in
 11159      // the base unit calculation).
 11160      var DIMENSION = $interpolate('calc((' + expr('unit') + ') * ' + expr('span') + ' + (' + expr('span') + ' - 1) * ' + expr('gutter') + ')');
 11161  
 11162      /**
 11163       * Gets the styles applied to a tile element described by the given parameters.
 11164       * @param {{row: number, col: number}} position The row and column indices of the tile.
 11165       * @param {{row: number, col: number}} spans The rowSpan and colSpan of the tile.
 11166       * @param {number} colCount The number of columns.
 11167       * @param {number} rowCount The number of rows.
 11168       * @param {string} gutter The amount of space between tiles. This will be something like
 11169       *     '5px' or '2em'.
 11170       * @param {string} rowMode The row height mode. Can be one of:
 11171       *     'fixed': all rows have a fixed size, given by rowHeight,
 11172       *     'ratio': row height defined as a ratio to width, or
 11173       *     'fit': fit to the grid-list element height, divinding evenly among rows.
 11174       * @param {string|number} rowHeight The height of a row. This is only used for 'fixed' mode and
 11175       *     for 'ratio' mode. For 'ratio' mode, this is the *ratio* of width-to-height (e.g., 0.75).
 11176       * @returns {Object} Map of CSS properties to be applied to the style element. Will define
 11177       *     values for top, left, width, height, marginTop, and paddingTop.
 11178       */
 11179      function getTileStyle(position, spans, colCount, rowCount, gutter, rowMode, rowHeight) {
 11180        // TODO(shyndman): There are style caching opportunities here.
 11181  
 11182        // Percent of the available horizontal space that one column takes up.
 11183        var hShare = (1 / colCount) * 100;
 11184  
 11185        // Fraction of the gutter size that each column takes up.
 11186        var hGutterShare = (colCount - 1) / colCount;
 11187  
 11188        // Base horizontal size of a column.
 11189        var hUnit = UNIT({share: hShare, gutterShare: hGutterShare, gutter: gutter});
 11190  
 11191        // The width and horizontal position of each tile is always calculated the same way, but the
 11192        // height and vertical position depends on the rowMode.
 11193        var style = {
 11194          left: POSITION({ unit: hUnit, offset: position.col, gutter: gutter }),
 11195          width: DIMENSION({ unit: hUnit, span: spans.col, gutter: gutter }),
 11196          // resets
 11197          paddingTop: '',
 11198          marginTop: '',
 11199          top: '',
 11200          height: ''
 11201        };
 11202  
 11203        switch (rowMode) {
 11204          case 'fixed':
 11205            // In fixed mode, simply use the given rowHeight.
 11206            style.top = POSITION({ unit: rowHeight, offset: position.row, gutter: gutter });
 11207            style.height = DIMENSION({ unit: rowHeight, span: spans.row, gutter: gutter });
 11208            break;
 11209  
 11210          case 'ratio':
 11211            // Percent of the available vertical space that one row takes up. Here, rowHeight holds
 11212            // the ratio value. For example, if the width:height ratio is 4:3, rowHeight = 1.333.
 11213            var vShare = hShare / rowHeight;
 11214  
 11215            // Base veritcal size of a row.
 11216            var vUnit = UNIT({ share: vShare, gutterShare: hGutterShare, gutter: gutter });
 11217  
 11218            // padidngTop and marginTop are used to maintain the given aspect ratio, as
 11219            // a percentage-based value for these properties is applied to the *width* of the
 11220            // containing block. See http://www.w3.org/TR/CSS2/box.html#margin-properties
 11221            style.paddingTop = DIMENSION({ unit: vUnit, span: spans.row, gutter: gutter});
 11222            style.marginTop = POSITION({ unit: vUnit, offset: position.row, gutter: gutter });
 11223            break;
 11224  
 11225          case 'fit':
 11226            // Fraction of the gutter size that each column takes up.
 11227            var vGutterShare = (rowCount - 1) / rowCount;
 11228  
 11229            // Percent of the available vertical space that one row takes up.
 11230            var vShare = (1 / rowCount) * 100;
 11231  
 11232            // Base vertical size of a row.
 11233            var vUnit = UNIT({share: vShare, gutterShare: vGutterShare, gutter: gutter});
 11234  
 11235            style.top = POSITION({unit: vUnit, offset: position.row, gutter: gutter});
 11236            style.height = DIMENSION({unit: vUnit, span: spans.row, gutter: gutter});
 11237            break;
 11238        }
 11239  
 11240        return style;
 11241      }
 11242  
 11243      function getGridStyle(colCount, rowCount, gutter, rowMode, rowHeight) {
 11244        var style = {};
 11245  
 11246        switch(rowMode) {
 11247          case 'fixed':
 11248            style.height = DIMENSION({ unit: rowHeight, span: rowCount, gutter: gutter });
 11249            style.paddingBottom = '';
 11250            break;
 11251  
 11252          case 'ratio':
 11253            // rowHeight is width / height
 11254            var hGutterShare = colCount === 1 ? 0 : (colCount - 1) / colCount,
 11255                hShare = (1 / colCount) * 100,
 11256                vShare = hShare * (1 / rowHeight),
 11257                vUnit = UNIT({ share: vShare, gutterShare: hGutterShare, gutter: gutter });
 11258  
 11259            style.height = '';
 11260            style.paddingBottom = DIMENSION({ unit: vUnit, span: rowCount, gutter: gutter});
 11261            break;
 11262  
 11263          case 'fit':
 11264            // noop, as the height is user set
 11265            break;
 11266        }
 11267  
 11268        return style;
 11269      }
 11270  
 11271      function getTileElements() {
 11272        return [].filter.call(element.children(), function(ele) {
 11273          return ele.tagName == 'MD-GRID-TILE' && !ele.$$mdDestroyed;
 11274        });
 11275      }
 11276  
 11277      /**
 11278       * Gets an array of objects containing the rowspan and colspan for each tile.
 11279       * @returns {Array<{row: number, col: number}>}
 11280       */
 11281      function getTileSpans(tileElements) {
 11282        return [].map.call(tileElements, function(ele) {
 11283          var ctrl = angular.element(ele).controller('mdGridTile');
 11284          return {
 11285            row: parseInt(
 11286                $mdMedia.getResponsiveAttribute(ctrl.$attrs, 'md-rowspan'), 10) || 1,
 11287            col: parseInt(
 11288                $mdMedia.getResponsiveAttribute(ctrl.$attrs, 'md-colspan'), 10) || 1
 11289          };
 11290        });
 11291      }
 11292  
 11293      function getColumnCount() {
 11294        var colCount = parseInt($mdMedia.getResponsiveAttribute(attrs, 'md-cols'), 10);
 11295        if (isNaN(colCount)) {
 11296          throw 'md-grid-list: md-cols attribute was not found, or contained a non-numeric value';
 11297        }
 11298        return colCount;
 11299      }
 11300  
 11301      function getGutter() {
 11302        return applyDefaultUnit($mdMedia.getResponsiveAttribute(attrs, 'md-gutter') || 1);
 11303      }
 11304  
 11305      function getRowHeight() {
 11306        var rowHeight = $mdMedia.getResponsiveAttribute(attrs, 'md-row-height');
 11307        if (!rowHeight) {
 11308          throw 'md-grid-list: md-row-height attribute was not found';
 11309        }
 11310  
 11311        switch (getRowMode()) {
 11312          case 'fixed':
 11313            return applyDefaultUnit(rowHeight);
 11314          case 'ratio':
 11315            var whRatio = rowHeight.split(':');
 11316            return parseFloat(whRatio[0]) / parseFloat(whRatio[1]);
 11317          case 'fit':
 11318            return 0; // N/A
 11319        }
 11320      }
 11321  
 11322      function getRowMode() {
 11323        var rowHeight = $mdMedia.getResponsiveAttribute(attrs, 'md-row-height');
 11324        if (!rowHeight) {
 11325          throw 'md-grid-list: md-row-height attribute was not found';
 11326        }
 11327  
 11328        if (rowHeight == 'fit') {
 11329          return 'fit';
 11330        } else if (rowHeight.indexOf(':') !== -1) {
 11331          return 'ratio';
 11332        } else {
 11333          return 'fixed';
 11334        }
 11335      }
 11336  
 11337      function applyDefaultUnit(val) {
 11338        return /\D$/.test(val) ? val : val + 'px';
 11339      }
 11340    }
 11341  }
 11342  GridListDirective.$inject = ["$interpolate", "$mdConstant", "$mdGridLayout", "$mdMedia"];
 11343  
 11344  /* @ngInject */
 11345  function GridListController($mdUtil) {
 11346    this.layoutInvalidated = false;
 11347    this.tilesInvalidated = false;
 11348    this.$timeout_ = $mdUtil.nextTick;
 11349    this.layoutDelegate = angular.noop;
 11350  }
 11351  GridListController.$inject = ["$mdUtil"];
 11352  
 11353  GridListController.prototype = {
 11354    invalidateTiles: function() {
 11355      this.tilesInvalidated = true;
 11356      this.invalidateLayout();
 11357    },
 11358  
 11359    invalidateLayout: function() {
 11360      if (this.layoutInvalidated) {
 11361        return;
 11362      }
 11363      this.layoutInvalidated = true;
 11364      this.$timeout_(angular.bind(this, this.layout));
 11365    },
 11366  
 11367    layout: function() {
 11368      try {
 11369        this.layoutDelegate(this.tilesInvalidated);
 11370      } finally {
 11371        this.layoutInvalidated = false;
 11372        this.tilesInvalidated = false;
 11373      }
 11374    }
 11375  };
 11376  
 11377  
 11378  /* @ngInject */
 11379  function GridLayoutFactory($mdUtil) {
 11380    var defaultAnimator = GridTileAnimator;
 11381  
 11382    /**
 11383     * Set the reflow animator callback
 11384     */
 11385    GridLayout.animateWith = function(customAnimator) {
 11386      defaultAnimator = !angular.isFunction(customAnimator) ? GridTileAnimator : customAnimator;
 11387    };
 11388  
 11389    return GridLayout;
 11390  
 11391    /**
 11392     * Publish layout function
 11393     */
 11394    function GridLayout(colCount, tileSpans) {
 11395        var self, layoutInfo, gridStyles, layoutTime, mapTime, reflowTime;
 11396  
 11397        layoutTime = $mdUtil.time(function() {
 11398          layoutInfo = calculateGridFor(colCount, tileSpans);
 11399        });
 11400  
 11401        return self = {
 11402  
 11403          /**
 11404           * An array of objects describing each tile's position in the grid.
 11405           */
 11406          layoutInfo: function() {
 11407            return layoutInfo;
 11408          },
 11409  
 11410          /**
 11411           * Maps grid positioning to an element and a set of styles using the
 11412           * provided updateFn.
 11413           */
 11414          map: function(updateFn) {
 11415            mapTime = $mdUtil.time(function() {
 11416              var info = self.layoutInfo();
 11417              gridStyles = updateFn(info.positioning, info.rowCount);
 11418            });
 11419            return self;
 11420          },
 11421  
 11422          /**
 11423           * Default animator simply sets the element.css( <styles> ). An alternate
 11424           * animator can be provided as an argument. The function has the following
 11425           * signature:
 11426           *
 11427           *    function({grid: {element: JQLite, style: Object}, tiles: Array<{element: JQLite, style: Object}>)
 11428           */
 11429          reflow: function(animatorFn) {
 11430            reflowTime = $mdUtil.time(function() {
 11431              var animator = animatorFn || defaultAnimator;
 11432              animator(gridStyles.grid, gridStyles.tiles);
 11433            });
 11434            return self;
 11435          },
 11436  
 11437          /**
 11438           * Timing for the most recent layout run.
 11439           */
 11440          performance: function() {
 11441            return {
 11442              tileCount: tileSpans.length,
 11443              layoutTime: layoutTime,
 11444              mapTime: mapTime,
 11445              reflowTime: reflowTime,
 11446              totalTime: layoutTime + mapTime + reflowTime
 11447            };
 11448          }
 11449        };
 11450      }
 11451  
 11452    /**
 11453     * Default Gridlist animator simple sets the css for each element;
 11454     * NOTE: any transitions effects must be manually set in the CSS.
 11455     * e.g.
 11456     *
 11457     *  md-grid-tile {
 11458     *    transition: all 700ms ease-out 50ms;
 11459     *  }
 11460     *
 11461     */
 11462    function GridTileAnimator(grid, tiles) {
 11463      grid.element.css(grid.style);
 11464      tiles.forEach(function(t) {
 11465        t.element.css(t.style);
 11466      })
 11467    }
 11468  
 11469    /**
 11470     * Calculates the positions of tiles.
 11471     *
 11472     * The algorithm works as follows:
 11473     *    An Array<Number> with length colCount (spaceTracker) keeps track of
 11474     *    available tiling positions, where elements of value 0 represents an
 11475     *    empty position. Space for a tile is reserved by finding a sequence of
 11476     *    0s with length <= than the tile's colspan. When such a space has been
 11477     *    found, the occupied tile positions are incremented by the tile's
 11478     *    rowspan value, as these positions have become unavailable for that
 11479     *    many rows.
 11480     *
 11481     *    If the end of a row has been reached without finding space for the
 11482     *    tile, spaceTracker's elements are each decremented by 1 to a minimum
 11483     *    of 0. Rows are searched in this fashion until space is found.
 11484     */
 11485    function calculateGridFor(colCount, tileSpans) {
 11486      var curCol = 0,
 11487          curRow = 0,
 11488          spaceTracker = newSpaceTracker();
 11489  
 11490      return {
 11491        positioning: tileSpans.map(function(spans, i) {
 11492          return {
 11493            spans: spans,
 11494            position: reserveSpace(spans, i)
 11495          };
 11496        }),
 11497        rowCount: curRow + Math.max.apply(Math, spaceTracker)
 11498      };
 11499  
 11500      function reserveSpace(spans, i) {
 11501        if (spans.col > colCount) {
 11502          throw 'md-grid-list: Tile at position ' + i + ' has a colspan ' +
 11503              '(' + spans.col + ') that exceeds the column count ' +
 11504              '(' + colCount + ')';
 11505        }
 11506  
 11507        var start = 0,
 11508            end = 0;
 11509  
 11510        // TODO(shyndman): This loop isn't strictly necessary if you can
 11511        // determine the minimum number of rows before a space opens up. To do
 11512        // this, recognize that you've iterated across an entire row looking for
 11513        // space, and if so fast-forward by the minimum rowSpan count. Repeat
 11514        // until the required space opens up.
 11515        while (end - start < spans.col) {
 11516          if (curCol >= colCount) {
 11517            nextRow();
 11518            continue;
 11519          }
 11520  
 11521          start = spaceTracker.indexOf(0, curCol);
 11522          if (start === -1 || (end = findEnd(start + 1)) === -1) {
 11523            start = end = 0;
 11524            nextRow();
 11525            continue;
 11526          }
 11527  
 11528          curCol = end + 1;
 11529        }
 11530  
 11531        adjustRow(start, spans.col, spans.row);
 11532        curCol = start + spans.col;
 11533  
 11534        return {
 11535          col: start,
 11536          row: curRow
 11537        };
 11538      }
 11539  
 11540      function nextRow() {
 11541        curCol = 0;
 11542        curRow++;
 11543        adjustRow(0, colCount, -1); // Decrement row spans by one
 11544      }
 11545  
 11546      function adjustRow(from, cols, by) {
 11547        for (var i = from; i < from + cols; i++) {
 11548          spaceTracker[i] = Math.max(spaceTracker[i] + by, 0);
 11549        }
 11550      }
 11551  
 11552      function findEnd(start) {
 11553        var i;
 11554        for (i = start; i < spaceTracker.length; i++) {
 11555          if (spaceTracker[i] !== 0) {
 11556            return i;
 11557          }
 11558        }
 11559  
 11560        if (i === spaceTracker.length) {
 11561          return i;
 11562        }
 11563      }
 11564  
 11565      function newSpaceTracker() {
 11566        var tracker = [];
 11567        for (var i = 0; i < colCount; i++) {
 11568          tracker.push(0);
 11569        }
 11570        return tracker;
 11571      }
 11572    }
 11573  }
 11574  GridLayoutFactory.$inject = ["$mdUtil"];
 11575  
 11576  /**
 11577   * @ngdoc directive
 11578   * @name mdGridTile
 11579   * @module material.components.gridList
 11580   * @restrict E
 11581   * @description
 11582   * Tiles contain the content of an `md-grid-list`. They span one or more grid
 11583   * cells vertically or horizontally, and use `md-grid-tile-{footer,header}` to
 11584   * display secondary content.
 11585   *
 11586   * ### Responsive Attributes
 11587   *
 11588   * The `md-grid-tile` directive supports "responsive" attributes, which allow
 11589   * different `md-rowspan` and `md-colspan` values depending on the currently
 11590   * matching media query.
 11591   *
 11592   * In order to set a responsive attribute, first define the fallback value with
 11593   * the standard attribute name, then add additional attributes with the
 11594   * following convention: `{base-attribute-name}-{media-query-name}="{value}"`
 11595   * (ie. `md-colspan-sm="4"`)
 11596   *
 11597   * @param {number=} md-colspan The number of columns to span (default 1). Cannot
 11598   *    exceed the number of columns in the grid. Supports interpolation.
 11599   * @param {number=} md-rowspan The number of rows to span (default 1). Supports
 11600   *     interpolation.
 11601   *
 11602   * @usage
 11603   * With header:
 11604   * <hljs lang="html">
 11605   * <md-grid-tile>
 11606   *   <md-grid-tile-header>
 11607   *     <h3>This is a header</h3>
 11608   *   </md-grid-tile-header>
 11609   * </md-grid-tile>
 11610   * </hljs>
 11611   *
 11612   * With footer:
 11613   * <hljs lang="html">
 11614   * <md-grid-tile>
 11615   *   <md-grid-tile-footer>
 11616   *     <h3>This is a footer</h3>
 11617   *   </md-grid-tile-footer>
 11618   * </md-grid-tile>
 11619   * </hljs>
 11620   *
 11621   * Spanning multiple rows/columns:
 11622   * <hljs lang="html">
 11623   * <md-grid-tile md-colspan="2" md-rowspan="3">
 11624   * </md-grid-tile>
 11625   * </hljs>
 11626   *
 11627   * Responsive attributes:
 11628   * <hljs lang="html">
 11629   * <md-grid-tile md-colspan="1" md-colspan-sm="3" md-colspan-md="5">
 11630   * </md-grid-tile>
 11631   * </hljs>
 11632   */
 11633  function GridTileDirective($mdMedia) {
 11634    return {
 11635      restrict: 'E',
 11636      require: '^mdGridList',
 11637      template: '<figure ng-transclude></figure>',
 11638      transclude: true,
 11639      scope: {},
 11640      // Simple controller that exposes attributes to the grid directive
 11641      controller: ["$attrs", function($attrs) {
 11642        this.$attrs = $attrs;
 11643      }],
 11644      link: postLink
 11645    };
 11646  
 11647    function postLink(scope, element, attrs, gridCtrl) {
 11648      // Apply semantics
 11649      element.attr('role', 'listitem');
 11650  
 11651      // If our colspan or rowspan changes, trigger a layout
 11652      var unwatchAttrs = $mdMedia.watchResponsiveAttributes(['md-colspan', 'md-rowspan'],
 11653          attrs, angular.bind(gridCtrl, gridCtrl.invalidateLayout));
 11654  
 11655      // Tile registration/deregistration
 11656      gridCtrl.invalidateTiles();
 11657      scope.$on('$destroy', function() {
 11658        // Mark the tile as destroyed so it is no longer considered in layout,
 11659        // even if the DOM element sticks around (like during a leave animation)
 11660        element[0].$$mdDestroyed = true;
 11661        unwatchAttrs();
 11662        gridCtrl.invalidateLayout();
 11663      });
 11664  
 11665      if (angular.isDefined(scope.$parent.$index)) {
 11666        scope.$watch(function() { return scope.$parent.$index; },
 11667          function indexChanged(newIdx, oldIdx) {
 11668            if (newIdx === oldIdx) {
 11669              return;
 11670            }
 11671            gridCtrl.invalidateTiles();
 11672          });
 11673      }
 11674    }
 11675  }
 11676  GridTileDirective.$inject = ["$mdMedia"];
 11677  
 11678  
 11679  function GridTileCaptionDirective() {
 11680    return {
 11681      template: '<figcaption ng-transclude></figcaption>',
 11682      transclude: true
 11683    };
 11684  }
 11685  
 11686  })();
 11687  (function(){
 11688  "use strict";
 11689  
 11690  /**
 11691   * @ngdoc module
 11692   * @name material.components.icon
 11693   * @description
 11694   * Icon
 11695   */
 11696  angular.module('material.components.icon', ['material.core']);
 11697  
 11698  })();
 11699  (function(){
 11700  "use strict";
 11701  
 11702  /**
 11703   * @ngdoc module
 11704   * @name material.components.input
 11705   */
 11706  
 11707  angular.module('material.components.input', [
 11708      'material.core'
 11709    ])
 11710    .directive('mdInputContainer', mdInputContainerDirective)
 11711    .directive('label', labelDirective)
 11712    .directive('input', inputTextareaDirective)
 11713    .directive('textarea', inputTextareaDirective)
 11714    .directive('mdMaxlength', mdMaxlengthDirective)
 11715    .directive('placeholder', placeholderDirective)
 11716    .directive('ngMessages', ngMessagesDirective)
 11717    .directive('ngMessage', ngMessageDirective)
 11718    .directive('ngMessageExp', ngMessageDirective)
 11719    .directive('mdSelectOnFocus', mdSelectOnFocusDirective)
 11720  
 11721    .animation('.md-input-invalid', mdInputInvalidMessagesAnimation)
 11722    .animation('.md-input-messages-animation', ngMessagesAnimation)
 11723    .animation('.md-input-message-animation', ngMessageAnimation);
 11724  
 11725  /**
 11726   * @ngdoc directive
 11727   * @name mdInputContainer
 11728   * @module material.components.input
 11729   *
 11730   * @restrict E
 11731   *
 11732   * @description
 11733   * `<md-input-container>` is the parent of any input or textarea element.
 11734   *
 11735   * Input and textarea elements will not behave properly unless the md-input-container
 11736   * parent is provided.
 11737   *
 11738   * A single `<md-input-container>` should contain only one `<input>` element, otherwise it will throw an error.
 11739   *
 11740   * <b>Exception:</b> Hidden inputs (`<input type="hidden" />`) are ignored and will not throw an error, so
 11741   * you may combine these with other inputs.
 11742   *
 11743   * @param md-is-error {expression=} When the given expression evaluates to true, the input container
 11744   *   will go into error state. Defaults to erroring if the input has been touched and is invalid.
 11745   * @param md-no-float {boolean=} When present, `placeholder` attributes on the input will not be converted to floating
 11746   *   labels.
 11747   *
 11748   * @usage
 11749   * <hljs lang="html">
 11750   *
 11751   * <md-input-container>
 11752   *   <label>Username</label>
 11753   *   <input type="text" ng-model="user.name">
 11754   * </md-input-container>
 11755   *
 11756   * <md-input-container>
 11757   *   <label>Description</label>
 11758   *   <textarea ng-model="user.description"></textarea>
 11759   * </md-input-container>
 11760   *
 11761   * </hljs>
 11762   *
 11763   * <h3>When disabling floating labels</h3>
 11764   * <hljs lang="html">
 11765   *
 11766   * <md-input-container md-no-float>
 11767   *   <input type="text" placeholder="Non-Floating Label">
 11768   * </md-input-container>
 11769   *
 11770   * </hljs>
 11771   */
 11772  function mdInputContainerDirective($mdTheming, $parse) {
 11773    ContainerCtrl.$inject = ["$scope", "$element", "$attrs", "$animate"];
 11774    return {
 11775      restrict: 'E',
 11776      link: postLink,
 11777      controller: ContainerCtrl
 11778    };
 11779  
 11780    function postLink(scope, element, attr) {
 11781      $mdTheming(element);
 11782      if (element.find('md-icon').length) element.addClass('md-has-icon');
 11783    }
 11784  
 11785    function ContainerCtrl($scope, $element, $attrs, $animate) {
 11786      var self = this;
 11787  
 11788      self.isErrorGetter = $attrs.mdIsError && $parse($attrs.mdIsError);
 11789  
 11790      self.delegateClick = function() {
 11791        self.input.focus();
 11792      };
 11793      self.element = $element;
 11794      self.setFocused = function(isFocused) {
 11795        $element.toggleClass('md-input-focused', !!isFocused);
 11796      };
 11797      self.setHasValue = function(hasValue) {
 11798        $element.toggleClass('md-input-has-value', !!hasValue);
 11799      };
 11800      self.setHasPlaceholder = function(hasPlaceholder) {
 11801        $element.toggleClass('md-input-has-placeholder', !!hasPlaceholder);
 11802      };
 11803      self.setInvalid = function(isInvalid) {
 11804        if (isInvalid) {
 11805          $animate.addClass($element, 'md-input-invalid');
 11806        } else {
 11807          $animate.removeClass($element, 'md-input-invalid');
 11808        }
 11809      };
 11810      $scope.$watch(function() {
 11811        return self.label && self.input;
 11812      }, function(hasLabelAndInput) {
 11813        if (hasLabelAndInput && !self.label.attr('for')) {
 11814          self.label.attr('for', self.input.attr('id'));
 11815        }
 11816      });
 11817    }
 11818  }
 11819  mdInputContainerDirective.$inject = ["$mdTheming", "$parse"];
 11820  
 11821  function labelDirective() {
 11822    return {
 11823      restrict: 'E',
 11824      require: '^?mdInputContainer',
 11825      link: function(scope, element, attr, containerCtrl) {
 11826        if (!containerCtrl || attr.mdNoFloat || element.hasClass('md-container-ignore')) return;
 11827  
 11828        containerCtrl.label = element;
 11829        scope.$on('$destroy', function() {
 11830          containerCtrl.label = null;
 11831        });
 11832      }
 11833    };
 11834  }
 11835  
 11836  /**
 11837   * @ngdoc directive
 11838   * @name mdInput
 11839   * @restrict E
 11840   * @module material.components.input
 11841   *
 11842   * @description
 11843   * You can use any `<input>` or `<textarea>` element as a child of an `<md-input-container>`. This
 11844   * allows you to build complex forms for data entry.
 11845   *
 11846   * @param {number=} md-maxlength The maximum number of characters allowed in this input. If this is
 11847   *   specified, a character counter will be shown underneath the input.<br/><br/>
 11848   *   The purpose of **`md-maxlength`** is exactly to show the max length counter text. If you don't
 11849   *   want the counter text and only need "plain" validation, you can use the "simple" `ng-maxlength`
 11850   *   or maxlength attributes.
 11851   * @param {string=} aria-label Aria-label is required when no label is present.  A warning message
 11852   *   will be logged in the console if not present.
 11853   * @param {string=} placeholder An alternative approach to using aria-label when the label is not
 11854   *   PRESENT. The placeholder text is copied to the aria-label attribute.
 11855   * @param md-no-autogrow {boolean=} When present, textareas will not grow automatically.
 11856   * @param md-detect-hidden {boolean=} When present, textareas will be sized properly when they are
 11857   *   revealed after being hidden. This is off by default for performance reasons because it
 11858   *   guarantees a reflow every digest cycle.
 11859   *
 11860   * @usage
 11861   * <hljs lang="html">
 11862   * <md-input-container>
 11863   *   <label>Color</label>
 11864   *   <input type="text" ng-model="color" required md-maxlength="10">
 11865   * </md-input-container>
 11866   * </hljs>
 11867   *
 11868   * <h3>With Errors</h3>
 11869   *
 11870   * `md-input-container` also supports errors using the standard `ng-messages` directives and
 11871   * animates the messages when they become visible using from the `ngEnter`/`ngLeave` events or
 11872   * the `ngShow`/`ngHide` events.
 11873   *
 11874   * By default, the messages will be hidden until the input is in an error state. This is based off
 11875   * of the `md-is-error` expression of the `md-input-container`. This gives the user a chance to
 11876   * fill out the form before the errors become visible.
 11877   *
 11878   * <hljs lang="html">
 11879   * <form name="colorForm">
 11880   *   <md-input-container>
 11881   *     <label>Favorite Color</label>
 11882   *     <input name="favoriteColor" ng-model="favoriteColor" required>
 11883   *     <div ng-messages="userForm.lastName.$error">
 11884   *       <div ng-message="required">This is required!</div>
 11885   *     </div>
 11886   *   </md-input-container>
 11887   * </form>
 11888   * </hljs>
 11889   *
 11890   * We automatically disable this auto-hiding functionality if you provide any of the following
 11891   * visibility directives on the `ng-messages` container:
 11892   *
 11893   *  - `ng-if`
 11894   *  - `ng-show`/`ng-hide`
 11895   *  - `ng-switch-when`/`ng-switch-default`
 11896   *
 11897   * You can also disable this functionality manually by adding the `md-auto-hide="false"` expression
 11898   * to the `ng-messages` container. This may be helpful if you always want to see the error messages
 11899   * or if you are building your own visibilty directive.
 11900   *
 11901   * _<b>Note:</b> The `md-auto-hide` attribute is a static string that is  only checked upon
 11902   * initialization of the `ng-messages` directive to see if it equals the string `false`._
 11903   *
 11904   * <hljs lang="html">
 11905   * <form name="userForm">
 11906   *   <md-input-container>
 11907   *     <label>Last Name</label>
 11908   *     <input name="lastName" ng-model="lastName" required md-maxlength="10" minlength="4">
 11909   *     <div ng-messages="userForm.lastName.$error" ng-show="userForm.lastName.$dirty">
 11910   *       <div ng-message="required">This is required!</div>
 11911   *       <div ng-message="md-maxlength">That's too long!</div>
 11912   *       <div ng-message="minlength">That's too short!</div>
 11913   *     </div>
 11914   *   </md-input-container>
 11915   *   <md-input-container>
 11916   *     <label>Biography</label>
 11917   *     <textarea name="bio" ng-model="biography" required md-maxlength="150"></textarea>
 11918   *     <div ng-messages="userForm.bio.$error" ng-show="userForm.bio.$dirty">
 11919   *       <div ng-message="required">This is required!</div>
 11920   *       <div ng-message="md-maxlength">That's too long!</div>
 11921   *     </div>
 11922   *   </md-input-container>
 11923   *   <md-input-container>
 11924   *     <input aria-label='title' ng-model='title'>
 11925   *   </md-input-container>
 11926   *   <md-input-container>
 11927   *     <input placeholder='title' ng-model='title'>
 11928   *   </md-input-container>
 11929   * </form>
 11930   * </hljs>
 11931   *
 11932   * <h3>Notes</h3>
 11933   *
 11934   * - Requires [ngMessages](https://docs.angularjs.org/api/ngMessages).
 11935   * - Behaves like the [AngularJS input directive](https://docs.angularjs.org/api/ng/directive/input).
 11936   *
 11937   * The `md-input` and `md-input-container` directives use very specific positioning to achieve the
 11938   * error animation effects. Therefore, it is *not* advised to use the Layout system inside of the
 11939   * `<md-input-container>` tags. Instead, use relative or absolute positioning.
 11940   *
 11941   */
 11942  
 11943  function inputTextareaDirective($mdUtil, $window, $mdAria) {
 11944    return {
 11945      restrict: 'E',
 11946      require: ['^?mdInputContainer', '?ngModel'],
 11947      link: postLink
 11948    };
 11949  
 11950    function postLink(scope, element, attr, ctrls) {
 11951  
 11952      var containerCtrl = ctrls[0];
 11953      var hasNgModel = !!ctrls[1];
 11954      var ngModelCtrl = ctrls[1] || $mdUtil.fakeNgModel();
 11955      var isReadonly = angular.isDefined(attr.readonly);
 11956  
 11957      if (!containerCtrl) return;
 11958      if (attr.type === 'hidden') {
 11959        element.attr('aria-hidden', 'true');
 11960        return;
 11961      } else if (containerCtrl.input) {
 11962        throw new Error("<md-input-container> can only have *one* <input>, <textarea> or <md-select> child element!");
 11963      }
 11964      containerCtrl.input = element;
 11965  
 11966      // Add an error spacer div after our input to provide space for the char counter and any ng-messages
 11967      var errorsSpacer = angular.element('<div class="md-errors-spacer">');
 11968      element.after(errorsSpacer);
 11969  
 11970      if (!containerCtrl.label) {
 11971        $mdAria.expect(element, 'aria-label', element.attr('placeholder'));
 11972      }
 11973  
 11974      element.addClass('md-input');
 11975      if (!element.attr('id')) {
 11976        element.attr('id', 'input_' + $mdUtil.nextUid());
 11977      }
 11978  
 11979      if (element[0].tagName.toLowerCase() === 'textarea') {
 11980        setupTextarea();
 11981      }
 11982  
 11983      // If the input doesn't have an ngModel, it may have a static value. For that case,
 11984      // we have to do one initial check to determine if the container should be in the
 11985      // "has a value" state.
 11986      if (!hasNgModel) {
 11987        inputCheckValue();
 11988      }
 11989  
 11990      var isErrorGetter = containerCtrl.isErrorGetter || function() {
 11991        return ngModelCtrl.$invalid && (ngModelCtrl.$touched || isParentFormSubmitted());
 11992      };
 11993  
 11994      var isParentFormSubmitted = function () {
 11995        var parent = $mdUtil.getClosest(element, 'form');
 11996        var form = parent ? angular.element(parent).controller('form') : null;
 11997  
 11998        return form ? form.$submitted : false;
 11999      };
 12000  
 12001      scope.$watch(isErrorGetter, containerCtrl.setInvalid);
 12002  
 12003      ngModelCtrl.$parsers.push(ngModelPipelineCheckValue);
 12004      ngModelCtrl.$formatters.push(ngModelPipelineCheckValue);
 12005  
 12006      element.on('input', inputCheckValue);
 12007  
 12008      if (!isReadonly) {
 12009        element
 12010          .on('focus', function(ev) {
 12011            $mdUtil.nextTick(function() {
 12012              containerCtrl.setFocused(true);
 12013            });
 12014          })
 12015          .on('blur', function(ev) {
 12016            $mdUtil.nextTick(function() {
 12017              containerCtrl.setFocused(false);
 12018              inputCheckValue();
 12019            });
 12020          });
 12021      }
 12022  
 12023      //ngModelCtrl.$setTouched();
 12024      //if( ngModelCtrl.$invalid ) containerCtrl.setInvalid();
 12025  
 12026      scope.$on('$destroy', function() {
 12027        containerCtrl.setFocused(false);
 12028        containerCtrl.setHasValue(false);
 12029        containerCtrl.input = null;
 12030      });
 12031  
 12032      /**
 12033       *
 12034       */
 12035      function ngModelPipelineCheckValue(arg) {
 12036        containerCtrl.setHasValue(!ngModelCtrl.$isEmpty(arg));
 12037        return arg;
 12038      }
 12039  
 12040      function inputCheckValue() {
 12041        // An input's value counts if its length > 0,
 12042        // or if the input's validity state says it has bad input (eg string in a number input)
 12043        containerCtrl.setHasValue(element.val().length > 0 || (element[0].validity || {}).badInput);
 12044      }
 12045  
 12046      function setupTextarea() {
 12047        if (angular.isDefined(element.attr('md-no-autogrow'))) {
 12048          return;
 12049        }
 12050  
 12051        var node = element[0];
 12052        var container = containerCtrl.element[0];
 12053  
 12054        var min_rows = NaN;
 12055        var lineHeight = null;
 12056        // can't check if height was or not explicitly set,
 12057        // so rows attribute will take precedence if present
 12058        if (node.hasAttribute('rows')) {
 12059          min_rows = parseInt(node.getAttribute('rows'));
 12060        }
 12061  
 12062        var onChangeTextarea = $mdUtil.debounce(growTextarea, 1);
 12063  
 12064        function pipelineListener(value) {
 12065          onChangeTextarea();
 12066          return value;
 12067        }
 12068  
 12069        if (ngModelCtrl) {
 12070          ngModelCtrl.$formatters.push(pipelineListener);
 12071          ngModelCtrl.$viewChangeListeners.push(pipelineListener);
 12072        } else {
 12073          onChangeTextarea();
 12074        }
 12075        element.on('keydown input', onChangeTextarea);
 12076  
 12077        if (isNaN(min_rows)) {
 12078          element.attr('rows', '1');
 12079  
 12080          element.on('scroll', onScroll);
 12081        }
 12082  
 12083        angular.element($window).on('resize', onChangeTextarea);
 12084  
 12085        scope.$on('$destroy', function() {
 12086          angular.element($window).off('resize', onChangeTextarea);
 12087        });
 12088  
 12089        function growTextarea() {
 12090          // sets the md-input-container height to avoid jumping around
 12091          container.style.height = container.offsetHeight + 'px';
 12092  
 12093          // temporarily disables element's flex so its height 'runs free'
 12094          element.addClass('md-no-flex');
 12095  
 12096          if (isNaN(min_rows)) {
 12097            node.style.height = "auto";
 12098            node.scrollTop = 0;
 12099            var height = getHeight();
 12100            if (height) node.style.height = height + 'px';
 12101          } else {
 12102            node.setAttribute("rows", 1);
 12103  
 12104            if (!lineHeight) {
 12105              node.style.minHeight = '0';
 12106  
 12107              lineHeight = element.prop('clientHeight');
 12108  
 12109              node.style.minHeight = null;
 12110            }
 12111  
 12112            var rows = Math.min(min_rows, Math.round(node.scrollHeight / lineHeight));
 12113            node.setAttribute("rows", rows);
 12114            node.style.height = lineHeight * rows + "px";
 12115          }
 12116  
 12117          // reset everything back to normal
 12118          element.removeClass('md-no-flex');
 12119          container.style.height = 'auto';
 12120        }
 12121  
 12122        function getHeight() {
 12123          var line = node.scrollHeight - node.offsetHeight;
 12124          return node.offsetHeight + (line > 0 ? line : 0);
 12125        }
 12126  
 12127        function onScroll(e) {
 12128          node.scrollTop = 0;
 12129          // for smooth new line adding
 12130          var line = node.scrollHeight - node.offsetHeight;
 12131          var height = node.offsetHeight + line;
 12132          node.style.height = height + 'px';
 12133        }
 12134  
 12135        // Attach a watcher to detect when the textarea gets shown.
 12136        if (angular.isDefined(element.attr('md-detect-hidden'))) {
 12137  
 12138          var handleHiddenChange = function() {
 12139            var wasHidden = false;
 12140  
 12141            return function() {
 12142              var isHidden = node.offsetHeight === 0;
 12143  
 12144              if (isHidden === false && wasHidden === true) {
 12145                growTextarea();
 12146              }
 12147  
 12148              wasHidden = isHidden;
 12149            };
 12150          }();
 12151  
 12152          // Check every digest cycle whether the visibility of the textarea has changed.
 12153          // Queue up to run after the digest cycle is complete.
 12154          scope.$watch(function() {
 12155            $mdUtil.nextTick(handleHiddenChange, false);
 12156            return true;
 12157          });
 12158        }
 12159      }
 12160    }
 12161  }
 12162  inputTextareaDirective.$inject = ["$mdUtil", "$window", "$mdAria"];
 12163  
 12164  function mdMaxlengthDirective($animate, $mdUtil) {
 12165    return {
 12166      restrict: 'A',
 12167      require: ['ngModel', '^mdInputContainer'],
 12168      link: postLink
 12169    };
 12170  
 12171    function postLink(scope, element, attr, ctrls) {
 12172      var maxlength;
 12173      var ngModelCtrl = ctrls[0];
 12174      var containerCtrl = ctrls[1];
 12175      var charCountEl, errorsSpacer;
 12176  
 12177      // Wait until the next tick to ensure that the input has setup the errors spacer where we will
 12178      // append our counter
 12179      $mdUtil.nextTick(function() {
 12180        errorsSpacer = angular.element(containerCtrl.element[0].querySelector('.md-errors-spacer'));
 12181        charCountEl = angular.element('<div class="md-char-counter">');
 12182  
 12183        // Append our character counter inside the errors spacer
 12184        errorsSpacer.append(charCountEl);
 12185  
 12186        // Stop model from trimming. This makes it so whitespace
 12187        // over the maxlength still counts as invalid.
 12188        attr.$set('ngTrim', 'false');
 12189  
 12190        ngModelCtrl.$formatters.push(renderCharCount);
 12191        ngModelCtrl.$viewChangeListeners.push(renderCharCount);
 12192        element.on('input keydown keyup', function() {
 12193          renderCharCount(); //make sure it's called with no args
 12194        });
 12195  
 12196        scope.$watch(attr.mdMaxlength, function(value) {
 12197          maxlength = value;
 12198          if (angular.isNumber(value) && value > 0) {
 12199            if (!charCountEl.parent().length) {
 12200              $animate.enter(charCountEl, errorsSpacer);
 12201            }
 12202            renderCharCount();
 12203          } else {
 12204            $animate.leave(charCountEl);
 12205          }
 12206        });
 12207  
 12208        ngModelCtrl.$validators['md-maxlength'] = function(modelValue, viewValue) {
 12209          if (!angular.isNumber(maxlength) || maxlength < 0) {
 12210            return true;
 12211          }
 12212          return ( modelValue || element.val() || viewValue || '' ).length <= maxlength;
 12213        };
 12214      });
 12215  
 12216      function renderCharCount(value) {
 12217        // If we have not been appended to the body yet; do not render
 12218        if (!charCountEl.parent) {
 12219          return value;
 12220        }
 12221  
 12222        // Force the value into a string since it may be a number,
 12223        // which does not have a length property.
 12224        charCountEl.text(String(element.val() || value || '').length + '/' + maxlength);
 12225        return value;
 12226      }
 12227    }
 12228  }
 12229  mdMaxlengthDirective.$inject = ["$animate", "$mdUtil"];
 12230  
 12231  function placeholderDirective($log) {
 12232    return {
 12233      restrict: 'A',
 12234      require: '^^?mdInputContainer',
 12235      priority: 200,
 12236      link: postLink
 12237    };
 12238  
 12239    function postLink(scope, element, attr, inputContainer) {
 12240      // If there is no input container, just return
 12241      if (!inputContainer) return;
 12242  
 12243      var label = inputContainer.element.find('label');
 12244      var hasNoFloat = angular.isDefined(inputContainer.element.attr('md-no-float'));
 12245  
 12246      // If we have a label, or they specify the md-no-float attribute, just return
 12247      if ((label && label.length) || hasNoFloat) {
 12248        // Add a placeholder class so we can target it in the CSS
 12249        inputContainer.setHasPlaceholder(true);
 12250        return;
 12251      }
 12252  
 12253      // Otherwise, grab/remove the placeholder
 12254      var placeholderText = attr.placeholder;
 12255      element.removeAttr('placeholder');
 12256  
 12257      // And add the placeholder text as a separate label
 12258      if (inputContainer.input && inputContainer.input[0].nodeName != 'MD-SELECT') {
 12259        var placeholder = '<label ng-click="delegateClick()">' + placeholderText + '</label>';
 12260  
 12261        inputContainer.element.addClass('md-icon-float');
 12262        inputContainer.element.prepend(placeholder);
 12263      }
 12264    }
 12265  }
 12266  placeholderDirective.$inject = ["$log"];
 12267  
 12268  /**
 12269   * @ngdoc directive
 12270   * @name mdSelectOnFocus
 12271   * @module material.components.input
 12272   *
 12273   * @restrict A
 12274   *
 12275   * @description
 12276   * The `md-select-on-focus` directive allows you to automatically select the element's input text on focus.
 12277   *
 12278   * <h3>Notes</h3>
 12279   * - The use of `md-select-on-focus` is restricted to `<input>` and `<textarea>` elements.
 12280   *
 12281   * @usage
 12282   * <h3>Using with an Input</h3>
 12283   * <hljs lang="html">
 12284   *
 12285   * <md-input-container>
 12286   *   <label>Auto Select</label>
 12287   *   <input type="text" md-select-on-focus>
 12288   * </md-input-container>
 12289   * </hljs>
 12290   *
 12291   * <h3>Using with a Textarea</h3>
 12292   * <hljs lang="html">
 12293   *
 12294   * <md-input-container>
 12295   *   <label>Auto Select</label>
 12296   *   <textarea md-select-on-focus>This text will be selected on focus.</textarea>
 12297   * </md-input-container>
 12298   *
 12299   * </hljs>
 12300   */
 12301  function mdSelectOnFocusDirective() {
 12302  
 12303    return {
 12304      restrict: 'A',
 12305      link: postLink
 12306    };
 12307  
 12308    function postLink(scope, element, attr) {
 12309      if (element[0].nodeName !== 'INPUT' && element[0].nodeName !== "TEXTAREA") return;
 12310  
 12311      element.on('focus', onFocus);
 12312  
 12313      scope.$on('$destroy', function() {
 12314        element.off('focus', onFocus);
 12315      });
 12316  
 12317      function onFocus() {
 12318        // Use HTMLInputElement#select to fix firefox select issues
 12319        element[0].select();
 12320      }
 12321    }
 12322  }
 12323  
 12324  var visibilityDirectives = ['ngIf', 'ngShow', 'ngHide', 'ngSwitchWhen', 'ngSwitchDefault'];
 12325  function ngMessagesDirective() {
 12326    return {
 12327      restrict: 'EA',
 12328      link: postLink,
 12329  
 12330      // This is optional because we don't want target *all* ngMessage instances, just those inside of
 12331      // mdInputContainer.
 12332      require: '^^?mdInputContainer'
 12333    };
 12334  
 12335    function postLink(scope, element, attrs, inputContainer) {
 12336      // If we are not a child of an input container, don't do anything
 12337      if (!inputContainer) return;
 12338  
 12339      // Add our animation class
 12340      element.toggleClass('md-input-messages-animation', true);
 12341  
 12342      // Add our md-auto-hide class to automatically hide/show messages when container is invalid
 12343      element.toggleClass('md-auto-hide', true);
 12344  
 12345      // If we see some known visibility directives, remove the md-auto-hide class
 12346      if (attrs.mdAutoHide == 'false' || hasVisibiltyDirective(attrs)) {
 12347        element.toggleClass('md-auto-hide', false);
 12348      }
 12349    }
 12350  
 12351    function hasVisibiltyDirective(attrs) {
 12352      return visibilityDirectives.some(function(attr) {
 12353        return attrs[attr];
 12354      });
 12355    }
 12356  }
 12357  
 12358  function ngMessageDirective($mdUtil) {
 12359    return {
 12360      restrict: 'EA',
 12361      compile: compile,
 12362      priority: 100
 12363    };
 12364  
 12365    function compile(element) {
 12366      var inputContainer = $mdUtil.getClosest(element, "md-input-container");
 12367  
 12368      // If we are not a child of an input container, don't do anything
 12369      if (!inputContainer) return;
 12370  
 12371      // Add our animation class
 12372      element.toggleClass('md-input-message-animation', true);
 12373  
 12374      return {};
 12375    }
 12376  }
 12377  ngMessageDirective.$inject = ["$mdUtil"];
 12378  
 12379  function mdInputInvalidMessagesAnimation($q, $animateCss) {
 12380    return {
 12381      addClass: function(element, className, done) {
 12382        var messages = getMessagesElement(element);
 12383  
 12384        if (className == "md-input-invalid" && messages.hasClass('md-auto-hide')) {
 12385          showInputMessages(element, $animateCss, $q).finally(done);
 12386        } else {
 12387          done();
 12388        }
 12389      }
 12390  
 12391      // NOTE: We do not need the removeClass method, because the message ng-leave animation will fire
 12392    }
 12393  }
 12394  mdInputInvalidMessagesAnimation.$inject = ["$q", "$animateCss"];
 12395  
 12396  function ngMessagesAnimation($q, $animateCss) {
 12397    return {
 12398      enter: function(element, done) {
 12399        showInputMessages(element, $animateCss, $q).finally(done);
 12400      },
 12401  
 12402      leave: function(element, done) {
 12403        hideInputMessages(element, $animateCss, $q).finally(done);
 12404      },
 12405  
 12406      addClass: function(element, className, done) {
 12407        if (className == "ng-hide") {
 12408          hideInputMessages(element, $animateCss, $q).finally(done);
 12409        } else {
 12410          done();
 12411        }
 12412      },
 12413  
 12414      removeClass: function(element, className, done) {
 12415        if (className == "ng-hide") {
 12416          showInputMessages(element, $animateCss, $q).finally(done);
 12417        } else {
 12418          done();
 12419        }
 12420      }
 12421    }
 12422  }
 12423  ngMessagesAnimation.$inject = ["$q", "$animateCss"];
 12424  
 12425  function ngMessageAnimation($animateCss) {
 12426    return {
 12427      enter: function(element, done) {
 12428        var messages = getMessagesElement(element);
 12429  
 12430        // If we have the md-auto-hide class, the md-input-invalid animation will fire, so we can skip
 12431        if (messages.hasClass('md-auto-hide')) {
 12432          done();
 12433          return;
 12434        }
 12435  
 12436        return showMessage(element, $animateCss);
 12437      },
 12438  
 12439      leave: function(element, done) {
 12440        return hideMessage(element, $animateCss);
 12441      }
 12442    }
 12443  }
 12444  ngMessageAnimation.$inject = ["$animateCss"];
 12445  
 12446  function showInputMessages(element, $animateCss, $q) {
 12447    var animators = [], animator;
 12448    var messages = getMessagesElement(element);
 12449  
 12450    angular.forEach(messages.children(), function(child) {
 12451      animator = showMessage(angular.element(child), $animateCss);
 12452  
 12453      animators.push(animator.start());
 12454    });
 12455  
 12456    return $q.all(animators);
 12457  }
 12458  
 12459  function hideInputMessages(element, $animateCss, $q) {
 12460    var animators = [], animator;
 12461    var messages = getMessagesElement(element);
 12462  
 12463    angular.forEach(messages.children(), function(child) {
 12464      animator = hideMessage(angular.element(child), $animateCss);
 12465  
 12466      animators.push(animator.start());
 12467    });
 12468  
 12469    return $q.all(animators);
 12470  }
 12471  
 12472  function showMessage(element, $animateCss) {
 12473    var height = element[0].offsetHeight;
 12474  
 12475    return $animateCss(element, {
 12476      event: 'enter',
 12477      structural: true,
 12478      from: {"opacity": 0, "margin-top": -height + "px"},
 12479      to: {"opacity": 1, "margin-top": "0"},
 12480      duration: 0.3
 12481    });
 12482  }
 12483  
 12484  function hideMessage(element, $animateCss) {
 12485    var height = element[0].offsetHeight;
 12486    var styles = window.getComputedStyle(element[0]);
 12487  
 12488    // If we are already hidden, just return an empty animation
 12489    if (styles.opacity == 0) {
 12490      return $animateCss(element, {});
 12491    }
 12492  
 12493    // Otherwise, animate
 12494    return $animateCss(element, {
 12495      event: 'leave',
 12496      structural: true,
 12497      from: {"opacity": 1, "margin-top": 0},
 12498      to: {"opacity": 0, "margin-top": -height + "px"},
 12499      duration: 0.3
 12500    });
 12501  }
 12502  
 12503  function getInputElement(element) {
 12504    var inputContainer = element.controller('mdInputContainer');
 12505  
 12506    return inputContainer.element;
 12507  }
 12508  
 12509  function getMessagesElement(element) {
 12510    var input = getInputElement(element);
 12511    var selector = 'ng-messages,data-ng-messages,x-ng-messages,' +
 12512      '[ng-messages],[data-ng-messages],[x-ng-messages]';
 12513  
 12514    return angular.element(input[0].querySelector(selector));
 12515  }
 12516  
 12517  })();
 12518  (function(){
 12519  "use strict";
 12520  
 12521  /**
 12522   * @ngdoc module
 12523   * @name material.components.menu
 12524   */
 12525  
 12526  angular.module('material.components.menu', [
 12527    'material.core',
 12528    'material.components.backdrop'
 12529  ]);
 12530  
 12531  })();
 12532  (function(){
 12533  "use strict";
 12534  
 12535  /**
 12536   * @ngdoc module
 12537   * @name material.components.list
 12538   * @description
 12539   * List module
 12540   */
 12541  angular.module('material.components.list', [
 12542    'material.core'
 12543  ])
 12544    .controller('MdListController', MdListController)
 12545    .directive('mdList', mdListDirective)
 12546    .directive('mdListItem', mdListItemDirective);
 12547  
 12548  /**
 12549   * @ngdoc directive
 12550   * @name mdList
 12551   * @module material.components.list
 12552   *
 12553   * @restrict E
 12554   *
 12555   * @description
 12556   * The `<md-list>` directive is a list container for 1..n `<md-list-item>` tags.
 12557   *
 12558   * @usage
 12559   * <hljs lang="html">
 12560   * <md-list>
 12561   *   <md-list-item class="md-2-line" ng-repeat="item in todos">
 12562   *     <md-checkbox ng-model="item.done"></md-checkbox>
 12563   *     <div class="md-list-item-text">
 12564   *       <h3>{{item.title}}</h3>
 12565   *       <p>{{item.description}}</p>
 12566   *     </div>
 12567   *   </md-list-item>
 12568   * </md-list>
 12569   * </hljs>
 12570   */
 12571  
 12572  function mdListDirective($mdTheming) {
 12573    return {
 12574      restrict: 'E',
 12575      compile: function(tEl) {
 12576        tEl[0].setAttribute('role', 'list');
 12577        return $mdTheming;
 12578      }
 12579    };
 12580  }
 12581  mdListDirective.$inject = ["$mdTheming"];
 12582  /**
 12583   * @ngdoc directive
 12584   * @name mdListItem
 12585   * @module material.components.list
 12586   *
 12587   * @restrict E
 12588   *
 12589   * @description
 12590   * The `<md-list-item>` directive is a container intended for row items in a `<md-list>` container.
 12591   * The `md-2-line` and `md-3-line` classes can be added to a `<md-list-item>` 
 12592   * to increase the height with 22px and 40px respectively.
 12593   *
 12594   * ## CSS
 12595   * `.md-avatar` - class for image avatars
 12596   *
 12597   * `.md-avatar-icon` - class for icon avatars
 12598   *
 12599   * `.md-offset` - on content without an avatar
 12600   *
 12601   * @usage
 12602   * <hljs lang="html">
 12603   *  <md-list>
 12604   *    <md-list-item>
 12605   *      <img class="md-avatar" ng-src="path/to/img"/>
 12606   *      <span>Item content in list</span>
 12607   *    </md-list-item>
 12608   *    <md-list-item>
 12609   *      <md-icon class="md-avatar-icon" md-svg-icon="communication:phone"></md-icon>
 12610   *      <span>Item content in list</span>
 12611   *    </md-list-item>
 12612   *  </md-list>
 12613   * </hljs>
 12614   *
 12615   * _**Note:** We automatically apply special styling when the inner contents are wrapped inside
 12616   * of a `<md-button>` tag. This styling is automatically ignored for `class="md-secondary"` buttons
 12617   * and you can include a class of `class="md-exclude"` if you need to use a non-secondary button
 12618   * that is inside the list, but does not wrap the contents._
 12619   */
 12620  function mdListItemDirective($mdAria, $mdConstant, $mdUtil, $timeout) {
 12621    var proxiedTypes = ['md-checkbox', 'md-switch'];
 12622    return {
 12623      restrict: 'E',
 12624      controller: 'MdListController',
 12625      compile: function(tEl, tAttrs) {
 12626        // Check for proxy controls (no ng-click on parent, and a control inside)
 12627        var secondaryItem = tEl[0].querySelector('.md-secondary');
 12628        var hasProxiedElement;
 12629        var proxyElement;
 12630  
 12631        tEl[0].setAttribute('role', 'listitem');
 12632  
 12633        if (tAttrs.ngClick || tAttrs.ngHref || tAttrs.href || tAttrs.uiSref || tAttrs.ngAttrUiSref) {
 12634          wrapIn('button');
 12635        } else {
 12636          for (var i = 0, type; type = proxiedTypes[i]; ++i) {
 12637            if (proxyElement = tEl[0].querySelector(type)) {
 12638              hasProxiedElement = true;
 12639              break;
 12640            }
 12641          }
 12642          if (hasProxiedElement) {
 12643            wrapIn('div');
 12644          } else if (!tEl[0].querySelector('md-button:not(.md-secondary):not(.md-exclude)')) {
 12645            tEl.addClass('md-no-proxy');
 12646          }
 12647        }
 12648        wrapSecondary();
 12649        setupToggleAria();
 12650  
 12651  
 12652        function setupToggleAria() {
 12653          var toggleTypes = ['md-switch', 'md-checkbox'];
 12654          var toggle;
 12655  
 12656          for (var i = 0, toggleType; toggleType = toggleTypes[i]; ++i) {
 12657            if (toggle = tEl.find(toggleType)[0]) {
 12658              if (!toggle.hasAttribute('aria-label')) {
 12659                var p = tEl.find('p')[0];
 12660                if (!p) return;
 12661                toggle.setAttribute('aria-label', 'Toggle ' + p.textContent);
 12662              }
 12663            }
 12664          }
 12665        }
 12666  
 12667        function wrapIn(type) {
 12668          var container;
 12669          if (type == 'div') {
 12670            container = angular.element('<div class="md-no-style md-list-item-inner">');
 12671            container.append(tEl.contents());
 12672            tEl.addClass('md-proxy-focus');
 12673          } else {
 12674            container = angular.element('<md-button class="md-no-style"><div class="md-list-item-inner"></div></md-button>');
 12675            copyAttributes(tEl[0], container[0]);
 12676            container.children().eq(0).append(tEl.contents());
 12677          }
 12678  
 12679          tEl[0].setAttribute('tabindex', '-1');
 12680          tEl.append(container);
 12681        }
 12682  
 12683        function wrapSecondary() {
 12684          if (secondaryItem && !isButton(secondaryItem) && secondaryItem.hasAttribute('ng-click')) {
 12685            $mdAria.expect(secondaryItem, 'aria-label');
 12686            var buttonWrapper = angular.element('<md-button class="md-secondary-container md-icon-button">');
 12687            copyAttributes(secondaryItem, buttonWrapper[0]);
 12688            secondaryItem.setAttribute('tabindex', '-1');
 12689            secondaryItem.classList.remove('md-secondary');
 12690            buttonWrapper.append(secondaryItem);
 12691            secondaryItem = buttonWrapper[0];
 12692          }
 12693  
 12694          // Check for a secondary item and move it outside
 12695          if ( secondaryItem && (
 12696              secondaryItem.hasAttribute('ng-click') ||
 12697              ( tAttrs.ngClick &&
 12698              isProxiedElement(secondaryItem) )
 12699            )) {
 12700            tEl.addClass('md-with-secondary');
 12701            tEl.append(secondaryItem);
 12702          }
 12703        }
 12704  
 12705        function copyAttributes(item, wrapper) {
 12706          var copiedAttrs = ['ng-if', 'ng-click', 'aria-label', 'ng-disabled',
 12707            'ui-sref', 'href', 'ng-href', 'ng-attr-ui-sref'];
 12708          angular.forEach(copiedAttrs, function(attr) {
 12709            if (item.hasAttribute(attr)) {
 12710              wrapper.setAttribute(attr, item.getAttribute(attr));
 12711              item.removeAttribute(attr);
 12712            }
 12713          });
 12714        }
 12715  
 12716        function isProxiedElement(el) {
 12717          return proxiedTypes.indexOf(el.nodeName.toLowerCase()) != -1;
 12718        }
 12719  
 12720        function isButton(el) {
 12721          var nodeName = el.nodeName.toUpperCase();
 12722  
 12723          return nodeName == "MD-BUTTON" || nodeName == "BUTTON";
 12724        }
 12725  
 12726        return postLink;
 12727  
 12728        function postLink($scope, $element, $attr, ctrl) {
 12729  
 12730          var proxies    = [],
 12731              firstChild = $element[0].firstElementChild,
 12732              hasClick   = firstChild && hasClickEvent(firstChild);
 12733  
 12734          computeProxies();
 12735          computeClickable();
 12736  
 12737          if ($element.hasClass('md-proxy-focus') && proxies.length) {
 12738            angular.forEach(proxies, function(proxy) {
 12739              proxy = angular.element(proxy);
 12740  
 12741              $scope.mouseActive = false;
 12742              proxy.on('mousedown', function() {
 12743                $scope.mouseActive = true;
 12744                $timeout(function(){
 12745                  $scope.mouseActive = false;
 12746                }, 100);
 12747              })
 12748              .on('focus', function() {
 12749                if ($scope.mouseActive === false) { $element.addClass('md-focused'); }
 12750                proxy.on('blur', function proxyOnBlur() {
 12751                  $element.removeClass('md-focused');
 12752                  proxy.off('blur', proxyOnBlur);
 12753                });
 12754              });
 12755            });
 12756          }
 12757  
 12758          function hasClickEvent (element) {
 12759            var attr = element.attributes;
 12760            for (var i = 0; i < attr.length; i++) {
 12761              if ($attr.$normalize(attr[i].name) === 'ngClick') return true;
 12762            }
 12763            return false;
 12764          }
 12765  
 12766          function computeProxies() {
 12767            var children = $element.children();
 12768            if (children.length && !children[0].hasAttribute('ng-click')) {
 12769              angular.forEach(proxiedTypes, function(type) {
 12770                angular.forEach(firstChild.querySelectorAll(type), function(child) {
 12771                  proxies.push(child);
 12772                });
 12773              });
 12774            }
 12775          }
 12776          function computeClickable() {
 12777            if (proxies.length == 1 || hasClick) {
 12778              $element.addClass('md-clickable');
 12779  
 12780              if (!hasClick) {
 12781                ctrl.attachRipple($scope, angular.element($element[0].querySelector('.md-no-style')));
 12782              }
 12783            }
 12784          }
 12785  
 12786          var firstChildKeypressListener = function(e) {
 12787            if (e.target.nodeName != 'INPUT' && e.target.nodeName != 'TEXTAREA' && !e.target.isContentEditable) {
 12788              var keyCode = e.which || e.keyCode;
 12789              if (keyCode == $mdConstant.KEY_CODE.SPACE) {
 12790                if (firstChild) {
 12791                  firstChild.click();
 12792                  e.preventDefault();
 12793                  e.stopPropagation();
 12794                }
 12795              }
 12796            }
 12797          };
 12798  
 12799          if (!hasClick && !proxies.length) {
 12800            firstChild && firstChild.addEventListener('keypress', firstChildKeypressListener);
 12801          }
 12802  
 12803          $element.off('click');
 12804          $element.off('keypress');
 12805  
 12806          if (proxies.length == 1 && firstChild) {
 12807            $element.children().eq(0).on('click', function(e) {
 12808              var parentButton = $mdUtil.getClosest(e.target, 'BUTTON');
 12809              if (!parentButton && firstChild.contains(e.target)) {
 12810                angular.forEach(proxies, function(proxy) {
 12811                  if (e.target !== proxy && !proxy.contains(e.target)) {
 12812                    angular.element(proxy).triggerHandler('click');
 12813                  }
 12814                });
 12815              }
 12816            });
 12817          }
 12818  
 12819          $scope.$on('$destroy', function () {
 12820            firstChild && firstChild.removeEventListener('keypress', firstChildKeypressListener);
 12821          });
 12822        }
 12823      }
 12824    };
 12825  }
 12826  mdListItemDirective.$inject = ["$mdAria", "$mdConstant", "$mdUtil", "$timeout"];
 12827  
 12828  /*
 12829   * @private
 12830   * @ngdoc controller
 12831   * @name MdListController
 12832   * @module material.components.list
 12833   *
 12834   */
 12835  function MdListController($scope, $element, $mdListInkRipple) {
 12836    var ctrl = this;
 12837    ctrl.attachRipple = attachRipple;
 12838  
 12839    function attachRipple (scope, element) {
 12840      var options = {};
 12841      $mdListInkRipple.attach(scope, element, options);
 12842    }
 12843  }
 12844  MdListController.$inject = ["$scope", "$element", "$mdListInkRipple"];
 12845  
 12846  
 12847  })();
 12848  (function(){
 12849  "use strict";
 12850  
 12851  /**
 12852   * @ngdoc module
 12853   * @name material.components.menu-bar
 12854   */
 12855  
 12856  angular.module('material.components.menuBar', [
 12857    'material.core',
 12858    'material.components.menu'
 12859  ]);
 12860  
 12861  })();
 12862  (function(){
 12863  "use strict";
 12864  
 12865  /**
 12866   * @ngdoc module
 12867   * @name material.components.progressCircular
 12868   * @description Circular Progress module!
 12869   */
 12870  angular.module('material.components.progressCircular', [
 12871    'material.core'
 12872  ])
 12873    .directive('mdProgressCircular', MdProgressCircularDirective);
 12874  
 12875  /**
 12876   * @ngdoc directive
 12877   * @name mdProgressCircular
 12878   * @module material.components.progressCircular
 12879   * @restrict E
 12880   *
 12881  * @description
 12882   * The circular progress directive is used to make loading content in your app as delightful and
 12883   * painless as possible by minimizing the amount of visual change a user sees before they can view
 12884   * and interact with content.
 12885   *
 12886   * For operations where the percentage of the operation completed can be determined, use a
 12887   * determinate indicator. They give users a quick sense of how long an operation will take.
 12888   *
 12889   * For operations where the user is asked to wait a moment while something finishes up, and it’s
 12890   * not necessary to expose what's happening behind the scenes and how long it will take, use an
 12891   * indeterminate indicator.
 12892   *
 12893   * @param {string} md-mode Select from one of two modes: **'determinate'** and **'indeterminate'**.
 12894   *
 12895   * Note: if the `md-mode` value is set as undefined or specified as not 1 of the two (2) valid modes, then `.ng-hide`
 12896   * will be auto-applied as a style to the component.
 12897   *
 12898   * Note: if not configured, the `md-mode="indeterminate"` will be auto injected as an attribute.
 12899   * If `value=""` is also specified, however, then `md-mode="determinate"` would be auto-injected instead.
 12900   * @param {number=} value In determinate mode, this number represents the percentage of the
 12901   *     circular progress. Default: 0
 12902   * @param {number=} md-diameter This specifies the diameter of the circular progress. The value
 12903   * may be a percentage (eg '25%') or a pixel-size value (eg '48'). If this attribute is
 12904   * not present then a default value of '48px' is assumed.
 12905   *
 12906   * @usage
 12907   * <hljs lang="html">
 12908   * <md-progress-circular md-mode="determinate" value="..."></md-progress-circular>
 12909   *
 12910   * <md-progress-circular md-mode="determinate" ng-value="..."></md-progress-circular>
 12911   *
 12912   * <md-progress-circular md-mode="determinate" value="..." md-diameter="100"></md-progress-circular>
 12913   *
 12914   * <md-progress-circular md-mode="indeterminate"></md-progress-circular>
 12915   * </hljs>
 12916   */
 12917  function MdProgressCircularDirective($mdTheming, $mdUtil, $log) {
 12918    var DEFAULT_PROGRESS_SIZE = 100;
 12919    var DEFAULT_SCALING = 0.5;
 12920  
 12921    var MODE_DETERMINATE = "determinate",
 12922        MODE_INDETERMINATE = "indeterminate";
 12923  
 12924  
 12925    return {
 12926      restrict: 'E',
 12927      scope : true,
 12928      template:
 12929          // The progress 'circle' is composed of two half-circles: the left side and the right
 12930          // side. Each side has CSS applied to 'fill-in' the half-circle to the appropriate progress.
 12931          '<div class="md-scale-wrapper">' +
 12932            '<div class="md-spinner-wrapper">' +
 12933              '<div class="md-inner">' +
 12934                '<div class="md-gap"></div>' +
 12935                '<div class="md-left">' +
 12936                  '<div class="md-half-circle"></div>' +
 12937                '</div>' +
 12938                '<div class="md-right">' +
 12939                  '<div class="md-half-circle"></div>' +
 12940                '</div>' +
 12941              '</div>' +
 12942            '</div>' +
 12943          '</div>',
 12944      compile: compile
 12945    };
 12946  
 12947    function compile(tElement) {
 12948      // The javascript in this file is mainly responsible for setting the correct aria attributes.
 12949      // The animation of the progress spinner is done entirely with just CSS.
 12950      tElement.attr('aria-valuemin', 0);
 12951      tElement.attr('aria-valuemax', 100);
 12952      tElement.attr('role', 'progressbar');
 12953  
 12954      return postLink;
 12955    }
 12956  
 12957    function postLink(scope, element, attr) {
 12958      $mdTheming(element);
 12959  
 12960      var circle = element;
 12961      var spinnerWrapper =  angular.element(element.children()[0]);
 12962      var lastMode, toVendorCSS = $mdUtil.dom.animator.toCss;
 12963  
 12964      element.attr('md-mode', mode());
 12965  
 12966      updateScale();
 12967      validateMode();
 12968      watchAttributes();
 12969  
 12970      /**
 12971       * Watch the value and md-mode attributes
 12972       */
 12973      function watchAttributes() {
 12974       attr.$observe('value', function(value) {
 12975             var percentValue = clamp(value);
 12976             element.attr('aria-valuenow', percentValue);
 12977  
 12978             if (mode() == MODE_DETERMINATE) {
 12979               animateIndicator(percentValue);
 12980             }
 12981           });
 12982       attr.$observe('mdMode',function(mode){
 12983         switch( mode ) {
 12984           case MODE_DETERMINATE:
 12985           case MODE_INDETERMINATE:
 12986             spinnerWrapper.removeClass('ng-hide');
 12987             if (lastMode) spinnerWrapper.removeClass(lastMode);
 12988             spinnerWrapper.addClass( lastMode = "md-mode-" + mode );
 12989             break;
 12990           default:
 12991             if (lastMode) spinnerWrapper.removeClass( lastMode );
 12992             spinnerWrapper.addClass('ng-hide');
 12993             lastMode = undefined;
 12994             break;
 12995         }
 12996       });
 12997      }
 12998  
 12999      /**
 13000       * Update size/scaling of the progress indicator
 13001       * Watch the "value" and "md-mode" attributes
 13002       */
 13003      function updateScale() {
 13004        // set the outer container to the size the user specified
 13005        circle.css({
 13006          width: (100 * getDiameterRatio()) + 'px',
 13007          height: (100 * getDiameterRatio()) + 'px'
 13008        });
 13009        // the internal element is still 100px, so we have to scale it down to match the size
 13010        circle.children().eq(0).css(toVendorCSS({
 13011          transform : $mdUtil.supplant('translate(-50%, -50%) scale( {0} )',[getDiameterRatio()])
 13012        }));
 13013      }
 13014  
 13015      /**
 13016       * Auto-defaults the mode to either `determinate` or `indeterminate` mode; if not specified
 13017       */
 13018      function validateMode() {
 13019        if ( angular.isUndefined(attr.mdMode) ) {
 13020          var hasValue = angular.isDefined(attr.value);
 13021          var mode = hasValue ? MODE_DETERMINATE : MODE_INDETERMINATE;
 13022          var info = "Auto-adding the missing md-mode='{0}' to the ProgressCircular element";
 13023  
 13024          $log.debug( $mdUtil.supplant(info, [mode]) );
 13025  
 13026          element.attr("md-mode",mode);
 13027          attr['mdMode'] = mode;
 13028        }
 13029      }
 13030  
 13031      var leftC, rightC, gap;
 13032  
 13033      /**
 13034       * Manually animate the Determinate indicator based on the specified
 13035       * percentage value (0-100).
 13036       *
 13037       * Note: this animation was previously done using SCSS.
 13038       * - generated 54K of styles
 13039       * - use attribute selectors which had poor performances in IE
 13040       */
 13041      function animateIndicator(value) {
 13042        if ( !mode() ) return;
 13043  
 13044        leftC  = leftC  || angular.element(element[0].querySelector('.md-left > .md-half-circle'));
 13045        rightC = rightC || angular.element(element[0].querySelector('.md-right > .md-half-circle'));
 13046        gap    = gap    || angular.element(element[0].querySelector('.md-gap'));
 13047  
 13048        var gapStyles = removeEmptyValues({
 13049            borderBottomColor: (value <= 50) ? "transparent !important" : "",
 13050            transition: (value <= 50) ? "" : "borderBottomColor 0.1s linear"
 13051          }),
 13052          leftStyles = removeEmptyValues({
 13053            transition: (value <= 50) ? "transform 0.1s linear" : "",
 13054            transform: $mdUtil.supplant("rotate({0}deg)", [value <= 50 ? 135 : (((value - 50) / 50 * 180) + 135)])
 13055          }),
 13056          rightStyles = removeEmptyValues({
 13057            transition: (value >= 50) ? "transform 0.1s linear" : "",
 13058            transform: $mdUtil.supplant("rotate({0}deg)", [value >= 50 ? 45 : (value / 50 * 180 - 135)])
 13059          });
 13060  
 13061        leftC.css(toVendorCSS(leftStyles));
 13062        rightC.css(toVendorCSS(rightStyles));
 13063        gap.css(toVendorCSS(gapStyles));
 13064  
 13065      }
 13066  
 13067      /**
 13068       * We will scale the progress circle based on the default diameter.
 13069       *
 13070       * Determine the diameter percentage (defaults to 100%)
 13071       * May be express as float, percentage, or integer
 13072       */
 13073      function getDiameterRatio() {
 13074        if ( !attr.mdDiameter ) return DEFAULT_SCALING;
 13075  
 13076        var match = /([0-9]*)%/.exec(attr.mdDiameter);
 13077        var value = Math.max(0, (match && match[1]/100) || parseFloat(attr.mdDiameter));
 13078  
 13079        // should return ratio; DEFAULT_PROGRESS_SIZE === 100px is default size
 13080        return  (value > 1) ? value / DEFAULT_PROGRESS_SIZE : value;
 13081      }
 13082  
 13083      /**
 13084       * Is the md-mode a valid option?
 13085       */
 13086      function mode() {
 13087        var value = (attr.mdMode || "").trim();
 13088        if ( value ) {
 13089          switch(value) {
 13090            case MODE_DETERMINATE :
 13091            case MODE_INDETERMINATE :
 13092              break;
 13093            default:
 13094              value = undefined;
 13095              break;
 13096          }
 13097        }
 13098        return value;
 13099      }
 13100  
 13101    }
 13102  
 13103    /**
 13104     * Clamps the value to be between 0 and 100.
 13105     * @param {number} value The value to clamp.
 13106     * @returns {number}
 13107     */
 13108    function clamp(value) {
 13109      return Math.max(0, Math.min(value || 0, 100));
 13110    }
 13111  
 13112    function removeEmptyValues(target) {
 13113      for (var key in target) {
 13114        if (target.hasOwnProperty(key)) {
 13115          if ( target[key] == "" ) delete target[key];
 13116        }
 13117      }
 13118  
 13119      return target;
 13120    }
 13121  }
 13122  MdProgressCircularDirective.$inject = ["$mdTheming", "$mdUtil", "$log"];
 13123  
 13124  })();
 13125  (function(){
 13126  "use strict";
 13127  
 13128  /**
 13129   * @ngdoc module
 13130   * @name material.components.progressLinear
 13131   * @description Linear Progress module!
 13132   */
 13133  angular.module('material.components.progressLinear', [
 13134    'material.core'
 13135  ])
 13136    .directive('mdProgressLinear', MdProgressLinearDirective);
 13137  
 13138  /**
 13139   * @ngdoc directive
 13140   * @name mdProgressLinear
 13141   * @module material.components.progressLinear
 13142   * @restrict E
 13143   *
 13144   * @description
 13145   * The linear progress directive is used to make loading content
 13146   * in your app as delightful and painless as possible by minimizing
 13147   * the amount of visual change a user sees before they can view
 13148   * and interact with content.
 13149   *
 13150   * Each operation should only be represented by one activity indicator
 13151   * For example: one refresh operation should not display both a
 13152   * refresh bar and an activity circle.
 13153   *
 13154   * For operations where the percentage of the operation completed
 13155   * can be determined, use a determinate indicator. They give users
 13156   * a quick sense of how long an operation will take.
 13157   *
 13158   * For operations where the user is asked to wait a moment while
 13159   * something finishes up, and it’s not necessary to expose what's
 13160   * happening behind the scenes and how long it will take, use an
 13161   * indeterminate indicator.
 13162   *
 13163   * @param {string} md-mode Select from one of four modes: determinate, indeterminate, buffer or query.
 13164   *
 13165   * Note: if the `md-mode` value is set as undefined or specified as 1 of the four (4) valid modes, then `.ng-hide`
 13166   * will be auto-applied as a style to the component.
 13167   *
 13168   * Note: if not configured, the `md-mode="indeterminate"` will be auto injected as an attribute. If `value=""` is also specified, however,
 13169   * then `md-mode="determinate"` would be auto-injected instead.
 13170   * @param {number=} value In determinate and buffer modes, this number represents the percentage of the primary progress bar. Default: 0
 13171   * @param {number=} md-buffer-value In the buffer mode, this number represents the percentage of the secondary progress bar. Default: 0
 13172   *
 13173   * @usage
 13174   * <hljs lang="html">
 13175   * <md-progress-linear md-mode="determinate" value="..."></md-progress-linear>
 13176   *
 13177   * <md-progress-linear md-mode="determinate" ng-value="..."></md-progress-linear>
 13178   *
 13179   * <md-progress-linear md-mode="indeterminate"></md-progress-linear>
 13180   *
 13181   * <md-progress-linear md-mode="buffer" value="..." md-buffer-value="..."></md-progress-linear>
 13182   *
 13183   * <md-progress-linear md-mode="query"></md-progress-linear>
 13184   * </hljs>
 13185   */
 13186  function MdProgressLinearDirective($mdTheming, $mdUtil, $log) {
 13187    var MODE_DETERMINATE = "determinate",
 13188        MODE_INDETERMINATE = "indeterminate",
 13189        MODE_BUFFER = "buffer",
 13190        MODE_QUERY = "query";
 13191  
 13192    return {
 13193      restrict: 'E',
 13194      template: '<div class="md-container">' +
 13195        '<div class="md-dashed"></div>' +
 13196        '<div class="md-bar md-bar1"></div>' +
 13197        '<div class="md-bar md-bar2"></div>' +
 13198        '</div>',
 13199      compile: compile
 13200    };
 13201    
 13202    function compile(tElement, tAttrs, transclude) {
 13203      tElement.attr('aria-valuemin', 0);
 13204      tElement.attr('aria-valuemax', 100);
 13205      tElement.attr('role', 'progressbar');
 13206  
 13207      return postLink;
 13208    }
 13209    function postLink(scope, element, attr) {
 13210      $mdTheming(element);
 13211  
 13212      var lastMode, toVendorCSS = $mdUtil.dom.animator.toCss;
 13213      var bar1 = angular.element(element[0].querySelector('.md-bar1')),
 13214          bar2 = angular.element(element[0].querySelector('.md-bar2')),
 13215          container = angular.element(element[0].querySelector('.md-container'));
 13216  
 13217      element.attr('md-mode', mode());
 13218  
 13219      validateMode();
 13220      watchAttributes();
 13221  
 13222      /**
 13223       * Watch the value, md-buffer-value, and md-mode attributes
 13224       */
 13225      function watchAttributes() {
 13226        attr.$observe('value', function(value) {
 13227          var percentValue = clamp(value);
 13228          element.attr('aria-valuenow', percentValue);
 13229  
 13230          if (mode() != MODE_QUERY) animateIndicator(bar2, percentValue);
 13231        });
 13232  
 13233        attr.$observe('mdBufferValue', function(value) {
 13234          animateIndicator(bar1, clamp(value));
 13235        });
 13236  
 13237        attr.$observe('mdMode',function(mode){
 13238          switch( mode ) {
 13239            case MODE_QUERY:
 13240            case MODE_BUFFER:
 13241            case MODE_DETERMINATE:
 13242            case MODE_INDETERMINATE:
 13243              container.removeClass( 'ng-hide' + ' ' + lastMode );
 13244              container.addClass( lastMode = "md-mode-" + mode );
 13245              break;
 13246            default:
 13247              if (lastMode) container.removeClass( lastMode );
 13248              container.addClass('ng-hide');
 13249              lastMode = undefined;
 13250              break;
 13251          }
 13252        });
 13253      }
 13254  
 13255      /**
 13256       * Auto-defaults the mode to either `determinate` or `indeterminate` mode; if not specified
 13257       */
 13258      function validateMode() {
 13259        if ( angular.isUndefined(attr.mdMode) ) {
 13260          var hasValue = angular.isDefined(attr.value);
 13261          var mode = hasValue ? MODE_DETERMINATE : MODE_INDETERMINATE;
 13262          var info = "Auto-adding the missing md-mode='{0}' to the ProgressLinear element";
 13263  
 13264          $log.debug( $mdUtil.supplant(info, [mode]) );
 13265  
 13266          element.attr("md-mode",mode);
 13267          attr['mdMode'] = mode;
 13268        }
 13269      }
 13270  
 13271      /**
 13272       * Is the md-mode a valid option?
 13273       */
 13274      function mode() {
 13275        var value = (attr.mdMode || "").trim();
 13276        if ( value ) {
 13277          switch(value) {
 13278            case MODE_DETERMINATE:
 13279            case MODE_INDETERMINATE:
 13280            case MODE_BUFFER:
 13281            case MODE_QUERY:
 13282              break;
 13283            default:
 13284              value = undefined;
 13285              break;
 13286          }
 13287        }
 13288        return value;
 13289      }
 13290  
 13291      /**
 13292       * Manually set CSS to animate the Determinate indicator based on the specified
 13293       * percentage value (0-100).
 13294       */
 13295      function animateIndicator(target, value) {
 13296        if ( !mode() ) return;
 13297  
 13298        var to = $mdUtil.supplant("translateX({0}%) scale({1},1)", [ (value-100)/2, value/100 ]);
 13299        var styles = toVendorCSS({ transform : to });
 13300        angular.element(target).css( styles );
 13301      }
 13302    }
 13303  
 13304    /**
 13305     * Clamps the value to be between 0 and 100.
 13306     * @param {number} value The value to clamp.
 13307     * @returns {number}
 13308     */
 13309    function clamp(value) {
 13310      return Math.max(0, Math.min(value || 0, 100));
 13311    }
 13312  }
 13313  MdProgressLinearDirective.$inject = ["$mdTheming", "$mdUtil", "$log"];
 13314  
 13315  
 13316  })();
 13317  (function(){
 13318  "use strict";
 13319  
 13320  /**
 13321   * @ngdoc module
 13322   * @name material.components.radioButton
 13323   * @description radioButton module!
 13324   */
 13325  angular.module('material.components.radioButton', [
 13326    'material.core'
 13327  ])
 13328    .directive('mdRadioGroup', mdRadioGroupDirective)
 13329    .directive('mdRadioButton', mdRadioButtonDirective);
 13330  
 13331  /**
 13332   * @ngdoc directive
 13333   * @module material.components.radioButton
 13334   * @name mdRadioGroup
 13335   *
 13336   * @restrict E
 13337   *
 13338   * @description
 13339   * The `<md-radio-group>` directive identifies a grouping
 13340   * container for the 1..n grouped radio buttons; specified using nested
 13341   * `<md-radio-button>` tags.
 13342   *
 13343   * As per the [material design spec](http://www.google.com/design/spec/style/color.html#color-ui-color-application)
 13344   * the radio button is in the accent color by default. The primary color palette may be used with
 13345   * the `md-primary` class.
 13346   *
 13347   * Note: `<md-radio-group>` and `<md-radio-button>` handle tabindex differently
 13348   * than the native `<input type='radio'>` controls. Whereas the native controls
 13349   * force the user to tab through all the radio buttons, `<md-radio-group>`
 13350   * is focusable, and by default the `<md-radio-button>`s are not.
 13351   *
 13352   * @param {string} ng-model Assignable angular expression to data-bind to.
 13353   * @param {boolean=} md-no-ink Use of attribute indicates flag to disable ink ripple effects.
 13354   *
 13355   * @usage
 13356   * <hljs lang="html">
 13357   * <md-radio-group ng-model="selected">
 13358   *
 13359   *   <md-radio-button
 13360   *        ng-repeat="d in colorOptions"
 13361   *        ng-value="d.value" aria-label="{{ d.label }}">
 13362   *
 13363   *          {{ d.label }}
 13364   *
 13365   *   </md-radio-button>
 13366   *
 13367   * </md-radio-group>
 13368   * </hljs>
 13369   *
 13370   */
 13371  function mdRadioGroupDirective($mdUtil, $mdConstant, $mdTheming, $timeout) {
 13372    RadioGroupController.prototype = createRadioGroupControllerProto();
 13373  
 13374    return {
 13375      restrict: 'E',
 13376      controller: ['$element', RadioGroupController],
 13377      require: ['mdRadioGroup', '?ngModel'],
 13378      link: { pre: linkRadioGroup }
 13379    };
 13380  
 13381    function linkRadioGroup(scope, element, attr, ctrls) {
 13382      $mdTheming(element);
 13383      var rgCtrl = ctrls[0];
 13384      var ngModelCtrl = ctrls[1] || $mdUtil.fakeNgModel();
 13385  
 13386      rgCtrl.init(ngModelCtrl);
 13387  
 13388      scope.mouseActive = false;
 13389      element.attr({
 13390                'role': 'radiogroup',
 13391                'tabIndex': element.attr('tabindex') || '0'
 13392              })
 13393              .on('keydown', keydownListener)
 13394              .on('mousedown', function(event) {
 13395                scope.mouseActive = true;
 13396                $timeout(function() {
 13397                  scope.mouseActive = false;
 13398                }, 100);
 13399              })
 13400              .on('focus', function() {
 13401                if(scope.mouseActive === false) { rgCtrl.$element.addClass('md-focused'); }
 13402              })
 13403              .on('blur', function() { rgCtrl.$element.removeClass('md-focused'); });
 13404  
 13405      /**
 13406       *
 13407       */
 13408      function setFocus() {
 13409        if (!element.hasClass('md-focused')) { element.addClass('md-focused'); }
 13410      }
 13411  
 13412      /**
 13413       *
 13414       */
 13415      function keydownListener(ev) {
 13416        var keyCode = ev.which || ev.keyCode;
 13417  
 13418        // Only listen to events that we originated ourselves
 13419        // so that we don't trigger on things like arrow keys in
 13420        // inputs.
 13421  
 13422        if (keyCode != $mdConstant.KEY_CODE.ENTER &&
 13423            ev.currentTarget != ev.target) {
 13424          return;
 13425        }
 13426  
 13427        switch (keyCode) {
 13428          case $mdConstant.KEY_CODE.LEFT_ARROW:
 13429          case $mdConstant.KEY_CODE.UP_ARROW:
 13430            ev.preventDefault();
 13431            rgCtrl.selectPrevious();
 13432            setFocus();
 13433            break;
 13434  
 13435          case $mdConstant.KEY_CODE.RIGHT_ARROW:
 13436          case $mdConstant.KEY_CODE.DOWN_ARROW:
 13437            ev.preventDefault();
 13438            rgCtrl.selectNext();
 13439            setFocus();
 13440            break;
 13441  
 13442          case $mdConstant.KEY_CODE.ENTER:
 13443            var form = angular.element($mdUtil.getClosest(element[0], 'form'));
 13444            if (form.length > 0) {
 13445              form.triggerHandler('submit');
 13446            }
 13447            break;
 13448        }
 13449  
 13450      }
 13451    }
 13452  
 13453    function RadioGroupController($element) {
 13454      this._radioButtonRenderFns = [];
 13455      this.$element = $element;
 13456    }
 13457  
 13458    function createRadioGroupControllerProto() {
 13459      return {
 13460        init: function(ngModelCtrl) {
 13461          this._ngModelCtrl = ngModelCtrl;
 13462          this._ngModelCtrl.$render = angular.bind(this, this.render);
 13463        },
 13464        add: function(rbRender) {
 13465          this._radioButtonRenderFns.push(rbRender);
 13466        },
 13467        remove: function(rbRender) {
 13468          var index = this._radioButtonRenderFns.indexOf(rbRender);
 13469          if (index !== -1) {
 13470            this._radioButtonRenderFns.splice(index, 1);
 13471          }
 13472        },
 13473        render: function() {
 13474          this._radioButtonRenderFns.forEach(function(rbRender) {
 13475            rbRender();
 13476          });
 13477        },
 13478        setViewValue: function(value, eventType) {
 13479          this._ngModelCtrl.$setViewValue(value, eventType);
 13480          // update the other radio buttons as well
 13481          this.render();
 13482        },
 13483        getViewValue: function() {
 13484          return this._ngModelCtrl.$viewValue;
 13485        },
 13486        selectNext: function() {
 13487          return changeSelectedButton(this.$element, 1);
 13488        },
 13489        selectPrevious: function() {
 13490          return changeSelectedButton(this.$element, -1);
 13491        },
 13492        setActiveDescendant: function (radioId) {
 13493          this.$element.attr('aria-activedescendant', radioId);
 13494        }
 13495      };
 13496    }
 13497    /**
 13498     * Change the radio group's selected button by a given increment.
 13499     * If no button is selected, select the first button.
 13500     */
 13501    function changeSelectedButton(parent, increment) {
 13502      // Coerce all child radio buttons into an array, then wrap then in an iterator
 13503      var buttons = $mdUtil.iterator(parent[0].querySelectorAll('md-radio-button'), true);
 13504  
 13505      if (buttons.count()) {
 13506        var validate = function (button) {
 13507          // If disabled, then NOT valid
 13508          return !angular.element(button).attr("disabled");
 13509        };
 13510  
 13511        var selected = parent[0].querySelector('md-radio-button.md-checked');
 13512        var target = buttons[increment < 0 ? 'previous' : 'next'](selected, validate) || buttons.first();
 13513  
 13514        // Activate radioButton's click listener (triggerHandler won't create a real click event)
 13515        angular.element(target).triggerHandler('click');
 13516  
 13517  
 13518      }
 13519    }
 13520  
 13521  }
 13522  mdRadioGroupDirective.$inject = ["$mdUtil", "$mdConstant", "$mdTheming", "$timeout"];
 13523  
 13524  /**
 13525   * @ngdoc directive
 13526   * @module material.components.radioButton
 13527   * @name mdRadioButton
 13528   *
 13529   * @restrict E
 13530   *
 13531   * @description
 13532   * The `<md-radio-button>`directive is the child directive required to be used within `<md-radio-group>` elements.
 13533   *
 13534   * While similar to the `<input type="radio" ng-model="" value="">` directive,
 13535   * the `<md-radio-button>` directive provides ink effects, ARIA support, and
 13536   * supports use within named radio groups.
 13537   *
 13538   * @param {string} ngModel Assignable angular expression to data-bind to.
 13539   * @param {string=} ngChange Angular expression to be executed when input changes due to user
 13540   *    interaction with the input element.
 13541   * @param {string} ngValue Angular expression which sets the value to which the expression should
 13542   *    be set when selected.
 13543   * @param {string} value The value to which the expression should be set when selected.
 13544   * @param {string=} name Property name of the form under which the control is published.
 13545   * @param {string=} aria-label Adds label to radio button for accessibility.
 13546   * Defaults to radio button's text. If no text content is available, a warning will be logged.
 13547   *
 13548   * @usage
 13549   * <hljs lang="html">
 13550   *
 13551   * <md-radio-button value="1" aria-label="Label 1">
 13552   *   Label 1
 13553   * </md-radio-button>
 13554   *
 13555   * <md-radio-button ng-model="color" ng-value="specialValue" aria-label="Green">
 13556   *   Green
 13557   * </md-radio-button>
 13558   *
 13559   * </hljs>
 13560   *
 13561   */
 13562  function mdRadioButtonDirective($mdAria, $mdUtil, $mdTheming) {
 13563  
 13564    var CHECKED_CSS = 'md-checked';
 13565  
 13566    return {
 13567      restrict: 'E',
 13568      require: '^mdRadioGroup',
 13569      transclude: true,
 13570      template: '<div class="md-container" md-ink-ripple md-ink-ripple-checkbox>' +
 13571                  '<div class="md-off"></div>' +
 13572                  '<div class="md-on"></div>' +
 13573                '</div>' +
 13574                '<div ng-transclude class="md-label"></div>',
 13575      link: link
 13576    };
 13577  
 13578    function link(scope, element, attr, rgCtrl) {
 13579      var lastChecked;
 13580  
 13581      $mdTheming(element);
 13582      configureAria(element, scope);
 13583  
 13584      initialize();
 13585  
 13586      /**
 13587       *
 13588       */
 13589      function initialize(controller) {
 13590        if ( !rgCtrl ) {
 13591          throw 'RadioGroupController not found.';
 13592        }
 13593  
 13594        rgCtrl.add(render);
 13595        attr.$observe('value', render);
 13596  
 13597        element
 13598          .on('click', listener)
 13599          .on('$destroy', function() {
 13600            rgCtrl.remove(render);
 13601          });
 13602      }
 13603  
 13604      /**
 13605       *
 13606       */
 13607      function listener(ev) {
 13608        if (element[0].hasAttribute('disabled')) return;
 13609  
 13610        scope.$apply(function() {
 13611          rgCtrl.setViewValue(attr.value, ev && ev.type);
 13612        });
 13613      }
 13614  
 13615      /**
 13616       *  Add or remove the `.md-checked` class from the RadioButton (and conditionally its parent).
 13617       *  Update the `aria-activedescendant` attribute.
 13618       */
 13619      function render() {
 13620        var checked = (rgCtrl.getViewValue() == attr.value);
 13621        if (checked === lastChecked) {
 13622          return;
 13623        }
 13624  
 13625        lastChecked = checked;
 13626        element.attr('aria-checked', checked);
 13627  
 13628        if (checked) {
 13629          markParentAsChecked(true);
 13630          element.addClass(CHECKED_CSS);
 13631  
 13632          rgCtrl.setActiveDescendant(element.attr('id'));
 13633  
 13634        } else {
 13635          markParentAsChecked(false);
 13636          element.removeClass(CHECKED_CSS);
 13637        }
 13638  
 13639        /**
 13640         * If the radioButton is inside a div, then add class so highlighting will work...
 13641         */
 13642        function markParentAsChecked(addClass ) {
 13643          if ( element.parent()[0].nodeName != "MD-RADIO-GROUP") {
 13644            element.parent()[ !!addClass ? 'addClass' : 'removeClass'](CHECKED_CSS);
 13645          }
 13646  
 13647        }
 13648      }
 13649  
 13650      /**
 13651       * Inject ARIA-specific attributes appropriate for each radio button
 13652       */
 13653      function configureAria( element, scope ){
 13654        scope.ariaId = buildAriaID();
 13655  
 13656        element.attr({
 13657          'id' :  scope.ariaId,
 13658          'role' : 'radio',
 13659          'aria-checked' : 'false'
 13660        });
 13661  
 13662        $mdAria.expectWithText(element, 'aria-label');
 13663  
 13664        /**
 13665         * Build a unique ID for each radio button that will be used with aria-activedescendant.
 13666         * Preserve existing ID if already specified.
 13667         * @returns {*|string}
 13668         */
 13669        function buildAriaID() {
 13670          return attr.id || ( 'radio' + "_" + $mdUtil.nextUid() );
 13671        }
 13672      }
 13673    }
 13674  }
 13675  mdRadioButtonDirective.$inject = ["$mdAria", "$mdUtil", "$mdTheming"];
 13676  
 13677  })();
 13678  (function(){
 13679  "use strict";
 13680  
 13681  /**
 13682   * @ngdoc module
 13683   * @name material.components.showHide
 13684   */
 13685  
 13686  // Add additional handlers to ng-show and ng-hide that notify directives
 13687  // contained within that they should recompute their size.
 13688  // These run in addition to Angular's built-in ng-hide and ng-show directives.
 13689  angular.module('material.components.showHide', [
 13690    'material.core'
 13691  ])
 13692    .directive('ngShow', createDirective('ngShow', true))
 13693    .directive('ngHide', createDirective('ngHide', false));
 13694  
 13695  
 13696  function createDirective(name, targetValue) {
 13697    return ['$mdUtil', function($mdUtil) {
 13698      return {
 13699        restrict: 'A',
 13700        multiElement: true,
 13701        link: function($scope, $element, $attr) {
 13702          var unregister = $scope.$on('$md-resize-enable', function() {
 13703            unregister();
 13704  
 13705            $scope.$watch($attr[name], function(value) {
 13706              if (!!value === targetValue) {
 13707                $mdUtil.nextTick(function() {
 13708                  $scope.$broadcast('$md-resize');
 13709                });
 13710                $mdUtil.dom.animator.waitTransitionEnd($element).then(function() {
 13711                  $scope.$broadcast('$md-resize');
 13712                });
 13713              }
 13714            });
 13715          });
 13716        }
 13717      };
 13718    }];
 13719  }
 13720  })();
 13721  (function(){
 13722  "use strict";
 13723  
 13724  /**
 13725   * @ngdoc module
 13726   * @name material.components.select
 13727   */
 13728  
 13729  /***************************************************
 13730  
 13731   ### TODO - POST RC1 ###
 13732   - [ ] Abstract placement logic in $mdSelect service to $mdMenu service
 13733  
 13734   ***************************************************/
 13735  
 13736  var SELECT_EDGE_MARGIN = 8;
 13737  var selectNextId = 0;
 13738  
 13739  angular.module('material.components.select', [
 13740      'material.core',
 13741      'material.components.backdrop'
 13742    ])
 13743    .directive('mdSelect', SelectDirective)
 13744    .directive('mdSelectMenu', SelectMenuDirective)
 13745    .directive('mdOption', OptionDirective)
 13746    .directive('mdOptgroup', OptgroupDirective)
 13747    .provider('$mdSelect', SelectProvider);
 13748  
 13749  /**
 13750   * @ngdoc directive
 13751   * @name mdSelect
 13752   * @restrict E
 13753   * @module material.components.select
 13754   *
 13755   * @description Displays a select box, bound to an ng-model.
 13756   *
 13757   * @param {expression} ng-model The model!
 13758   * @param {boolean=} multiple Whether it's multiple.
 13759   * @param {expression=} md-on-close Expression to be evaluated when the select is closed.
 13760   * @param {expression=} md-on-open Expression to be evaluated when opening the select.
 13761   * Will hide the select options and show a spinner until the evaluated promise resolves.
 13762   * @param {string=} placeholder Placeholder hint text.
 13763   * @param {string=} aria-label Optional label for accessibility. Only necessary if no placeholder or
 13764   * explicit label is present.
 13765   * @param {string=} md-container-class Class list to get applied to the `.md-select-menu-container`
 13766   * element (for custom styling).
 13767   *
 13768   * @usage
 13769   * With a placeholder (label and aria-label are added dynamically)
 13770   * <hljs lang="html">
 13771   *   <md-input-container>
 13772   *     <md-select
 13773   *       ng-model="someModel"
 13774   *       placeholder="Select a state">
 13775   *       <md-option ng-value="opt" ng-repeat="opt in neighborhoods2">{{ opt }}</md-option>
 13776   *     </md-select>
 13777   *   </md-input-container>
 13778   * </hljs>
 13779   *
 13780   * With an explicit label
 13781   * <hljs lang="html">
 13782   *   <md-input-container>
 13783   *     <label>State</label>
 13784   *     <md-select
 13785   *       ng-model="someModel">
 13786   *       <md-option ng-value="opt" ng-repeat="opt in neighborhoods2">{{ opt }}</md-option>
 13787   *     </md-select>
 13788   *   </md-input-container>
 13789   * </hljs>
 13790   *
 13791   * ## Selects and object equality
 13792   * When using a `md-select` to pick from a list of objects, it is important to realize how javascript handles
 13793   * equality. Consider the following example:
 13794   * <hljs lang="js">
 13795   * angular.controller('MyCtrl', function($scope) {
 13796   *   $scope.users = [
 13797   *     { id: 1, name: 'Bob' },
 13798   *     { id: 2, name: 'Alice' },
 13799   *     { id: 3, name: 'Steve' }
 13800   *   ];
 13801   *   $scope.selectedUser = { id: 1, name: 'Bob' };
 13802   * });
 13803   * </hljs>
 13804   * <hljs lang="html">
 13805   * <div ng-controller="MyCtrl">
 13806   *   <md-select ng-model="selectedUser">
 13807   *     <md-option ng-value="user" ng-repeat="user in users">{{ user.name }}</md-option>
 13808   *   </md-select>
 13809   * </div>
 13810   * </hljs>
 13811   *
 13812   * At first one might expect that the select should be populated with "Bob" as the selected user. However,
 13813   * this is not true. To determine whether something is selected,
 13814   * `ngModelController` is looking at whether `$scope.selectedUser == (any user in $scope.users);`;
 13815   *
 13816   * Javascript's `==` operator does not check for deep equality (ie. that all properties
 13817   * on the object are the same), but instead whether the objects are *the same object in memory*.
 13818   * In this case, we have two instances of identical objects, but they exist in memory as unique
 13819   * entities. Because of this, the select will have no value populated for a selected user.
 13820   *
 13821   * To get around this, `ngModelController` provides a `track by` option that allows us to specify a different
 13822   * expression which will be used for the equality operator. As such, we can update our `html` to
 13823   * make use of this by specifying the `ng-model-options="{trackBy: '$value.id'}"` on the `md-select`
 13824   * element. This converts our equality expression to be
 13825   * `$scope.selectedUser.id == (any id in $scope.users.map(function(u) { return u.id; }));`
 13826   * which results in Bob being selected as desired.
 13827   *
 13828   * Working HTML:
 13829   * <hljs lang="html">
 13830   * <div ng-controller="MyCtrl">
 13831   *   <md-select ng-model="selectedUser" ng-model-options="{trackBy: '$value.id'}">
 13832   *     <md-option ng-value="user" ng-repeat="user in users">{{ user.name }}</md-option>
 13833   *   </md-select>
 13834   * </div>
 13835   * </hljs>
 13836   */
 13837  function SelectDirective($mdSelect, $mdUtil, $mdTheming, $mdAria, $compile, $parse) {
 13838    return {
 13839      restrict: 'E',
 13840      require: ['^?mdInputContainer', 'mdSelect', 'ngModel', '?^form'],
 13841      compile: compile,
 13842      controller: function() {
 13843      } // empty placeholder controller to be initialized in link
 13844    };
 13845  
 13846    function compile(element, attr) {
 13847      // add the select value that will hold our placeholder or selected option value
 13848      var valueEl = angular.element('<md-select-value><span></span></md-select-value>');
 13849      valueEl.append('<span class="md-select-icon" aria-hidden="true"></span>');
 13850      valueEl.addClass('md-select-value');
 13851      if (!valueEl[0].hasAttribute('id')) {
 13852        valueEl.attr('id', 'select_value_label_' + $mdUtil.nextUid());
 13853      }
 13854  
 13855      // There's got to be an md-content inside. If there's not one, let's add it.
 13856      if (!element.find('md-content').length) {
 13857        element.append(angular.element('<md-content>').append(element.contents()));
 13858      }
 13859  
 13860  
 13861      // Add progress spinner for md-options-loading
 13862      if (attr.mdOnOpen) {
 13863  
 13864        // Show progress indicator while loading async
 13865        // Use ng-hide for `display:none` so the indicator does not interfere with the options list
 13866        element
 13867          .find('md-content')
 13868          .prepend(angular.element(
 13869            '<div>' +
 13870            ' <md-progress-circular md-mode="{{progressMode}}" ng-hide="$$loadingAsyncDone" md-diameter="25px"></md-progress-circular>' +
 13871            '</div>'
 13872          ));
 13873  
 13874        // Hide list [of item options] while loading async
 13875        element
 13876          .find('md-option')
 13877          .attr('ng-show', '$$loadingAsyncDone');
 13878      }
 13879  
 13880      if (attr.name) {
 13881        var autofillClone = angular.element('<select class="md-visually-hidden">');
 13882        autofillClone.attr({
 13883          'name': '.' + attr.name,
 13884          'ng-model': attr.ngModel,
 13885          'aria-hidden': 'true',
 13886          'tabindex': '-1'
 13887        });
 13888        var opts = element.find('md-option');
 13889        angular.forEach(opts, function(el) {
 13890          var newEl = angular.element('<option>' + el.innerHTML + '</option>');
 13891          if (el.hasAttribute('ng-value')) newEl.attr('ng-value', el.getAttribute('ng-value'));
 13892          else if (el.hasAttribute('value')) newEl.attr('value', el.getAttribute('value'));
 13893          autofillClone.append(newEl);
 13894        });
 13895  
 13896        element.parent().append(autofillClone);
 13897      }
 13898  
 13899      // Use everything that's left inside element.contents() as the contents of the menu
 13900      var multiple = angular.isDefined(attr.multiple) ? 'multiple' : '';
 13901      var selectTemplate = '' +
 13902        '<div class="md-select-menu-container" aria-hidden="true">' +
 13903        '<md-select-menu {0}>{1}</md-select-menu>' +
 13904        '</div>';
 13905  
 13906      selectTemplate = $mdUtil.supplant(selectTemplate, [multiple, element.html()]);
 13907      element.empty().append(valueEl);
 13908      element.append(selectTemplate);
 13909  
 13910      if(!attr.tabindex){
 13911        attr.$set('tabindex', 0);
 13912      }
 13913  
 13914      return function postLink(scope, element, attr, ctrls) {
 13915        var untouched = true;
 13916        var isDisabled, ariaLabelBase;
 13917  
 13918        var containerCtrl = ctrls[0];
 13919        var mdSelectCtrl = ctrls[1];
 13920        var ngModelCtrl = ctrls[2];
 13921        var formCtrl = ctrls[3];
 13922        // grab a reference to the select menu value label
 13923        var valueEl = element.find('md-select-value');
 13924        var isReadonly = angular.isDefined(attr.readonly);
 13925  
 13926        if (containerCtrl) {
 13927          var isErrorGetter = containerCtrl.isErrorGetter || function() {
 13928              return ngModelCtrl.$invalid && ngModelCtrl.$touched;
 13929            };
 13930  
 13931          if (containerCtrl.input) {
 13932            throw new Error("<md-input-container> can only have *one* child <input>, <textarea> or <select> element!");
 13933          }
 13934  
 13935          containerCtrl.input = element;
 13936          if (!containerCtrl.label) {
 13937            $mdAria.expect(element, 'aria-label', element.attr('placeholder'));
 13938          }
 13939  
 13940          scope.$watch(isErrorGetter, containerCtrl.setInvalid);
 13941        }
 13942  
 13943        var selectContainer, selectScope, selectMenuCtrl;
 13944  
 13945        findSelectContainer();
 13946        $mdTheming(element);
 13947  
 13948        if (attr.name && formCtrl) {
 13949          var selectEl = element.parent()[0].querySelector('select[name=".' + attr.name + '"]');
 13950          $mdUtil.nextTick(function() {
 13951            var controller = angular.element(selectEl).controller('ngModel');
 13952            if (controller) {
 13953              formCtrl.$removeControl(controller);
 13954            }
 13955          });
 13956        }
 13957  
 13958        if (formCtrl && angular.isDefined(attr.multiple)) {
 13959          $mdUtil.nextTick(function() {
 13960            var hasModelValue = ngModelCtrl.$modelValue || ngModelCtrl.$viewValue;
 13961            if (hasModelValue) {
 13962              formCtrl.$setPristine();
 13963            }
 13964          });
 13965        }
 13966  
 13967        var originalRender = ngModelCtrl.$render;
 13968        ngModelCtrl.$render = function() {
 13969          originalRender();
 13970          syncLabelText();
 13971          syncAriaLabel();
 13972          inputCheckValue();
 13973        };
 13974  
 13975  
 13976        attr.$observe('placeholder', ngModelCtrl.$render);
 13977  
 13978  
 13979        mdSelectCtrl.setLabelText = function(text) {
 13980          mdSelectCtrl.setIsPlaceholder(!text);
 13981          // Use placeholder attribute, otherwise fallback to the md-input-container label
 13982          var tmpPlaceholder = attr.placeholder || (containerCtrl && containerCtrl.label ? containerCtrl.label.text() : '');
 13983          text = text || tmpPlaceholder || '';
 13984          var target = valueEl.children().eq(0);
 13985          target.html(text);
 13986        };
 13987  
 13988        mdSelectCtrl.setIsPlaceholder = function(isPlaceholder) {
 13989          if (isPlaceholder) {
 13990            valueEl.addClass('md-select-placeholder');
 13991            if (containerCtrl && containerCtrl.label) {
 13992              containerCtrl.label.addClass('md-placeholder');
 13993            }
 13994          } else {
 13995            valueEl.removeClass('md-select-placeholder');
 13996            if (containerCtrl && containerCtrl.label) {
 13997              containerCtrl.label.removeClass('md-placeholder');
 13998            }
 13999          }
 14000        };
 14001  
 14002        if (!isReadonly) {
 14003          element
 14004            .on('focus', function(ev) {
 14005              // only set focus on if we don't currently have a selected value. This avoids the "bounce"
 14006              // on the label transition because the focus will immediately switch to the open menu.
 14007              if (containerCtrl && containerCtrl.element.hasClass('md-input-has-value')) {
 14008                containerCtrl.setFocused(true);
 14009              }
 14010            });
 14011  
 14012          // Attach before ngModel's blur listener to stop propagation of blur event
 14013          // to prevent from setting $touched.
 14014          element.on('blur', function(event) {
 14015            if (untouched) {
 14016              untouched = false;
 14017              if (selectScope.isOpen) {
 14018                event.stopImmediatePropagation();
 14019              }
 14020            }
 14021  
 14022            if (selectScope.isOpen) return;
 14023            containerCtrl && containerCtrl.setFocused(false);
 14024            inputCheckValue();
 14025          });
 14026        }
 14027  
 14028        mdSelectCtrl.triggerClose = function() {
 14029          $parse(attr.mdOnClose)(scope);
 14030        };
 14031  
 14032        scope.$$postDigest(function() {
 14033          initAriaLabel();
 14034          syncLabelText();
 14035          syncAriaLabel();
 14036        });
 14037  
 14038        function initAriaLabel() {
 14039          var labelText = element.attr('aria-label') || element.attr('placeholder');
 14040          if (!labelText && containerCtrl && containerCtrl.label) {
 14041            labelText = containerCtrl.label.text();
 14042          }
 14043          ariaLabelBase = labelText;
 14044          $mdAria.expect(element, 'aria-label', labelText);
 14045        }
 14046  
 14047        scope.$watch(selectMenuCtrl.selectedLabels, syncLabelText);
 14048  
 14049        function syncLabelText() {
 14050          if (selectContainer) {
 14051            selectMenuCtrl = selectMenuCtrl || selectContainer.find('md-select-menu').controller('mdSelectMenu');
 14052            mdSelectCtrl.setLabelText(selectMenuCtrl.selectedLabels());
 14053          }
 14054        }
 14055  
 14056        function syncAriaLabel() {
 14057          if (!ariaLabelBase) return;
 14058          var ariaLabels = selectMenuCtrl.selectedLabels({mode: 'aria'});
 14059          element.attr('aria-label', ariaLabels.length ? ariaLabelBase + ': ' + ariaLabels : ariaLabelBase);
 14060        }
 14061  
 14062        var deregisterWatcher;
 14063        attr.$observe('ngMultiple', function(val) {
 14064          if (deregisterWatcher) deregisterWatcher();
 14065          var parser = $parse(val);
 14066          deregisterWatcher = scope.$watch(function() {
 14067            return parser(scope);
 14068          }, function(multiple, prevVal) {
 14069            if (multiple === undefined && prevVal === undefined) return; // assume compiler did a good job
 14070            if (multiple) {
 14071              element.attr('multiple', 'multiple');
 14072            } else {
 14073              element.removeAttr('multiple');
 14074            }
 14075            element.attr('aria-multiselectable', multiple ? 'true' : 'false');
 14076            if (selectContainer) {
 14077              selectMenuCtrl.setMultiple(multiple);
 14078              originalRender = ngModelCtrl.$render;
 14079              ngModelCtrl.$render = function() {
 14080                originalRender();
 14081                syncLabelText();
 14082                syncAriaLabel();
 14083                inputCheckValue();
 14084              };
 14085              ngModelCtrl.$render();
 14086            }
 14087          });
 14088        });
 14089  
 14090        attr.$observe('disabled', function(disabled) {
 14091          if (angular.isString(disabled)) {
 14092            disabled = true;
 14093          }
 14094          // Prevent click event being registered twice
 14095          if (isDisabled !== undefined && isDisabled === disabled) {
 14096            return;
 14097          }
 14098          isDisabled = disabled;
 14099          if (disabled) {
 14100            element
 14101              .attr({'aria-disabled': 'true'})
 14102              .removeAttr('tabindex')
 14103              .off('click', openSelect)
 14104              .off('keydown', handleKeypress);
 14105          } else {
 14106            element
 14107              .attr({'tabindex': attr.tabindex, 'aria-disabled': 'false'})
 14108              .on('click', openSelect)
 14109              .on('keydown', handleKeypress);
 14110          }
 14111        });
 14112  
 14113        if (!attr.hasOwnProperty('disabled') && !attr.hasOwnProperty('ngDisabled')) {
 14114          element.attr({'aria-disabled': 'false'});
 14115          element.on('click', openSelect);
 14116          element.on('keydown', handleKeypress);
 14117        }
 14118  
 14119        var ariaAttrs = {
 14120          role: 'listbox',
 14121          'aria-expanded': 'false',
 14122          'aria-multiselectable': attr.multiple !== undefined && !attr.ngMultiple ? 'true' : 'false'
 14123        };
 14124  
 14125        if (!element[0].hasAttribute('id')) {
 14126          ariaAttrs.id = 'select_' + $mdUtil.nextUid();
 14127        }
 14128  
 14129        var containerId = 'select_container_' + $mdUtil.nextUid();
 14130        selectContainer.attr('id', containerId);
 14131        ariaAttrs['aria-owns'] = containerId;
 14132        element.attr(ariaAttrs);
 14133  
 14134        scope.$on('$destroy', function() {
 14135          $mdSelect
 14136            .destroy()
 14137            .finally(function() {
 14138              if (containerCtrl) {
 14139                containerCtrl.setFocused(false);
 14140                containerCtrl.setHasValue(false);
 14141                containerCtrl.input = null;
 14142              }
 14143              ngModelCtrl.$setTouched();
 14144            });
 14145        });
 14146  
 14147  
 14148  
 14149        function inputCheckValue() {
 14150          // The select counts as having a value if one or more options are selected,
 14151          // or if the input's validity state says it has bad input (eg string in a number input)
 14152          containerCtrl && containerCtrl.setHasValue(selectMenuCtrl.selectedLabels().length > 0 || (element[0].validity || {}).badInput);
 14153        }
 14154  
 14155        function findSelectContainer() {
 14156          selectContainer = angular.element(
 14157            element[0].querySelector('.md-select-menu-container')
 14158          );
 14159          selectScope = scope;
 14160          if (attr.mdContainerClass) {
 14161            var value = selectContainer[0].getAttribute('class') + ' ' + attr.mdContainerClass;
 14162            selectContainer[0].setAttribute('class', value);
 14163          }
 14164          selectMenuCtrl = selectContainer.find('md-select-menu').controller('mdSelectMenu');
 14165          selectMenuCtrl.init(ngModelCtrl, attr.ngModel);
 14166          element.on('$destroy', function() {
 14167            selectContainer.remove();
 14168          });
 14169        }
 14170  
 14171        function handleKeypress(e) {
 14172          var allowedCodes = [32, 13, 38, 40];
 14173          if (allowedCodes.indexOf(e.keyCode) != -1) {
 14174            // prevent page scrolling on interaction
 14175            e.preventDefault();
 14176            openSelect(e);
 14177          } else {
 14178            if (e.keyCode <= 90 && e.keyCode >= 31) {
 14179              e.preventDefault();
 14180              var node = selectMenuCtrl.optNodeForKeyboardSearch(e);
 14181              if (!node) return;
 14182              var optionCtrl = angular.element(node).controller('mdOption');
 14183              if (!selectMenuCtrl.isMultiple) {
 14184                selectMenuCtrl.deselect(Object.keys(selectMenuCtrl.selected)[0]);
 14185              }
 14186              selectMenuCtrl.select(optionCtrl.hashKey, optionCtrl.value);
 14187              selectMenuCtrl.refreshViewValue();
 14188            }
 14189          }
 14190        }
 14191  
 14192        function openSelect() {
 14193          selectScope.isOpen = true;
 14194          element.attr('aria-expanded', 'true');
 14195  
 14196          $mdSelect.show({
 14197            scope: selectScope,
 14198            preserveScope: true,
 14199            skipCompile: true,
 14200            element: selectContainer,
 14201            target: element[0],
 14202            selectCtrl: mdSelectCtrl,
 14203            preserveElement: true,
 14204            hasBackdrop: true,
 14205            loadingAsync: attr.mdOnOpen ? scope.$eval(attr.mdOnOpen) || true : false
 14206          }).finally(function() {
 14207            selectScope.isOpen = false;
 14208            element.focus();
 14209            element.attr('aria-expanded', 'false');
 14210            ngModelCtrl.$setTouched();
 14211          });
 14212        }
 14213      };
 14214    }
 14215  }
 14216  SelectDirective.$inject = ["$mdSelect", "$mdUtil", "$mdTheming", "$mdAria", "$compile", "$parse"];
 14217  
 14218  function SelectMenuDirective($parse, $mdUtil, $mdTheming) {
 14219  
 14220    SelectMenuController.$inject = ["$scope", "$attrs", "$element"];
 14221    return {
 14222      restrict: 'E',
 14223      require: ['mdSelectMenu'],
 14224      scope: true,
 14225      controller: SelectMenuController,
 14226      link: {pre: preLink}
 14227    };
 14228  
 14229    // We use preLink instead of postLink to ensure that the select is initialized before
 14230    // its child options run postLink.
 14231    function preLink(scope, element, attr, ctrls) {
 14232      var selectCtrl = ctrls[0];
 14233  
 14234      $mdTheming(element);
 14235      element.on('click', clickListener);
 14236      element.on('keypress', keyListener);
 14237  
 14238      function keyListener(e) {
 14239        if (e.keyCode == 13 || e.keyCode == 32) {
 14240          clickListener(e);
 14241        }
 14242      }
 14243  
 14244      function clickListener(ev) {
 14245        var option = $mdUtil.getClosest(ev.target, 'md-option');
 14246        var optionCtrl = option && angular.element(option).data('$mdOptionController');
 14247        if (!option || !optionCtrl) return;
 14248        if (option.hasAttribute('disabled')) {
 14249          ev.stopImmediatePropagation();
 14250          return false;
 14251        }
 14252  
 14253        var optionHashKey = selectCtrl.hashGetter(optionCtrl.value);
 14254        var isSelected = angular.isDefined(selectCtrl.selected[optionHashKey]);
 14255  
 14256        scope.$apply(function() {
 14257          if (selectCtrl.isMultiple) {
 14258            if (isSelected) {
 14259              selectCtrl.deselect(optionHashKey);
 14260            } else {
 14261              selectCtrl.select(optionHashKey, optionCtrl.value);
 14262            }
 14263          } else {
 14264            if (!isSelected) {
 14265              selectCtrl.deselect(Object.keys(selectCtrl.selected)[0]);
 14266              selectCtrl.select(optionHashKey, optionCtrl.value);
 14267            }
 14268          }
 14269          selectCtrl.refreshViewValue();
 14270        });
 14271      }
 14272    }
 14273  
 14274    function SelectMenuController($scope, $attrs, $element) {
 14275      var self = this;
 14276      self.isMultiple = angular.isDefined($attrs.multiple);
 14277      // selected is an object with keys matching all of the selected options' hashed values
 14278      self.selected = {};
 14279      // options is an object with keys matching every option's hash value,
 14280      // and values matching every option's controller.
 14281      self.options = {};
 14282  
 14283      $scope.$watchCollection(function() {
 14284        return self.options;
 14285      }, function() {
 14286        self.ngModel.$render();
 14287      });
 14288  
 14289      var deregisterCollectionWatch;
 14290      var defaultIsEmpty;
 14291      self.setMultiple = function(isMultiple) {
 14292        var ngModel = self.ngModel;
 14293        defaultIsEmpty = defaultIsEmpty || ngModel.$isEmpty;
 14294  
 14295        self.isMultiple = isMultiple;
 14296        if (deregisterCollectionWatch) deregisterCollectionWatch();
 14297  
 14298        if (self.isMultiple) {
 14299          ngModel.$validators['md-multiple'] = validateArray;
 14300          ngModel.$render = renderMultiple;
 14301  
 14302          // watchCollection on the model because by default ngModel only watches the model's
 14303          // reference. This allowed the developer to also push and pop from their array.
 14304          $scope.$watchCollection(self.modelBinding, function(value) {
 14305            if (validateArray(value)) renderMultiple(value);
 14306            self.ngModel.$setPristine();
 14307          });
 14308  
 14309          ngModel.$isEmpty = function(value) {
 14310            return !value || value.length === 0;
 14311          };
 14312        } else {
 14313          delete ngModel.$validators['md-multiple'];
 14314          ngModel.$render = renderSingular;
 14315        }
 14316  
 14317        function validateArray(modelValue, viewValue) {
 14318          // If a value is truthy but not an array, reject it.
 14319          // If value is undefined/falsy, accept that it's an empty array.
 14320          return angular.isArray(modelValue || viewValue || []);
 14321        }
 14322      };
 14323  
 14324      var searchStr = '';
 14325      var clearSearchTimeout, optNodes, optText;
 14326      var CLEAR_SEARCH_AFTER = 300;
 14327      self.optNodeForKeyboardSearch = function(e) {
 14328        clearSearchTimeout && clearTimeout(clearSearchTimeout);
 14329        clearSearchTimeout = setTimeout(function() {
 14330          clearSearchTimeout = undefined;
 14331          searchStr = '';
 14332          optText = undefined;
 14333          optNodes = undefined;
 14334        }, CLEAR_SEARCH_AFTER);
 14335        searchStr += String.fromCharCode(e.keyCode);
 14336        var search = new RegExp('^' + searchStr, 'i');
 14337        if (!optNodes) {
 14338          optNodes = $element.find('md-option');
 14339          optText = new Array(optNodes.length);
 14340          angular.forEach(optNodes, function(el, i) {
 14341            optText[i] = el.textContent.trim();
 14342          });
 14343        }
 14344        for (var i = 0; i < optText.length; ++i) {
 14345          if (search.test(optText[i])) {
 14346            return optNodes[i];
 14347          }
 14348        }
 14349      };
 14350  
 14351      self.init = function(ngModel, binding) {
 14352        self.ngModel = ngModel;
 14353        self.modelBinding = binding;
 14354  
 14355        // Allow users to provide `ng-model="foo" ng-model-options="{trackBy: 'foo.id'}"` so
 14356        // that we can properly compare objects set on the model to the available options
 14357        if (ngModel.$options && ngModel.$options.trackBy) {
 14358          var trackByLocals = {};
 14359          var trackByParsed = $parse(ngModel.$options.trackBy);
 14360          self.hashGetter = function(value, valueScope) {
 14361            trackByLocals.$value = value;
 14362            return trackByParsed(valueScope || $scope, trackByLocals);
 14363          };
 14364          // If the user doesn't provide a trackBy, we automatically generate an id for every
 14365          // value passed in
 14366        } else {
 14367          self.hashGetter = function getHashValue(value) {
 14368            if (angular.isObject(value)) {
 14369              return 'object_' + (value.$$mdSelectId || (value.$$mdSelectId = ++selectNextId));
 14370            }
 14371            return value;
 14372          };
 14373        }
 14374        self.setMultiple(self.isMultiple);
 14375      };
 14376  
 14377      self.selectedLabels = function(opts) {
 14378        opts = opts || {};
 14379        var mode = opts.mode || 'html';
 14380        var selectedOptionEls = $mdUtil.nodesToArray($element[0].querySelectorAll('md-option[selected]'));
 14381        if (selectedOptionEls.length) {
 14382          var mapFn;
 14383  
 14384          if (mode == 'html') {
 14385            // Map the given element to its innerHTML string. If the element has a child ripple
 14386            // container remove it from the HTML string, before returning the string.
 14387            mapFn = function(el) {
 14388              var html = el.innerHTML;
 14389              // Remove the ripple container from the selected option, copying it would cause a CSP violation.
 14390              var rippleContainer = el.querySelector('.md-ripple-container');
 14391              return rippleContainer ? html.replace(rippleContainer.outerHTML, '') : html;
 14392            };
 14393          } else if (mode == 'aria') {
 14394            mapFn = function(el) { return el.hasAttribute('aria-label') ? el.getAttribute('aria-label') : el.textContent; };
 14395          }
 14396          return selectedOptionEls.map(mapFn).join(', ');
 14397        } else {
 14398          return '';
 14399        }
 14400      };
 14401  
 14402      self.select = function(hashKey, hashedValue) {
 14403        var option = self.options[hashKey];
 14404        option && option.setSelected(true);
 14405        self.selected[hashKey] = hashedValue;
 14406      };
 14407      self.deselect = function(hashKey) {
 14408        var option = self.options[hashKey];
 14409        option && option.setSelected(false);
 14410        delete self.selected[hashKey];
 14411      };
 14412  
 14413      self.addOption = function(hashKey, optionCtrl) {
 14414        if (angular.isDefined(self.options[hashKey])) {
 14415          throw new Error('Duplicate md-option values are not allowed in a select. ' +
 14416            'Duplicate value "' + optionCtrl.value + '" found.');
 14417        }
 14418        self.options[hashKey] = optionCtrl;
 14419  
 14420        // If this option's value was already in our ngModel, go ahead and select it.
 14421        if (angular.isDefined(self.selected[hashKey])) {
 14422          self.select(hashKey, optionCtrl.value);
 14423          self.refreshViewValue();
 14424        }
 14425      };
 14426      self.removeOption = function(hashKey) {
 14427        delete self.options[hashKey];
 14428        // Don't deselect an option when it's removed - the user's ngModel should be allowed
 14429        // to have values that do not match a currently available option.
 14430      };
 14431  
 14432      self.refreshViewValue = function() {
 14433        var values = [];
 14434        var option;
 14435        for (var hashKey in self.selected) {
 14436          // If this hashKey has an associated option, push that option's value to the model.
 14437          if ((option = self.options[hashKey])) {
 14438            values.push(option.value);
 14439          } else {
 14440            // Otherwise, the given hashKey has no associated option, and we got it
 14441            // from an ngModel value at an earlier time. Push the unhashed value of
 14442            // this hashKey to the model.
 14443            // This allows the developer to put a value in the model that doesn't yet have
 14444            // an associated option.
 14445            values.push(self.selected[hashKey]);
 14446          }
 14447        }
 14448        var usingTrackBy = self.ngModel.$options && self.ngModel.$options.trackBy;
 14449  
 14450        var newVal = self.isMultiple ? values : values[0];
 14451        var prevVal = self.ngModel.$modelValue;
 14452  
 14453        if (usingTrackBy ? !angular.equals(prevVal, newVal) : prevVal != newVal) {
 14454          self.ngModel.$setViewValue(newVal);
 14455          self.ngModel.$render();
 14456        }
 14457      };
 14458  
 14459      function renderMultiple() {
 14460        var newSelectedValues = self.ngModel.$modelValue || self.ngModel.$viewValue || [];
 14461        if (!angular.isArray(newSelectedValues)) return;
 14462  
 14463        var oldSelected = Object.keys(self.selected);
 14464  
 14465        var newSelectedHashes = newSelectedValues.map(self.hashGetter);
 14466        var deselected = oldSelected.filter(function(hash) {
 14467          return newSelectedHashes.indexOf(hash) === -1;
 14468        });
 14469  
 14470        deselected.forEach(self.deselect);
 14471        newSelectedHashes.forEach(function(hashKey, i) {
 14472          self.select(hashKey, newSelectedValues[i]);
 14473        });
 14474      }
 14475  
 14476      function renderSingular() {
 14477        var value = self.ngModel.$viewValue || self.ngModel.$modelValue;
 14478        Object.keys(self.selected).forEach(self.deselect);
 14479        self.select(self.hashGetter(value), value);
 14480      }
 14481    }
 14482  
 14483  }
 14484  SelectMenuDirective.$inject = ["$parse", "$mdUtil", "$mdTheming"];
 14485  
 14486  function OptionDirective($mdButtonInkRipple, $mdUtil) {
 14487  
 14488    OptionController.$inject = ["$element"];
 14489    return {
 14490      restrict: 'E',
 14491      require: ['mdOption', '^^mdSelectMenu'],
 14492      controller: OptionController,
 14493      compile: compile
 14494    };
 14495  
 14496    function compile(element, attr) {
 14497      // Manual transclusion to avoid the extra inner <span> that ng-transclude generates
 14498      element.append(angular.element('<div class="md-text">').append(element.contents()));
 14499  
 14500      element.attr('tabindex', attr.tabindex || '0');
 14501      return postLink;
 14502    }
 14503  
 14504    function postLink(scope, element, attr, ctrls) {
 14505      var optionCtrl = ctrls[0];
 14506      var selectCtrl = ctrls[1];
 14507  
 14508      if (angular.isDefined(attr.ngValue)) {
 14509        scope.$watch(attr.ngValue, setOptionValue);
 14510      } else if (angular.isDefined(attr.value)) {
 14511        setOptionValue(attr.value);
 14512      } else {
 14513        scope.$watch(function() {
 14514          return element.text().trim();
 14515        }, setOptionValue);
 14516      }
 14517  
 14518      attr.$observe('disabled', function(disabled) {
 14519        if (disabled) {
 14520          element.attr('tabindex', '-1');
 14521        } else {
 14522          element.attr('tabindex', '0');
 14523        }
 14524      });
 14525  
 14526      scope.$$postDigest(function() {
 14527        attr.$observe('selected', function(selected) {
 14528          if (!angular.isDefined(selected)) return;
 14529          if (typeof selected == 'string') selected = true;
 14530          if (selected) {
 14531            if (!selectCtrl.isMultiple) {
 14532              selectCtrl.deselect(Object.keys(selectCtrl.selected)[0]);
 14533            }
 14534            selectCtrl.select(optionCtrl.hashKey, optionCtrl.value);
 14535          } else {
 14536            selectCtrl.deselect(optionCtrl.hashKey);
 14537          }
 14538          selectCtrl.refreshViewValue();
 14539        });
 14540      });
 14541  
 14542      $mdButtonInkRipple.attach(scope, element);
 14543      configureAria();
 14544  
 14545      function setOptionValue(newValue, oldValue, prevAttempt) {
 14546        if (!selectCtrl.hashGetter) {
 14547          if (!prevAttempt) {
 14548            scope.$$postDigest(function() {
 14549              setOptionValue(newValue, oldValue, true);
 14550            });
 14551          }
 14552          return;
 14553        }
 14554        var oldHashKey = selectCtrl.hashGetter(oldValue, scope);
 14555        var newHashKey = selectCtrl.hashGetter(newValue, scope);
 14556  
 14557        optionCtrl.hashKey = newHashKey;
 14558        optionCtrl.value = newValue;
 14559  
 14560        selectCtrl.removeOption(oldHashKey, optionCtrl);
 14561        selectCtrl.addOption(newHashKey, optionCtrl);
 14562      }
 14563  
 14564      scope.$on('$destroy', function() {
 14565        selectCtrl.removeOption(optionCtrl.hashKey, optionCtrl);
 14566      });
 14567  
 14568      function configureAria() {
 14569        var ariaAttrs = {
 14570          'role': 'option',
 14571          'aria-selected': 'false'
 14572        };
 14573  
 14574        if (!element[0].hasAttribute('id')) {
 14575          ariaAttrs.id = 'select_option_' + $mdUtil.nextUid();
 14576        }
 14577        element.attr(ariaAttrs);
 14578      }
 14579    }
 14580  
 14581    function OptionController($element) {
 14582      this.selected = false;
 14583      this.setSelected = function(isSelected) {
 14584        if (isSelected && !this.selected) {
 14585          $element.attr({
 14586            'selected': 'selected',
 14587            'aria-selected': 'true'
 14588          });
 14589        } else if (!isSelected && this.selected) {
 14590          $element.removeAttr('selected');
 14591          $element.attr('aria-selected', 'false');
 14592        }
 14593        this.selected = isSelected;
 14594      };
 14595    }
 14596  
 14597  }
 14598  OptionDirective.$inject = ["$mdButtonInkRipple", "$mdUtil"];
 14599  
 14600  function OptgroupDirective() {
 14601    return {
 14602      restrict: 'E',
 14603      compile: compile
 14604    };
 14605    function compile(el, attrs) {
 14606      var labelElement = el.find('label');
 14607      if (!labelElement.length) {
 14608        labelElement = angular.element('<label>');
 14609        el.prepend(labelElement);
 14610      }
 14611      labelElement.addClass('md-container-ignore');
 14612      if (attrs.label) labelElement.text(attrs.label);
 14613    }
 14614  }
 14615  
 14616  function SelectProvider($$interimElementProvider) {
 14617    selectDefaultOptions.$inject = ["$mdSelect", "$mdConstant", "$mdUtil", "$window", "$q", "$$rAF", "$animateCss", "$animate", "$document"];
 14618    return $$interimElementProvider('$mdSelect')
 14619      .setDefaults({
 14620        methods: ['target'],
 14621        options: selectDefaultOptions
 14622      });
 14623  
 14624    /* @ngInject */
 14625    function selectDefaultOptions($mdSelect, $mdConstant, $mdUtil, $window, $q, $$rAF, $animateCss, $animate, $document) {
 14626      var ERRROR_TARGET_EXPECTED = "$mdSelect.show() expected a target element in options.target but got '{0}'!";
 14627      var animator = $mdUtil.dom.animator;
 14628  
 14629      return {
 14630        parent: 'body',
 14631        themable: true,
 14632        onShow: onShow,
 14633        onRemove: onRemove,
 14634        hasBackdrop: true,
 14635        disableParentScroll: true
 14636      };
 14637  
 14638      /**
 14639       * Interim-element onRemove logic....
 14640       */
 14641      function onRemove(scope, element, opts) {
 14642        opts = opts || { };
 14643        opts.cleanupInteraction();
 14644        opts.cleanupResizing();
 14645        opts.hideBackdrop();
 14646  
 14647        // For navigation $destroy events, do a quick, non-animated removal,
 14648        // but for normal closes (from clicks, etc) animate the removal
 14649  
 14650        return  (opts.$destroy === true) ? cleanElement() : animateRemoval().then( cleanElement );
 14651  
 14652        /**
 14653         * For normal closes (eg clicks), animate the removal.
 14654         * For forced closes (like $destroy events from navigation),
 14655         * skip the animations
 14656         */
 14657        function animateRemoval() {
 14658          return $animateCss(element, {addClass: 'md-leave'}).start();
 14659        }
 14660  
 14661        /**
 14662         * Restore the element to a closed state
 14663         */
 14664        function cleanElement() {
 14665  
 14666          element.removeClass('md-active');
 14667          element.attr('aria-hidden', 'true');
 14668          element[0].style.display = 'none';
 14669  
 14670          announceClosed(opts);
 14671  
 14672          if (!opts.$destroy && opts.restoreFocus) {
 14673            opts.target.focus();
 14674          }
 14675        }
 14676  
 14677      }
 14678  
 14679      /**
 14680       * Interim-element onShow logic....
 14681       */
 14682      function onShow(scope, element, opts) {
 14683  
 14684        watchAsyncLoad();
 14685        sanitizeAndConfigure(scope, opts);
 14686  
 14687        opts.hideBackdrop = showBackdrop(scope, element, opts);
 14688  
 14689        return showDropDown(scope, element, opts)
 14690          .then(function(response) {
 14691            element.attr('aria-hidden', 'false');
 14692            opts.alreadyOpen = true;
 14693            opts.cleanupInteraction = activateInteraction();
 14694            opts.cleanupResizing = activateResizing();
 14695  
 14696            return response;
 14697          }, opts.hideBackdrop);
 14698  
 14699        // ************************************
 14700        // Closure Functions
 14701        // ************************************
 14702  
 14703        /**
 14704         *  Attach the select DOM element(s) and animate to the correct positions
 14705         *  and scalings...
 14706         */
 14707        function showDropDown(scope, element, opts) {
 14708          opts.parent.append(element);
 14709  
 14710          return $q(function(resolve, reject) {
 14711  
 14712            try {
 14713  
 14714              $animateCss(element, {removeClass: 'md-leave', duration: 0})
 14715                .start()
 14716                .then(positionAndFocusMenu)
 14717                .then(resolve);
 14718  
 14719            } catch (e) {
 14720              reject(e);
 14721            }
 14722  
 14723          });
 14724        }
 14725  
 14726        /**
 14727         * Initialize container and dropDown menu positions/scale, then animate
 14728         * to show... and autoFocus.
 14729         */
 14730        function positionAndFocusMenu() {
 14731          return $q(function(resolve) {
 14732            if (opts.isRemoved) return $q.reject(false);
 14733  
 14734            var info = calculateMenuPositions(scope, element, opts);
 14735  
 14736            info.container.element.css(animator.toCss(info.container.styles));
 14737            info.dropDown.element.css(animator.toCss(info.dropDown.styles));
 14738  
 14739            $$rAF(function() {
 14740              element.addClass('md-active');
 14741              info.dropDown.element.css(animator.toCss({transform: ''}));
 14742  
 14743              autoFocus(opts.focusedNode);
 14744              resolve();
 14745            });
 14746  
 14747          });
 14748        }
 14749  
 14750        /**
 14751         * Show modal backdrop element...
 14752         */
 14753        function showBackdrop(scope, element, options) {
 14754  
 14755          // If we are not within a dialog...
 14756          if (options.disableParentScroll && !$mdUtil.getClosest(options.target, 'MD-DIALOG')) {
 14757            // !! DO this before creating the backdrop; since disableScrollAround()
 14758            //    configures the scroll offset; which is used by mdBackDrop postLink()
 14759            options.restoreScroll = $mdUtil.disableScrollAround(options.element, options.parent);
 14760          } else {
 14761            options.disableParentScroll = false;
 14762          }
 14763  
 14764          if (options.hasBackdrop) {
 14765            // Override duration to immediately show invisible backdrop
 14766            options.backdrop = $mdUtil.createBackdrop(scope, "md-select-backdrop md-click-catcher");
 14767            $animate.enter(options.backdrop, $document[0].body, null, {duration: 0});
 14768          }
 14769  
 14770          /**
 14771           * Hide modal backdrop element...
 14772           */
 14773          return function hideBackdrop() {
 14774            if (options.backdrop) options.backdrop.remove();
 14775            if (options.disableParentScroll) options.restoreScroll();
 14776  
 14777            delete options.restoreScroll;
 14778          };
 14779        }
 14780  
 14781        /**
 14782         *
 14783         */
 14784        function autoFocus(focusedNode) {
 14785          if (focusedNode && !focusedNode.hasAttribute('disabled')) {
 14786            focusedNode.focus();
 14787          }
 14788        }
 14789  
 14790        /**
 14791         * Check for valid opts and set some sane defaults
 14792         */
 14793        function sanitizeAndConfigure(scope, options) {
 14794          var selectEl = element.find('md-select-menu');
 14795  
 14796          if (!options.target) {
 14797            throw new Error($mdUtil.supplant(ERRROR_TARGET_EXPECTED, [options.target]));
 14798          }
 14799  
 14800          angular.extend(options, {
 14801            isRemoved: false,
 14802            target: angular.element(options.target), //make sure it's not a naked dom node
 14803            parent: angular.element(options.parent),
 14804            selectEl: selectEl,
 14805            contentEl: element.find('md-content'),
 14806            optionNodes: selectEl[0].getElementsByTagName('md-option')
 14807          });
 14808        }
 14809  
 14810        /**
 14811         * Configure various resize listeners for screen changes
 14812         */
 14813        function activateResizing() {
 14814          var debouncedOnResize = (function(scope, target, options) {
 14815  
 14816            return function() {
 14817              if (options.isRemoved) return;
 14818  
 14819              var updates = calculateMenuPositions(scope, target, options);
 14820              var container = updates.container;
 14821              var dropDown = updates.dropDown;
 14822  
 14823              container.element.css(animator.toCss(container.styles));
 14824              dropDown.element.css(animator.toCss(dropDown.styles));
 14825            };
 14826  
 14827          })(scope, element, opts);
 14828  
 14829          var window = angular.element($window);
 14830          window.on('resize', debouncedOnResize);
 14831          window.on('orientationchange', debouncedOnResize);
 14832  
 14833          // Publish deactivation closure...
 14834          return function deactivateResizing() {
 14835  
 14836            // Disable resizing handlers
 14837            window.off('resize', debouncedOnResize);
 14838            window.off('orientationchange', debouncedOnResize);
 14839          };
 14840        }
 14841  
 14842        /**
 14843         *  If asynchronously loading, watch and update internal
 14844         *  '$$loadingAsyncDone' flag
 14845         */
 14846        function watchAsyncLoad() {
 14847          if (opts.loadingAsync && !opts.isRemoved) {
 14848            scope.$$loadingAsyncDone = false;
 14849            scope.progressMode = 'indeterminate';
 14850  
 14851            $q.when(opts.loadingAsync)
 14852              .then(function() {
 14853                scope.$$loadingAsyncDone = true;
 14854                scope.progressMode = '';
 14855                delete opts.loadingAsync;
 14856              }).then(function() {
 14857                $$rAF(positionAndFocusMenu);
 14858              });
 14859          }
 14860        }
 14861  
 14862        /**
 14863         *
 14864         */
 14865        function activateInteraction() {
 14866          if (opts.isRemoved) return;
 14867  
 14868          var dropDown = opts.selectEl;
 14869          var selectCtrl = dropDown.controller('mdSelectMenu') || {};
 14870  
 14871          element.addClass('md-clickable');
 14872  
 14873          // Close on backdrop click
 14874          opts.backdrop && opts.backdrop.on('click', onBackdropClick);
 14875  
 14876          // Escape to close
 14877          // Cycling of options, and closing on enter
 14878          dropDown.on('keydown', onMenuKeyDown);
 14879          dropDown.on('click', checkCloseMenu);
 14880  
 14881          return function cleanupInteraction() {
 14882            opts.backdrop && opts.backdrop.off('click', onBackdropClick);
 14883            dropDown.off('keydown', onMenuKeyDown);
 14884            dropDown.off('click', checkCloseMenu);
 14885  
 14886            element.removeClass('md-clickable');
 14887            opts.isRemoved = true;
 14888          };
 14889  
 14890          // ************************************
 14891          // Closure Functions
 14892          // ************************************
 14893  
 14894          function onBackdropClick(e) {
 14895            e.preventDefault();
 14896            e.stopPropagation();
 14897            opts.restoreFocus = false;
 14898            $mdUtil.nextTick($mdSelect.hide, true);
 14899          }
 14900  
 14901          function onMenuKeyDown(ev) {
 14902            var keyCodes = $mdConstant.KEY_CODE;
 14903            ev.preventDefault();
 14904            ev.stopPropagation();
 14905  
 14906            switch (ev.keyCode) {
 14907              case keyCodes.UP_ARROW:
 14908                return focusPrevOption();
 14909              case keyCodes.DOWN_ARROW:
 14910                return focusNextOption();
 14911              case keyCodes.SPACE:
 14912              case keyCodes.ENTER:
 14913                var option = $mdUtil.getClosest(ev.target, 'md-option');
 14914                if (option) {
 14915                  dropDown.triggerHandler({
 14916                    type: 'click',
 14917                    target: option
 14918                  });
 14919                  ev.preventDefault();
 14920                }
 14921                checkCloseMenu(ev);
 14922                break;
 14923              case keyCodes.TAB:
 14924              case keyCodes.ESCAPE:
 14925                ev.stopPropagation();
 14926                ev.preventDefault();
 14927                opts.restoreFocus = true;
 14928                $mdUtil.nextTick($mdSelect.hide, true);
 14929                break;
 14930              default:
 14931                if (ev.keyCode >= 31 && ev.keyCode <= 90) {
 14932                  var optNode = dropDown.controller('mdSelectMenu').optNodeForKeyboardSearch(ev);
 14933                  opts.focusedNode = optNode || opts.focusedNode;
 14934                  optNode && optNode.focus();
 14935                }
 14936            }
 14937          }
 14938  
 14939          function focusOption(direction) {
 14940            var optionsArray = $mdUtil.nodesToArray(opts.optionNodes);
 14941            var index = optionsArray.indexOf(opts.focusedNode);
 14942  
 14943            var newOption;
 14944  
 14945            do {
 14946              if (index === -1) {
 14947                // We lost the previously focused element, reset to first option
 14948                index = 0;
 14949              } else if (direction === 'next' && index < optionsArray.length - 1) {
 14950                index++;
 14951              } else if (direction === 'prev' && index > 0) {
 14952                index--;
 14953              }
 14954              newOption = optionsArray[index];
 14955              if (newOption.hasAttribute('disabled')) newOption = undefined;
 14956            } while (!newOption && index < optionsArray.length - 1 && index > 0);
 14957            newOption && newOption.focus();
 14958            opts.focusedNode = newOption;
 14959          }
 14960  
 14961          function focusNextOption() {
 14962            focusOption('next');
 14963          }
 14964  
 14965          function focusPrevOption() {
 14966            focusOption('prev');
 14967          }
 14968  
 14969          function checkCloseMenu(ev) {
 14970            if (ev && ( ev.type == 'click') && (ev.currentTarget != dropDown[0])) return;
 14971            if ( mouseOnScrollbar() ) return;
 14972  
 14973            var option = $mdUtil.getClosest(ev.target, 'md-option');
 14974            if (option && option.hasAttribute && !option.hasAttribute('disabled')) {
 14975              ev.preventDefault();
 14976              ev.stopPropagation();
 14977              if (!selectCtrl.isMultiple) {
 14978                opts.restoreFocus = true;
 14979  
 14980                $mdUtil.nextTick(function () {
 14981                  $mdSelect.hide(selectCtrl.ngModel.$viewValue);
 14982                }, true);
 14983              }
 14984            }
 14985            /**
 14986             * check if the mouseup event was on a scrollbar
 14987             */
 14988            function mouseOnScrollbar() {
 14989              var clickOnScrollbar = false;
 14990              if (ev && (ev.currentTarget.children.length > 0)) {
 14991                var child = ev.currentTarget.children[0];
 14992                var hasScrollbar = child.scrollHeight > child.clientHeight;
 14993                if (hasScrollbar && child.children.length > 0) {
 14994                  var relPosX = ev.pageX - ev.currentTarget.getBoundingClientRect().left;
 14995                  if (relPosX > child.querySelector('md-option').offsetWidth)
 14996                    clickOnScrollbar = true;
 14997                }
 14998              }
 14999              return clickOnScrollbar;
 15000            }
 15001          }
 15002        }
 15003  
 15004      }
 15005  
 15006      /**
 15007       * To notify listeners that the Select menu has closed,
 15008       * trigger the [optional] user-defined expression
 15009       */
 15010      function announceClosed(opts) {
 15011        var mdSelect = opts.selectCtrl;
 15012        if (mdSelect) {
 15013          var menuController = opts.selectEl.controller('mdSelectMenu');
 15014          mdSelect.setLabelText(menuController.selectedLabels());
 15015          mdSelect.triggerClose();
 15016        }
 15017      }
 15018  
 15019  
 15020      /**
 15021       * Calculate the
 15022       */
 15023      function calculateMenuPositions(scope, element, opts) {
 15024        var
 15025          containerNode = element[0],
 15026          targetNode = opts.target[0].children[0], // target the label
 15027          parentNode = $document[0].body,
 15028          selectNode = opts.selectEl[0],
 15029          contentNode = opts.contentEl[0],
 15030          parentRect = parentNode.getBoundingClientRect(),
 15031          targetRect = targetNode.getBoundingClientRect(),
 15032          shouldOpenAroundTarget = false,
 15033          bounds = {
 15034            left: parentRect.left + SELECT_EDGE_MARGIN,
 15035            top: SELECT_EDGE_MARGIN,
 15036            bottom: parentRect.height - SELECT_EDGE_MARGIN,
 15037            right: parentRect.width - SELECT_EDGE_MARGIN - ($mdUtil.floatingScrollbars() ? 16 : 0)
 15038          },
 15039          spaceAvailable = {
 15040            top: targetRect.top - bounds.top,
 15041            left: targetRect.left - bounds.left,
 15042            right: bounds.right - (targetRect.left + targetRect.width),
 15043            bottom: bounds.bottom - (targetRect.top + targetRect.height)
 15044          },
 15045          maxWidth = parentRect.width - SELECT_EDGE_MARGIN * 2,
 15046          selectedNode = selectNode.querySelector('md-option[selected]'),
 15047          optionNodes = selectNode.getElementsByTagName('md-option'),
 15048          optgroupNodes = selectNode.getElementsByTagName('md-optgroup'),
 15049          isScrollable = calculateScrollable(element, contentNode),
 15050          centeredNode;
 15051  
 15052        var loading = isPromiseLike(opts.loadingAsync);
 15053        if (!loading) {
 15054          // If a selected node, center around that
 15055          if (selectedNode) {
 15056            centeredNode = selectedNode;
 15057            // If there are option groups, center around the first option group
 15058          } else if (optgroupNodes.length) {
 15059            centeredNode = optgroupNodes[0];
 15060            // Otherwise - if we are not loading async - center around the first optionNode
 15061          } else if (optionNodes.length) {
 15062            centeredNode = optionNodes[0];
 15063            // In case there are no options, center on whatever's in there... (eg progress indicator)
 15064          } else {
 15065            centeredNode = contentNode.firstElementChild || contentNode;
 15066          }
 15067        } else {
 15068          // If loading, center on progress indicator
 15069          centeredNode = contentNode.firstElementChild || contentNode;
 15070        }
 15071  
 15072        if (contentNode.offsetWidth > maxWidth) {
 15073          contentNode.style['max-width'] = maxWidth + 'px';
 15074        } else {
 15075          contentNode.style.maxWidth = null;
 15076        }
 15077        if (shouldOpenAroundTarget) {
 15078          contentNode.style['min-width'] = targetRect.width + 'px';
 15079        }
 15080  
 15081        // Remove padding before we compute the position of the menu
 15082        if (isScrollable) {
 15083          selectNode.classList.add('md-overflow');
 15084        }
 15085  
 15086        var focusedNode = centeredNode;
 15087        if ((focusedNode.tagName || '').toUpperCase() === 'MD-OPTGROUP') {
 15088          focusedNode = optionNodes[0] || contentNode.firstElementChild || contentNode;
 15089          centeredNode = focusedNode;
 15090        }
 15091        // Cache for autoFocus()
 15092        opts.focusedNode = focusedNode;
 15093  
 15094        // Get the selectMenuRect *after* max-width is possibly set above
 15095        containerNode.style.display = 'block';
 15096        var selectMenuRect = selectNode.getBoundingClientRect();
 15097        var centeredRect = getOffsetRect(centeredNode);
 15098  
 15099        if (centeredNode) {
 15100          var centeredStyle = $window.getComputedStyle(centeredNode);
 15101          centeredRect.paddingLeft = parseInt(centeredStyle.paddingLeft, 10) || 0;
 15102          centeredRect.paddingRight = parseInt(centeredStyle.paddingRight, 10) || 0;
 15103        }
 15104  
 15105        if (isScrollable) {
 15106          var scrollBuffer = contentNode.offsetHeight / 2;
 15107          contentNode.scrollTop = centeredRect.top + centeredRect.height / 2 - scrollBuffer;
 15108  
 15109          if (spaceAvailable.top < scrollBuffer) {
 15110            contentNode.scrollTop = Math.min(
 15111              centeredRect.top,
 15112              contentNode.scrollTop + scrollBuffer - spaceAvailable.top
 15113            );
 15114          } else if (spaceAvailable.bottom < scrollBuffer) {
 15115            contentNode.scrollTop = Math.max(
 15116              centeredRect.top + centeredRect.height - selectMenuRect.height,
 15117              contentNode.scrollTop - scrollBuffer + spaceAvailable.bottom
 15118            );
 15119          }
 15120        }
 15121  
 15122        var left, top, transformOrigin, minWidth;
 15123        if (shouldOpenAroundTarget) {
 15124          left = targetRect.left;
 15125          top = targetRect.top + targetRect.height;
 15126          transformOrigin = '50% 0';
 15127          if (top + selectMenuRect.height > bounds.bottom) {
 15128            top = targetRect.top - selectMenuRect.height;
 15129            transformOrigin = '50% 100%';
 15130          }
 15131        } else {
 15132          left = (targetRect.left + centeredRect.left - centeredRect.paddingLeft) + 2;
 15133          top = Math.floor(targetRect.top + targetRect.height / 2 - centeredRect.height / 2 -
 15134              centeredRect.top + contentNode.scrollTop) + 2;
 15135  
 15136          transformOrigin = (centeredRect.left + targetRect.width / 2) + 'px ' +
 15137            (centeredRect.top + centeredRect.height / 2 - contentNode.scrollTop) + 'px 0px';
 15138  
 15139          minWidth = Math.min(targetRect.width + centeredRect.paddingLeft + centeredRect.paddingRight, maxWidth);
 15140        }
 15141  
 15142        // Keep left and top within the window
 15143        var containerRect = containerNode.getBoundingClientRect();
 15144        var scaleX = Math.round(100 * Math.min(targetRect.width / selectMenuRect.width, 1.0)) / 100;
 15145        var scaleY = Math.round(100 * Math.min(targetRect.height / selectMenuRect.height, 1.0)) / 100;
 15146  
 15147        return {
 15148          container: {
 15149            element: angular.element(containerNode),
 15150            styles: {
 15151              left: Math.floor(clamp(bounds.left, left, bounds.right - containerRect.width)),
 15152              top: Math.floor(clamp(bounds.top, top, bounds.bottom - containerRect.height)),
 15153              'min-width': minWidth
 15154            }
 15155          },
 15156          dropDown: {
 15157            element: angular.element(selectNode),
 15158            styles: {
 15159              transformOrigin: transformOrigin,
 15160              transform: !opts.alreadyOpen ? $mdUtil.supplant('scale({0},{1})', [scaleX, scaleY]) : ""
 15161            }
 15162          }
 15163        };
 15164  
 15165      }
 15166  
 15167    }
 15168  
 15169    function isPromiseLike(obj) {
 15170      return obj && angular.isFunction(obj.then);
 15171    }
 15172  
 15173    function clamp(min, n, max) {
 15174      return Math.max(min, Math.min(n, max));
 15175    }
 15176  
 15177    function getOffsetRect(node) {
 15178      return node ? {
 15179        left: node.offsetLeft,
 15180        top: node.offsetTop,
 15181        width: node.offsetWidth,
 15182        height: node.offsetHeight
 15183      } : {left: 0, top: 0, width: 0, height: 0};
 15184    }
 15185  
 15186    function calculateScrollable(element, contentNode) {
 15187      var isScrollable = false;
 15188  
 15189      try {
 15190        var oldDisplay = element[0].style.display;
 15191  
 15192        // Set the element's display to block so that this calculation is correct
 15193        element[0].style.display = 'block';
 15194  
 15195        isScrollable = contentNode.scrollHeight > contentNode.offsetHeight;
 15196  
 15197        // Reset it back afterwards
 15198        element[0].style.display = oldDisplay;
 15199      } finally {
 15200        // Nothing to do
 15201      }
 15202      return isScrollable;
 15203    }
 15204  }
 15205  SelectProvider.$inject = ["$$interimElementProvider"];
 15206  
 15207  
 15208  })();
 15209  (function(){
 15210  "use strict";
 15211  
 15212  /**
 15213   * @ngdoc module
 15214   * @name material.components.sidenav
 15215   *
 15216   * @description
 15217   * A Sidenav QP component.
 15218   */
 15219  angular
 15220    .module('material.components.sidenav', [
 15221      'material.core',
 15222      'material.components.backdrop'
 15223    ])
 15224    .factory('$mdSidenav', SidenavService )
 15225    .directive('mdSidenav', SidenavDirective)
 15226    .directive('mdSidenavFocus', SidenavFocusDirective)
 15227    .controller('$mdSidenavController', SidenavController);
 15228  
 15229  
 15230  /**
 15231   * @ngdoc service
 15232   * @name $mdSidenav
 15233   * @module material.components.sidenav
 15234   *
 15235   * @description
 15236   * `$mdSidenav` makes it easy to interact with multiple sidenavs
 15237   * in an app.
 15238   *
 15239   * @usage
 15240   * <hljs lang="js">
 15241   * // Async lookup for sidenav instance; will resolve when the instance is available
 15242   * $mdSidenav(componentId).then(function(instance) {
 15243   *   $log.debug( componentId + "is now ready" );
 15244   * });
 15245   * // Async toggle the given sidenav;
 15246   * // when instance is known ready and lazy lookup is not needed.
 15247   * $mdSidenav(componentId)
 15248   *    .toggle()
 15249   *    .then(function(){
 15250   *      $log.debug('toggled');
 15251   *    });
 15252   * // Async open the given sidenav
 15253   * $mdSidenav(componentId)
 15254   *    .open()
 15255   *    .then(function(){
 15256   *      $log.debug('opened');
 15257   *    });
 15258   * // Async close the given sidenav
 15259   * $mdSidenav(componentId)
 15260   *    .close()
 15261   *    .then(function(){
 15262   *      $log.debug('closed');
 15263   *    });
 15264   * // Sync check to see if the specified sidenav is set to be open
 15265   * $mdSidenav(componentId).isOpen();
 15266   * // Sync check to whether given sidenav is locked open
 15267   * // If this is true, the sidenav will be open regardless of close()
 15268   * $mdSidenav(componentId).isLockedOpen();
 15269   * </hljs>
 15270   */
 15271  function SidenavService($mdComponentRegistry, $q) {
 15272    return function(handle) {
 15273  
 15274      // Lookup the controller instance for the specified sidNav instance
 15275      var self;
 15276      var errorMsg = "SideNav '" + handle + "' is not available!";
 15277      var instance = $mdComponentRegistry.get(handle);
 15278  
 15279      if(!instance) {
 15280        $mdComponentRegistry.notFoundError(handle);
 15281      }
 15282  
 15283      return self = {
 15284        // -----------------
 15285        // Sync methods
 15286        // -----------------
 15287        isOpen: function() {
 15288          return instance && instance.isOpen();
 15289        },
 15290        isLockedOpen: function() {
 15291          return instance && instance.isLockedOpen();
 15292        },
 15293        // -----------------
 15294        // Async methods
 15295        // -----------------
 15296        toggle: function() {
 15297          return instance ? instance.toggle() : $q.reject(errorMsg);
 15298        },
 15299        open: function() {
 15300          return instance ? instance.open() : $q.reject(errorMsg);
 15301        },
 15302        close: function() {
 15303          return instance ? instance.close() : $q.reject(errorMsg);
 15304        },
 15305        then : function( callbackFn ) {
 15306          var promise = instance ? $q.when(instance) : waitForInstance();
 15307          return promise.then( callbackFn || angular.noop );
 15308        }
 15309      };
 15310  
 15311      /**
 15312       * Deferred lookup of component instance using $component registry
 15313       */
 15314      function waitForInstance() {
 15315        return $mdComponentRegistry
 15316                  .when(handle)
 15317                  .then(function( it ){
 15318                    instance = it;
 15319                    return it;
 15320                  });
 15321      }
 15322    };
 15323  }
 15324  SidenavService.$inject = ["$mdComponentRegistry", "$q"];
 15325  /**
 15326   * @ngdoc directive
 15327   * @name mdSidenavFocus
 15328   * @module material.components.sidenav
 15329   *
 15330   * @restrict A
 15331   *
 15332   * @description
 15333   * `mdSidenavFocus` provides a way to specify the focused element when a sidenav opens.
 15334   * This is completely optional, as the sidenav itself is focused by default.
 15335   *
 15336   * @usage
 15337   * <hljs lang="html">
 15338   * <md-sidenav>
 15339   *   <form>
 15340   *     <md-input-container>
 15341   *       <label for="testInput">Label</label>
 15342   *       <input id="testInput" type="text" md-sidenav-focus>
 15343   *     </md-input-container>
 15344   *   </form>
 15345   * </md-sidenav>
 15346   * </hljs>
 15347   **/
 15348  function SidenavFocusDirective() {
 15349    return {
 15350      restrict: 'A',
 15351      require: '^mdSidenav',
 15352      link: function(scope, element, attr, sidenavCtrl) {
 15353        // @see $mdUtil.findFocusTarget(...)
 15354      }
 15355    };
 15356  }
 15357  /**
 15358   * @ngdoc directive
 15359   * @name mdSidenav
 15360   * @module material.components.sidenav
 15361   * @restrict E
 15362   *
 15363   * @description
 15364   *
 15365   * A Sidenav component that can be opened and closed programatically.
 15366   *
 15367   * By default, upon opening it will slide out on top of the main content area.
 15368   *
 15369   * For keyboard and screen reader accessibility, focus is sent to the sidenav wrapper by default.
 15370   * It can be overridden with the `md-autofocus` directive on the child element you want focused.
 15371   *
 15372   * @usage
 15373   * <hljs lang="html">
 15374   * <div layout="row" ng-controller="MyController">
 15375   *   <md-sidenav md-component-id="left" class="md-sidenav-left">
 15376   *     Left Nav!
 15377   *   </md-sidenav>
 15378   *
 15379   *   <md-content>
 15380   *     Center Content
 15381   *     <md-button ng-click="openLeftMenu()">
 15382   *       Open Left Menu
 15383   *     </md-button>
 15384   *   </md-content>
 15385   *
 15386   *   <md-sidenav md-component-id="right"
 15387   *     md-is-locked-open="$mdMedia('min-width: 333px')"
 15388   *     class="md-sidenav-right">
 15389   *     <form>
 15390   *       <md-input-container>
 15391   *         <label for="testInput">Test input</label>
 15392   *         <input id="testInput" type="text"
 15393   *                ng-model="data" md-autofocus>
 15394   *       </md-input-container>
 15395   *     </form>
 15396   *   </md-sidenav>
 15397   * </div>
 15398   * </hljs>
 15399   *
 15400   * <hljs lang="js">
 15401   * var app = angular.module('myApp', ['ngMaterial']);
 15402   * app.controller('MyController', function($scope, $mdSidenav) {
 15403   *   $scope.openLeftMenu = function() {
 15404   *     $mdSidenav('left').toggle();
 15405   *   };
 15406   * });
 15407   * </hljs>
 15408   *
 15409   * @param {expression=} md-is-open A model bound to whether the sidenav is opened.
 15410   * @param {string=} md-component-id componentId to use with $mdSidenav service.
 15411   * @param {expression=} md-is-locked-open When this expression evaluates to true,
 15412   * the sidenav 'locks open': it falls into the content's flow instead
 15413   * of appearing over it. This overrides the `md-is-open` attribute.
 15414   *
 15415   * The $mdMedia() service is exposed to the is-locked-open attribute, which
 15416   * can be given a media query or one of the `sm`, `gt-sm`, `md`, `gt-md`, `lg` or `gt-lg` presets.
 15417   * Examples:
 15418   *
 15419   *   - `<md-sidenav md-is-locked-open="shouldLockOpen"></md-sidenav>`
 15420   *   - `<md-sidenav md-is-locked-open="$mdMedia('min-width: 1000px')"></md-sidenav>`
 15421   *   - `<md-sidenav md-is-locked-open="$mdMedia('sm')"></md-sidenav>` (locks open on small screens)
 15422   */
 15423  function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $animate, $compile, $parse, $log, $q, $document) {
 15424    return {
 15425      restrict: 'E',
 15426      scope: {
 15427        isOpen: '=?mdIsOpen'
 15428      },
 15429      controller: '$mdSidenavController',
 15430      compile: function(element) {
 15431        element.addClass('md-closed');
 15432        element.attr('tabIndex', '-1');
 15433        return postLink;
 15434      }
 15435    };
 15436  
 15437    /**
 15438     * Directive Post Link function...
 15439     */
 15440    function postLink(scope, element, attr, sidenavCtrl) {
 15441      var lastParentOverFlow;
 15442      var triggeringElement = null;
 15443      var promise = $q.when(true);
 15444  
 15445      var isLockedOpenParsed = $parse(attr.mdIsLockedOpen);
 15446      var isLocked = function() {
 15447        return isLockedOpenParsed(scope.$parent, {
 15448          $media: function(arg) {
 15449            $log.warn("$media is deprecated for is-locked-open. Use $mdMedia instead.");
 15450            return $mdMedia(arg);
 15451          },
 15452          $mdMedia: $mdMedia
 15453        });
 15454      };
 15455      var backdrop = $mdUtil.createBackdrop(scope, "md-sidenav-backdrop md-opaque ng-enter");
 15456  
 15457      $mdTheming(element);
 15458  
 15459      // The backdrop should inherit the sidenavs theme,
 15460      // because the backdrop will take its parent theme by default.
 15461      $mdTheming.inherit(backdrop, element);
 15462  
 15463      element.on('$destroy', function() {
 15464        backdrop.remove();
 15465        sidenavCtrl.destroy();
 15466      });
 15467  
 15468      scope.$on('$destroy', function(){
 15469        backdrop.remove()
 15470      });
 15471  
 15472      scope.$watch(isLocked, updateIsLocked);
 15473      scope.$watch('isOpen', updateIsOpen);
 15474  
 15475  
 15476      // Publish special accessor for the Controller instance
 15477      sidenavCtrl.$toggleOpen = toggleOpen;
 15478  
 15479      /**
 15480       * Toggle the DOM classes to indicate `locked`
 15481       * @param isLocked
 15482       */
 15483      function updateIsLocked(isLocked, oldValue) {
 15484        scope.isLockedOpen = isLocked;
 15485        if (isLocked === oldValue) {
 15486          element.toggleClass('md-locked-open', !!isLocked);
 15487        } else {
 15488          $animate[isLocked ? 'addClass' : 'removeClass'](element, 'md-locked-open');
 15489        }
 15490        backdrop.toggleClass('md-locked-open', !!isLocked);
 15491      }
 15492  
 15493      /**
 15494       * Toggle the SideNav view and attach/detach listeners
 15495       * @param isOpen
 15496       */
 15497      function updateIsOpen(isOpen) {
 15498        // Support deprecated md-sidenav-focus attribute as fallback
 15499        var focusEl = $mdUtil.findFocusTarget(element) || $mdUtil.findFocusTarget(element,'[md-sidenav-focus]') || element;
 15500        var parent = element.parent();
 15501  
 15502        parent[isOpen ? 'on' : 'off']('keydown', onKeyDown);
 15503        backdrop[isOpen ? 'on' : 'off']('click', close);
 15504  
 15505        if ( isOpen ) {
 15506          // Capture upon opening..
 15507          triggeringElement = $document[0].activeElement;
 15508        }
 15509  
 15510        disableParentScroll(isOpen);
 15511  
 15512        return promise = $q.all([
 15513                  isOpen ? $animate.enter(backdrop, parent) : $animate.leave(backdrop),
 15514                  $animate[isOpen ? 'removeClass' : 'addClass'](element, 'md-closed')
 15515                ])
 15516                .then(function() {
 15517                  // Perform focus when animations are ALL done...
 15518                  if (scope.isOpen) {
 15519                    focusEl && focusEl.focus();
 15520                  }
 15521                });
 15522      }
 15523  
 15524      /**
 15525       * Prevent parent scrolling (when the SideNav is open)
 15526       */
 15527      function disableParentScroll(disabled) {
 15528        var parent = element.parent();
 15529        if ( disabled && !lastParentOverFlow ) {
 15530  
 15531          lastParentOverFlow = parent.css('overflow');
 15532          parent.css('overflow', 'hidden');
 15533  
 15534        } else if (angular.isDefined(lastParentOverFlow)) {
 15535  
 15536          parent.css('overflow', lastParentOverFlow);
 15537          lastParentOverFlow = undefined;
 15538  
 15539        }
 15540      }
 15541  
 15542      /**
 15543       * Toggle the sideNav view and publish a promise to be resolved when
 15544       * the view animation finishes.
 15545       *
 15546       * @param isOpen
 15547       * @returns {*}
 15548       */
 15549      function toggleOpen( isOpen ) {
 15550        if (scope.isOpen == isOpen ) {
 15551  
 15552          return $q.when(true);
 15553  
 15554        } else {
 15555          return $q(function(resolve){
 15556            // Toggle value to force an async `updateIsOpen()` to run
 15557            scope.isOpen = isOpen;
 15558  
 15559            $mdUtil.nextTick(function() {
 15560              // When the current `updateIsOpen()` animation finishes
 15561              promise.then(function(result) {
 15562  
 15563                if ( !scope.isOpen ) {
 15564                  // reset focus to originating element (if available) upon close
 15565                  triggeringElement && triggeringElement.focus();
 15566                  triggeringElement = null;
 15567                }
 15568  
 15569                resolve(result);
 15570              });
 15571            });
 15572  
 15573          });
 15574  
 15575        }
 15576      }
 15577  
 15578      /**
 15579       * Auto-close sideNav when the `escape` key is pressed.
 15580       * @param evt
 15581       */
 15582      function onKeyDown(ev) {
 15583        var isEscape = (ev.keyCode === $mdConstant.KEY_CODE.ESCAPE);
 15584        return isEscape ? close(ev) : $q.when(true);
 15585      }
 15586  
 15587      /**
 15588       * With backdrop `clicks` or `escape` key-press, immediately
 15589       * apply the CSS close transition... Then notify the controller
 15590       * to close() and perform its own actions.
 15591       */
 15592      function close(ev) {
 15593        ev.preventDefault();
 15594  
 15595        return sidenavCtrl.close();
 15596      }
 15597  
 15598    }
 15599  }
 15600  SidenavDirective.$inject = ["$mdMedia", "$mdUtil", "$mdConstant", "$mdTheming", "$animate", "$compile", "$parse", "$log", "$q", "$document"];
 15601  
 15602  /*
 15603   * @private
 15604   * @ngdoc controller
 15605   * @name SidenavController
 15606   * @module material.components.sidenav
 15607   *
 15608   */
 15609  function SidenavController($scope, $element, $attrs, $mdComponentRegistry, $q) {
 15610  
 15611    var self = this;
 15612  
 15613    // Use Default internal method until overridden by directive postLink
 15614  
 15615    // Synchronous getters
 15616    self.isOpen = function() { return !!$scope.isOpen; };
 15617    self.isLockedOpen = function() { return !!$scope.isLockedOpen; };
 15618  
 15619    // Async actions
 15620    self.open   = function() { return self.$toggleOpen( true );  };
 15621    self.close  = function() { return self.$toggleOpen( false ); };
 15622    self.toggle = function() { return self.$toggleOpen( !$scope.isOpen );  };
 15623    self.$toggleOpen = function(value) { return $q.when($scope.isOpen = value); };
 15624  
 15625    self.destroy = $mdComponentRegistry.register(self, $attrs.mdComponentId);
 15626  }
 15627  SidenavController.$inject = ["$scope", "$element", "$attrs", "$mdComponentRegistry", "$q"];
 15628  
 15629  })();
 15630  (function(){
 15631  "use strict";
 15632  
 15633    /**
 15634     * @ngdoc module
 15635     * @name material.components.slider
 15636     */
 15637    angular.module('material.components.slider', [
 15638      'material.core'
 15639    ])
 15640    .directive('mdSlider', SliderDirective);
 15641  
 15642  /**
 15643   * @ngdoc directive
 15644   * @name mdSlider
 15645   * @module material.components.slider
 15646   * @restrict E
 15647   * @description
 15648   * The `<md-slider>` component allows the user to choose from a range of
 15649   * values.
 15650   *
 15651   * As per the [material design spec](http://www.google.com/design/spec/style/color.html#color-ui-color-application)
 15652   * the slider is in the accent color by default. The primary color palette may be used with
 15653   * the `md-primary` class.
 15654   *
 15655   * It has two modes: 'normal' mode, where the user slides between a wide range
 15656   * of values, and 'discrete' mode, where the user slides between only a few
 15657   * select values.
 15658   *
 15659   * To enable discrete mode, add the `md-discrete` attribute to a slider,
 15660   * and use the `step` attribute to change the distance between
 15661   * values the user is allowed to pick.
 15662   *
 15663   * @usage
 15664   * <h4>Normal Mode</h4>
 15665   * <hljs lang="html">
 15666   * <md-slider ng-model="myValue" min="5" max="500">
 15667   * </md-slider>
 15668   * </hljs>
 15669   * <h4>Discrete Mode</h4>
 15670   * <hljs lang="html">
 15671   * <md-slider md-discrete ng-model="myDiscreteValue" step="10" min="10" max="130">
 15672   * </md-slider>
 15673   * </hljs>
 15674   *
 15675   * @param {boolean=} md-discrete Whether to enable discrete mode.
 15676   * @param {number=} step The distance between values the user is allowed to pick. Default 1.
 15677   * @param {number=} min The minimum value the user is allowed to pick. Default 0.
 15678   * @param {number=} max The maximum value the user is allowed to pick. Default 100.
 15679   */
 15680  function SliderDirective($$rAF, $window, $mdAria, $mdUtil, $mdConstant, $mdTheming, $mdGesture, $parse, $log) {
 15681    return {
 15682      scope: {},
 15683      require: '?ngModel',
 15684      template:
 15685        '<div class="md-slider-wrapper">' +
 15686          '<div class="md-track-container">' +
 15687            '<div class="md-track"></div>' +
 15688            '<div class="md-track md-track-fill"></div>' +
 15689            '<div class="md-track-ticks"></div>' +
 15690          '</div>' +
 15691          '<div class="md-thumb-container">' +
 15692            '<div class="md-thumb"></div>' +
 15693            '<div class="md-focus-thumb"></div>' +
 15694            '<div class="md-focus-ring"></div>' +
 15695            '<div class="md-sign">' +
 15696              '<span class="md-thumb-text"></span>' +
 15697            '</div>' +
 15698            '<div class="md-disabled-thumb"></div>' +
 15699          '</div>' +
 15700        '</div>',
 15701      compile: compile
 15702    };
 15703  
 15704    // **********************************************************
 15705    // Private Methods
 15706    // **********************************************************
 15707  
 15708    function compile (tElement, tAttrs) {
 15709      if (!tAttrs.tabindex) tElement.attr('tabindex', 0);
 15710      tElement.attr('role', 'slider');
 15711  
 15712      $mdAria.expect(tElement, 'aria-label');
 15713  
 15714      return postLink;
 15715    }
 15716  
 15717    function postLink(scope, element, attr, ngModelCtrl) {
 15718      $mdTheming(element);
 15719      ngModelCtrl = ngModelCtrl || {
 15720        // Mock ngModelController if it doesn't exist to give us
 15721        // the minimum functionality needed
 15722        $setViewValue: function(val) {
 15723          this.$viewValue = val;
 15724          this.$viewChangeListeners.forEach(function(cb) { cb(); });
 15725        },
 15726        $parsers: [],
 15727        $formatters: [],
 15728        $viewChangeListeners: []
 15729      };
 15730  
 15731      var isDisabled = false;
 15732  
 15733      attr.$observe('disabled', function (value) {
 15734        isDisabled = $mdUtil.parseAttributeBoolean(value, false);
 15735        updateAriaDisabled();
 15736      });
 15737  
 15738      var thumb = angular.element(element[0].querySelector('.md-thumb'));
 15739      var thumbText = angular.element(element[0].querySelector('.md-thumb-text'));
 15740      var thumbContainer = thumb.parent();
 15741      var trackContainer = angular.element(element[0].querySelector('.md-track-container'));
 15742      var activeTrack = angular.element(element[0].querySelector('.md-track-fill'));
 15743      var tickContainer = angular.element(element[0].querySelector('.md-track-ticks'));
 15744      var throttledRefreshDimensions = $mdUtil.throttle(refreshSliderDimensions, 5000);
 15745  
 15746      // Default values, overridable by attrs
 15747      angular.isDefined(attr.min) ? attr.$observe('min', updateMin) : updateMin(0);
 15748      angular.isDefined(attr.max) ? attr.$observe('max', updateMax) : updateMax(100);
 15749      angular.isDefined(attr.step)? attr.$observe('step', updateStep) : updateStep(1);
 15750  
 15751      $mdGesture.register(element, 'drag');
 15752  
 15753      element
 15754        .on('keydown', keydownListener)
 15755        .on('$md.pressdown', onPressDown)
 15756        .on('$md.pressup', onPressUp)
 15757        .on('$md.dragstart', onDragStart)
 15758        .on('$md.drag', onDrag)
 15759        .on('$md.dragend', onDragEnd);
 15760  
 15761      // On resize, recalculate the slider's dimensions and re-render
 15762      function updateAll() {
 15763        refreshSliderDimensions();
 15764        ngModelRender();
 15765        redrawTicks();
 15766      }
 15767      setTimeout(updateAll, 0);
 15768  
 15769      var debouncedUpdateAll = $$rAF.throttle(updateAll);
 15770      angular.element($window).on('resize', debouncedUpdateAll);
 15771  
 15772      scope.$on('$destroy', function() {
 15773        angular.element($window).off('resize', debouncedUpdateAll);
 15774      });
 15775  
 15776      ngModelCtrl.$render = ngModelRender;
 15777      ngModelCtrl.$viewChangeListeners.push(ngModelRender);
 15778      ngModelCtrl.$formatters.push(minMaxValidator);
 15779      ngModelCtrl.$formatters.push(stepValidator);
 15780  
 15781      /**
 15782       * Attributes
 15783       */
 15784      var min;
 15785      var max;
 15786      var step;
 15787      function updateMin(value) {
 15788        min = parseFloat(value);
 15789        element.attr('aria-valuemin', value);
 15790        updateAll();
 15791      }
 15792      function updateMax(value) {
 15793        max = parseFloat(value);
 15794        element.attr('aria-valuemax', value);
 15795        updateAll();
 15796      }
 15797      function updateStep(value) {
 15798        step = parseFloat(value);
 15799        redrawTicks();
 15800      }
 15801      function updateAriaDisabled() {
 15802        element.attr('aria-disabled', !!isDisabled);
 15803      }
 15804  
 15805      // Draw the ticks with canvas.
 15806      // The alternative to drawing ticks with canvas is to draw one element for each tick,
 15807      // which could quickly become a performance bottleneck.
 15808      var tickCanvas, tickCtx;
 15809      function redrawTicks() {
 15810        if (!angular.isDefined(attr.mdDiscrete)) return;
 15811        if ( angular.isUndefined(step) )         return;
 15812  
 15813        if ( step <= 0 ) {
 15814          var msg = 'Slider step value must be greater than zero when in discrete mode';
 15815          $log.error(msg);
 15816          throw new Error(msg);
 15817        }
 15818  
 15819        var numSteps = Math.floor( (max - min) / step );
 15820        if (!tickCanvas) {
 15821          tickCanvas = angular.element('<canvas>').css('position', 'absolute');
 15822          tickContainer.append(tickCanvas);
 15823  
 15824          var trackTicksStyle = $window.getComputedStyle(tickContainer[0]);
 15825          tickCtx = tickCanvas[0].getContext('2d');
 15826          tickCtx.fillStyle = trackTicksStyle.backgroundColor || 'black';
 15827        }
 15828  
 15829        var dimensions = getSliderDimensions();
 15830        tickCanvas[0].width = dimensions.width;
 15831        tickCanvas[0].height = dimensions.height;
 15832  
 15833        var distance;
 15834        for (var i = 0; i <= numSteps; i++) {
 15835          distance = Math.floor(dimensions.width * (i / numSteps));
 15836          tickCtx.fillRect(distance - 1, 0, 2, dimensions.height);
 15837        }
 15838      }
 15839  
 15840  
 15841      /**
 15842       * Refreshing Dimensions
 15843       */
 15844      var sliderDimensions = {};
 15845      refreshSliderDimensions();
 15846      function refreshSliderDimensions() {
 15847        sliderDimensions = trackContainer[0].getBoundingClientRect();
 15848      }
 15849      function getSliderDimensions() {
 15850        throttledRefreshDimensions();
 15851        return sliderDimensions;
 15852      }
 15853  
 15854      /**
 15855       * left/right arrow listener
 15856       */
 15857      function keydownListener(ev) {
 15858        if (isDisabled) return;
 15859  
 15860        var changeAmount;
 15861        if (ev.keyCode === $mdConstant.KEY_CODE.LEFT_ARROW) {
 15862          changeAmount = -step;
 15863        } else if (ev.keyCode === $mdConstant.KEY_CODE.RIGHT_ARROW) {
 15864          changeAmount = step;
 15865        }
 15866        if (changeAmount) {
 15867          if (ev.metaKey || ev.ctrlKey || ev.altKey) {
 15868            changeAmount *= 4;
 15869          }
 15870          ev.preventDefault();
 15871          ev.stopPropagation();
 15872          scope.$evalAsync(function() {
 15873            setModelValue(ngModelCtrl.$viewValue + changeAmount);
 15874          });
 15875        }
 15876      }
 15877  
 15878      /**
 15879       * ngModel setters and validators
 15880       */
 15881      function setModelValue(value) {
 15882        ngModelCtrl.$setViewValue( minMaxValidator(stepValidator(value)) );
 15883      }
 15884      function ngModelRender() {
 15885        if (isNaN(ngModelCtrl.$viewValue)) {
 15886          ngModelCtrl.$viewValue = ngModelCtrl.$modelValue;
 15887        }
 15888  
 15889        var percent = (ngModelCtrl.$viewValue - min) / (max - min);
 15890        scope.modelValue = ngModelCtrl.$viewValue;
 15891        element.attr('aria-valuenow', ngModelCtrl.$viewValue);
 15892        setSliderPercent(percent);
 15893        thumbText.text( ngModelCtrl.$viewValue );
 15894      }
 15895  
 15896      function minMaxValidator(value) {
 15897        if (angular.isNumber(value)) {
 15898          return Math.max(min, Math.min(max, value));
 15899        }
 15900      }
 15901      function stepValidator(value) {
 15902        if (angular.isNumber(value)) {
 15903          var formattedValue = (Math.round((value - min) / step) * step + min);
 15904          // Format to 3 digits after the decimal point - fixes #2015.
 15905          return (Math.round(formattedValue * 1000) / 1000);
 15906        }
 15907      }
 15908  
 15909      /**
 15910       * @param percent 0-1
 15911       */
 15912      function setSliderPercent(percent) {
 15913  
 15914          percent = clamp(percent);
 15915  
 15916          var percentStr = (percent * 100) + '%';
 15917  
 15918          activeTrack.css('width', percentStr);
 15919          thumbContainer.css('left',percentStr);
 15920  
 15921          element.toggleClass('md-min', percent === 0);
 15922          element.toggleClass('md-max', percent === 1);
 15923      }
 15924  
 15925  
 15926      /**
 15927       * Slide listeners
 15928       */
 15929      var isDragging = false;
 15930      var isDiscrete = angular.isDefined(attr.mdDiscrete);
 15931  
 15932      function onPressDown(ev) {
 15933        if (isDisabled) return;
 15934  
 15935        element.addClass('md-active');
 15936        element[0].focus();
 15937        refreshSliderDimensions();
 15938  
 15939        var exactVal = percentToValue( positionToPercent( ev.pointer.x ));
 15940        var closestVal = minMaxValidator( stepValidator(exactVal) );
 15941        scope.$apply(function() {
 15942          setModelValue( closestVal );
 15943          setSliderPercent( valueToPercent(closestVal));
 15944        });
 15945      }
 15946      function onPressUp(ev) {
 15947        if (isDisabled) return;
 15948  
 15949        element.removeClass('md-dragging md-active');
 15950  
 15951        var exactVal = percentToValue( positionToPercent( ev.pointer.x ));
 15952        var closestVal = minMaxValidator( stepValidator(exactVal) );
 15953        scope.$apply(function() {
 15954          setModelValue(closestVal);
 15955          ngModelRender();
 15956        });
 15957      }
 15958      function onDragStart(ev) {
 15959        if (isDisabled) return;
 15960        isDragging = true;
 15961        ev.stopPropagation();
 15962  
 15963        element.addClass('md-dragging');
 15964        setSliderFromEvent(ev);
 15965      }
 15966      function onDrag(ev) {
 15967        if (!isDragging) return;
 15968        ev.stopPropagation();
 15969        setSliderFromEvent(ev);
 15970      }
 15971      function onDragEnd(ev) {
 15972        if (!isDragging) return;
 15973        ev.stopPropagation();
 15974        isDragging = false;
 15975      }
 15976  
 15977      function setSliderFromEvent(ev) {
 15978        // While panning discrete, update only the
 15979        // visual positioning but not the model value.
 15980        if ( isDiscrete ) adjustThumbPosition( ev.pointer.x );
 15981        else              doSlide( ev.pointer.x );
 15982      }
 15983  
 15984      /**
 15985       * Slide the UI by changing the model value
 15986       * @param x
 15987       */
 15988      function doSlide( x ) {
 15989        scope.$evalAsync( function() {
 15990          setModelValue( percentToValue( positionToPercent(x) ));
 15991        });
 15992      }
 15993  
 15994      /**
 15995       * Slide the UI without changing the model (while dragging/panning)
 15996       * @param x
 15997       */
 15998      function adjustThumbPosition( x ) {
 15999        var exactVal = percentToValue( positionToPercent( x ));
 16000        var closestVal = minMaxValidator( stepValidator(exactVal) );
 16001        setSliderPercent( positionToPercent(x) );
 16002        thumbText.text( closestVal );
 16003      }
 16004  
 16005      /**
 16006      * Clamps the value to be between 0 and 1.
 16007      * @param {number} value The value to clamp.
 16008      * @returns {number}
 16009      */
 16010      function clamp(value) {
 16011        return Math.max(0, Math.min(value || 0, 1));
 16012      }
 16013  
 16014      /**
 16015       * Convert horizontal position on slider to percentage value of offset from beginning...
 16016       * @param x
 16017       * @returns {number}
 16018       */
 16019      function positionToPercent( x ) {
 16020        return Math.max(0, Math.min(1, (x - sliderDimensions.left) / (sliderDimensions.width)));
 16021      }
 16022  
 16023      /**
 16024       * Convert percentage offset on slide to equivalent model value
 16025       * @param percent
 16026       * @returns {*}
 16027       */
 16028      function percentToValue( percent ) {
 16029        return (min + percent * (max - min));
 16030      }
 16031  
 16032      function valueToPercent( val ) {
 16033        return (val - min)/(max - min);
 16034      }
 16035    }
 16036  }
 16037  SliderDirective.$inject = ["$$rAF", "$window", "$mdAria", "$mdUtil", "$mdConstant", "$mdTheming", "$mdGesture", "$parse", "$log"];
 16038  
 16039  })();
 16040  (function(){
 16041  "use strict";
 16042  
 16043  /**
 16044   * @ngdoc module
 16045   * @name material.components.sticky
 16046   * @description
 16047   * Sticky effects for md
 16048   *
 16049   */
 16050  angular
 16051    .module('material.components.sticky', [
 16052      'material.core',
 16053      'material.components.content'
 16054    ])
 16055    .factory('$mdSticky', MdSticky);
 16056  
 16057  /**
 16058   * @ngdoc service
 16059   * @name $mdSticky
 16060   * @module material.components.sticky
 16061   *
 16062   * @description
 16063   * The `$mdSticky`service provides a mixin to make elements sticky.
 16064   *
 16065   * By default the `$mdSticky` service compiles the cloned element, when not specified through the `elementClone`
 16066   * parameter, in the same scope as the actual element lives.
 16067   *
 16068   *
 16069   * <h3>Notes</h3>
 16070   * When using an element which is containing a compiled directive, which changed its DOM structure during compilation,
 16071   * you should compile the clone yourself using the plain template.<br/><br/>
 16072   * See the right usage below:
 16073   * <hljs lang="js">
 16074   *   angular.module('myModule')
 16075   *     .directive('stickySelect', function($mdSticky, $compile) {
 16076   *       var SELECT_TEMPLATE =
 16077   *         '<md-select ng-model="selected">' +
 16078   *           '<md-option>Option 1</md-option>' +
 16079   *         '</md-select>';
 16080   *
 16081   *       return {
 16082   *         restrict: 'E',
 16083   *         replace: true,
 16084   *         template: SELECT_TEMPLATE,
 16085   *         link: function(scope,element) {
 16086   *           $mdSticky(scope, element, $compile(SELECT_TEMPLATE)(scope));
 16087   *         }
 16088   *       };
 16089   *     });
 16090   * </hljs>
 16091   *
 16092   * @usage
 16093   * <hljs lang="js">
 16094   *   angular.module('myModule')
 16095   *     .directive('stickyText', function($mdSticky, $compile) {
 16096   *       return {
 16097   *         restrict: 'E',
 16098   *         template: '<span>Sticky Text</span>',
 16099   *         link: function(scope,element) {
 16100   *           $mdSticky(scope, element);
 16101   *         }
 16102   *       };
 16103   *     });
 16104   * </hljs>
 16105   *
 16106   * @returns A `$mdSticky` function that takes three arguments:
 16107   *   - `scope`
 16108   *   - `element`: The element that will be 'sticky'
 16109   *   - `elementClone`: A clone of the element, that will be shown
 16110   *     when the user starts scrolling past the original element.
 16111   *     If not provided, it will use the result of `element.clone()` and compiles it in the given scope.
 16112   */
 16113  function MdSticky($document, $mdConstant, $$rAF, $mdUtil, $compile) {
 16114  
 16115    var browserStickySupport = checkStickySupport();
 16116  
 16117    /**
 16118     * Registers an element as sticky, used internally by directives to register themselves
 16119     */
 16120    return function registerStickyElement(scope, element, stickyClone) {
 16121      var contentCtrl = element.controller('mdContent');
 16122      if (!contentCtrl) return;
 16123  
 16124      if (browserStickySupport) {
 16125        element.css({
 16126          position: browserStickySupport,
 16127          top: 0,
 16128          'z-index': 2
 16129        });
 16130      } else {
 16131        var $$sticky = contentCtrl.$element.data('$$sticky');
 16132        if (!$$sticky) {
 16133          $$sticky = setupSticky(contentCtrl);
 16134          contentCtrl.$element.data('$$sticky', $$sticky);
 16135        }
 16136  
 16137        // Compile our cloned element, when cloned in this service, into the given scope.
 16138        var cloneElement = stickyClone || $compile(element.clone())(scope);
 16139  
 16140        var deregister = $$sticky.add(element, cloneElement);
 16141        scope.$on('$destroy', deregister);
 16142      }
 16143    };
 16144  
 16145    function setupSticky(contentCtrl) {
 16146      var contentEl = contentCtrl.$element;
 16147  
 16148      // Refresh elements is very expensive, so we use the debounced
 16149      // version when possible.
 16150      var debouncedRefreshElements = $$rAF.throttle(refreshElements);
 16151  
 16152      // setupAugmentedScrollEvents gives us `$scrollstart` and `$scroll`,
 16153      // more reliable than `scroll` on android.
 16154      setupAugmentedScrollEvents(contentEl);
 16155      contentEl.on('$scrollstart', debouncedRefreshElements);
 16156      contentEl.on('$scroll', onScroll);
 16157  
 16158      var self;
 16159      return self = {
 16160        prev: null,
 16161        current: null, //the currently stickied item
 16162        next: null,
 16163        items: [],
 16164        add: add,
 16165        refreshElements: refreshElements
 16166      };
 16167  
 16168      /***************
 16169       * Public
 16170       ***************/
 16171      // Add an element and its sticky clone to this content's sticky collection
 16172      function add(element, stickyClone) {
 16173        stickyClone.addClass('md-sticky-clone');
 16174  
 16175        var item = {
 16176          element: element,
 16177          clone: stickyClone
 16178        };
 16179        self.items.push(item);
 16180  
 16181        $mdUtil.nextTick(function() {
 16182          contentEl.prepend(item.clone);
 16183        });
 16184  
 16185        debouncedRefreshElements();
 16186  
 16187        return function remove() {
 16188          self.items.forEach(function(item, index) {
 16189            if (item.element[0] === element[0]) {
 16190              self.items.splice(index, 1);
 16191              item.clone.remove();
 16192            }
 16193          });
 16194          debouncedRefreshElements();
 16195        };
 16196      }
 16197  
 16198      function refreshElements() {
 16199        // Sort our collection of elements by their current position in the DOM.
 16200        // We need to do this because our elements' order of being added may not
 16201        // be the same as their order of display.
 16202        self.items.forEach(refreshPosition);
 16203        self.items = self.items.sort(function(a, b) {
 16204          return a.top < b.top ? -1 : 1;
 16205        });
 16206  
 16207        // Find which item in the list should be active, 
 16208        // based upon the content's current scroll position
 16209        var item;
 16210        var currentScrollTop = contentEl.prop('scrollTop');
 16211        for (var i = self.items.length - 1; i >= 0; i--) {
 16212          if (currentScrollTop > self.items[i].top) {
 16213            item = self.items[i];
 16214            break;
 16215          }
 16216        }
 16217        setCurrentItem(item);
 16218      }
 16219  
 16220      /***************
 16221       * Private
 16222       ***************/
 16223  
 16224      // Find the `top` of an item relative to the content element,
 16225      // and also the height.
 16226      function refreshPosition(item) {
 16227        // Find the top of an item by adding to the offsetHeight until we reach the 
 16228        // content element.
 16229        var current = item.element[0];
 16230        item.top = 0;
 16231        item.left = 0;
 16232        while (current && current !== contentEl[0]) {
 16233          item.top += current.offsetTop;
 16234          item.left += current.offsetLeft;
 16235          if ( current.offsetParent ){
 16236            item.right += current.offsetParent.offsetWidth - current.offsetWidth - current.offsetLeft; //Compute offsetRight
 16237          }
 16238          current = current.offsetParent;
 16239        }
 16240        item.height = item.element.prop('offsetHeight');
 16241        item.clone.css('margin-left', item.left + 'px');
 16242        if ($mdUtil.floatingScrollbars()) {
 16243          item.clone.css('margin-right', '0');
 16244        }
 16245      }
 16246  
 16247      // As we scroll, push in and select the correct sticky element.
 16248      function onScroll() {
 16249        var scrollTop = contentEl.prop('scrollTop');
 16250        var isScrollingDown = scrollTop > (onScroll.prevScrollTop || 0);
 16251  
 16252        // Store the previous scroll so we know which direction we are scrolling
 16253        onScroll.prevScrollTop = scrollTop;
 16254  
 16255        //
 16256        // AT TOP (not scrolling)
 16257        //
 16258        if (scrollTop === 0) {
 16259          // If we're at the top, just clear the current item and return
 16260          setCurrentItem(null);
 16261          return;
 16262        }
 16263  
 16264        //
 16265        // SCROLLING DOWN (going towards the next item)
 16266        //
 16267        if (isScrollingDown) {
 16268  
 16269          // If we've scrolled down past the next item's position, sticky it and return
 16270          if (self.next && self.next.top <= scrollTop) {
 16271            setCurrentItem(self.next);
 16272            return;
 16273          }
 16274  
 16275          // If the next item is close to the current one, push the current one up out of the way
 16276          if (self.current && self.next && self.next.top - scrollTop <= self.next.height) {
 16277            translate(self.current, scrollTop + (self.next.top - self.next.height - scrollTop));
 16278            return;
 16279          }
 16280        }
 16281  
 16282        //
 16283        // SCROLLING UP (not at the top & not scrolling down; must be scrolling up)
 16284        //
 16285        if (!isScrollingDown) {
 16286  
 16287          // If we've scrolled up past the previous item's position, sticky it and return
 16288          if (self.current && self.prev && scrollTop < self.current.top) {
 16289            setCurrentItem(self.prev);
 16290            return;
 16291          }
 16292  
 16293          // If the next item is close to the current one, pull the current one down into view
 16294          if (self.next && self.current && (scrollTop >= (self.next.top - self.current.height))) {
 16295            translate(self.current, scrollTop + (self.next.top - scrollTop - self.current.height));
 16296            return;
 16297          }
 16298        }
 16299  
 16300        //
 16301        // Otherwise, just move the current item to the proper place (scrolling up or down)
 16302        //
 16303        if (self.current) {
 16304          translate(self.current, scrollTop);
 16305        }
 16306      }
 16307  
 16308      function setCurrentItem(item) {
 16309        if (self.current === item) return;
 16310        // Deactivate currently active item
 16311        if (self.current) {
 16312          translate(self.current, null);
 16313          setStickyState(self.current, null);
 16314        }
 16315  
 16316        // Activate new item if given
 16317        if (item) {
 16318          setStickyState(item, 'active');
 16319        }
 16320  
 16321        self.current = item;
 16322        var index = self.items.indexOf(item);
 16323        // If index === -1, index + 1 = 0. It works out.
 16324        self.next = self.items[index + 1];
 16325        self.prev = self.items[index - 1];
 16326        setStickyState(self.next, 'next');
 16327        setStickyState(self.prev, 'prev');
 16328      }
 16329  
 16330      function setStickyState(item, state) {
 16331        if (!item || item.state === state) return;
 16332        if (item.state) {
 16333          item.clone.attr('sticky-prev-state', item.state);
 16334          item.element.attr('sticky-prev-state', item.state);
 16335        }
 16336        item.clone.attr('sticky-state', state);
 16337        item.element.attr('sticky-state', state);
 16338        item.state = state;
 16339      }
 16340  
 16341      function translate(item, amount) {
 16342        if (!item) return;
 16343        if (amount === null || amount === undefined) {
 16344          if (item.translateY) {
 16345            item.translateY = null;
 16346            item.clone.css($mdConstant.CSS.TRANSFORM, '');
 16347          }
 16348        } else {
 16349          item.translateY = amount;
 16350          item.clone.css(
 16351            $mdConstant.CSS.TRANSFORM,
 16352            'translate3d(' + item.left + 'px,' + amount + 'px,0)'
 16353          );
 16354        }
 16355      }
 16356    }
 16357  
 16358    // Function to check for browser sticky support
 16359    function checkStickySupport($el) {
 16360      var stickyProp;
 16361      var testEl = angular.element('<div>');
 16362      $document[0].body.appendChild(testEl[0]);
 16363  
 16364      var stickyProps = ['sticky', '-webkit-sticky'];
 16365      for (var i = 0; i < stickyProps.length; ++i) {
 16366        testEl.css({position: stickyProps[i], top: 0, 'z-index': 2});
 16367        if (testEl.css('position') == stickyProps[i]) {
 16368          stickyProp = stickyProps[i];
 16369          break;
 16370        }
 16371      }
 16372      testEl.remove();
 16373      return stickyProp;
 16374    }
 16375  
 16376    // Android 4.4 don't accurately give scroll events.
 16377    // To fix this problem, we setup a fake scroll event. We say:
 16378    // > If a scroll or touchmove event has happened in the last DELAY milliseconds, 
 16379    //   then send a `$scroll` event every animationFrame.
 16380    // Additionally, we add $scrollstart and $scrollend events.
 16381    function setupAugmentedScrollEvents(element) {
 16382      var SCROLL_END_DELAY = 200;
 16383      var isScrolling;
 16384      var lastScrollTime;
 16385      element.on('scroll touchmove', function() {
 16386        if (!isScrolling) {
 16387          isScrolling = true;
 16388          $$rAF.throttle(loopScrollEvent);
 16389          element.triggerHandler('$scrollstart');
 16390        }
 16391        element.triggerHandler('$scroll');
 16392        lastScrollTime = +$mdUtil.now();
 16393      });
 16394  
 16395      function loopScrollEvent() {
 16396        if (+$mdUtil.now() - lastScrollTime > SCROLL_END_DELAY) {
 16397          isScrolling = false;
 16398          element.triggerHandler('$scrollend');
 16399        } else {
 16400          element.triggerHandler('$scroll');
 16401          $$rAF.throttle(loopScrollEvent);
 16402        }
 16403      }
 16404    }
 16405  
 16406  }
 16407  MdSticky.$inject = ["$document", "$mdConstant", "$$rAF", "$mdUtil", "$compile"];
 16408  
 16409  })();
 16410  (function(){
 16411  "use strict";
 16412  
 16413  /**
 16414   * @ngdoc module
 16415   * @name material.components.subheader
 16416   * @description
 16417   * SubHeader module
 16418   *
 16419   *  Subheaders are special list tiles that delineate distinct sections of a
 16420   *  list or grid list and are typically related to the current filtering or
 16421   *  sorting criteria. Subheader tiles are either displayed inline with tiles or
 16422   *  can be associated with content, for example, in an adjacent column.
 16423   *
 16424   *  Upon scrolling, subheaders remain pinned to the top of the screen and remain
 16425   *  pinned until pushed on or off screen by the next subheader. @see [Material
 16426   *  Design Specifications](https://www.google.com/design/spec/components/subheaders.html)
 16427   *
 16428   *  > To improve the visual grouping of content, use the system color for your subheaders.
 16429   *
 16430   */
 16431  angular
 16432    .module('material.components.subheader', [
 16433      'material.core',
 16434      'material.components.sticky'
 16435    ])
 16436    .directive('mdSubheader', MdSubheaderDirective);
 16437  
 16438  /**
 16439   * @ngdoc directive
 16440   * @name mdSubheader
 16441   * @module material.components.subheader
 16442   *
 16443   * @restrict E
 16444   *
 16445   * @description
 16446   * The `<md-subheader>` directive is a subheader for a section. By default it is sticky.
 16447   * You can make it not sticky by applying the `md-no-sticky` class to the subheader.
 16448   *
 16449   *
 16450   * @usage
 16451   * <hljs lang="html">
 16452   * <md-subheader>Online Friends</md-subheader>
 16453   * </hljs>
 16454   */
 16455  
 16456  function MdSubheaderDirective($mdSticky, $compile, $mdTheming, $mdUtil) {
 16457    return {
 16458      restrict: 'E',
 16459      replace: true,
 16460      transclude: true,
 16461      template: (
 16462      '<div class="md-subheader">' +
 16463      '  <div class="md-subheader-inner">' +
 16464      '    <span class="md-subheader-content"></span>' +
 16465      '  </div>' +
 16466      '</div>'
 16467      ),
 16468      link: function postLink(scope, element, attr, controllers, transclude) {
 16469        $mdTheming(element);
 16470        var outerHTML = element[0].outerHTML;
 16471  
 16472        function getContent(el) {
 16473          return angular.element(el[0].querySelector('.md-subheader-content'));
 16474        }
 16475  
 16476        // Transclude the user-given contents of the subheader
 16477        // the conventional way.
 16478        transclude(scope, function(clone) {
 16479          getContent(element).append(clone);
 16480        });
 16481  
 16482        // Create another clone, that uses the outer and inner contents
 16483        // of the element, that will be 'stickied' as the user scrolls.
 16484        if (!element.hasClass('md-no-sticky')) {
 16485          transclude(scope, function(clone) {
 16486            // If the user adds an ng-if or ng-repeat directly to the md-subheader element, the
 16487            // compiled clone below will only be a comment tag (since they replace their elements with
 16488            // a comment) which cannot be properly passed to the $mdSticky; so we wrap it in our own
 16489            // DIV to ensure we have something $mdSticky can use
 16490            var wrapperHtml = '<div class="md-subheader-wrapper">' + outerHTML + '</div>';
 16491            var stickyClone = $compile(wrapperHtml)(scope);
 16492  
 16493            // Append the sticky
 16494            $mdSticky(scope, element, stickyClone);
 16495  
 16496            // Delay initialization until after any `ng-if`/`ng-repeat`/etc has finished before
 16497            // attempting to create the clone
 16498            $mdUtil.nextTick(function() {
 16499              getContent(stickyClone).append(clone);
 16500            });
 16501          });
 16502        }
 16503      }
 16504    }
 16505  }
 16506  MdSubheaderDirective.$inject = ["$mdSticky", "$compile", "$mdTheming", "$mdUtil"];
 16507  
 16508  })();
 16509  (function(){
 16510  "use strict";
 16511  
 16512  /**
 16513   * @ngdoc module
 16514   * @name material.components.swipe
 16515   * @description Swipe module!
 16516   */
 16517  /**
 16518   * @ngdoc directive
 16519   * @module material.components.swipe
 16520   * @name mdSwipeLeft
 16521   *
 16522   * @restrict A
 16523   *
 16524   * @description
 16525   * The md-swipe-left directive allows you to specify custom behavior when an element is swiped
 16526   * left.
 16527   *
 16528   * @usage
 16529   * <hljs lang="html">
 16530   * <div md-swipe-left="onSwipeLeft()">Swipe me left!</div>
 16531   * </hljs>
 16532   */
 16533  /**
 16534   * @ngdoc directive
 16535   * @module material.components.swipe
 16536   * @name mdSwipeRight
 16537   *
 16538   * @restrict A
 16539   *
 16540   * @description
 16541   * The md-swipe-right directive allows you to specify custom behavior when an element is swiped
 16542   * right.
 16543   *
 16544   * @usage
 16545   * <hljs lang="html">
 16546   * <div md-swipe-right="onSwipeRight()">Swipe me right!</div>
 16547   * </hljs>
 16548   */
 16549  /**
 16550   * @ngdoc directive
 16551   * @module material.components.swipe
 16552   * @name mdSwipeUp
 16553   *
 16554   * @restrict A
 16555   *
 16556   * @description
 16557   * The md-swipe-up directive allows you to specify custom behavior when an element is swiped
 16558   * up.
 16559   *
 16560   * @usage
 16561   * <hljs lang="html">
 16562   * <div md-swipe-up="onSwipeUp()">Swipe me up!</div>
 16563   * </hljs>
 16564   */
 16565  /**
 16566   * @ngdoc directive
 16567   * @module material.components.swipe
 16568   * @name mdSwipeDown
 16569   *
 16570   * @restrict A
 16571   *
 16572   * @description
 16573   * The md-swipe-down directive allows you to specify custom behavior when an element is swiped
 16574   * down.
 16575   *
 16576   * @usage
 16577   * <hljs lang="html">
 16578   * <div md-swipe-down="onSwipDown()">Swipe me down!</div>
 16579   * </hljs>
 16580   */
 16581  
 16582  angular.module('material.components.swipe', ['material.core'])
 16583      .directive('mdSwipeLeft', getDirective('SwipeLeft'))
 16584      .directive('mdSwipeRight', getDirective('SwipeRight'))
 16585      .directive('mdSwipeUp', getDirective('SwipeUp'))
 16586      .directive('mdSwipeDown', getDirective('SwipeDown'));
 16587  
 16588  function getDirective(name) {
 16589    var directiveName = 'md' + name;
 16590    var eventName = '$md.' + name.toLowerCase();
 16591  
 16592      DirectiveFactory.$inject = ["$parse"];
 16593    return DirectiveFactory;
 16594  
 16595    /* @ngInject */
 16596    function DirectiveFactory($parse) {
 16597        return { restrict: 'A', link: postLink };
 16598        function postLink(scope, element, attr) {
 16599          var fn = $parse(attr[directiveName]);
 16600          element.on(eventName, function(ev) {
 16601            scope.$apply(function() { fn(scope, { $event: ev }); });
 16602          });
 16603        }
 16604      }
 16605  }
 16606  
 16607  
 16608  
 16609  })();
 16610  (function(){
 16611  "use strict";
 16612  
 16613  /**
 16614   * @private
 16615   * @ngdoc module
 16616   * @name material.components.switch
 16617   */
 16618  
 16619  angular.module('material.components.switch', [
 16620    'material.core',
 16621    'material.components.checkbox'
 16622  ])
 16623    .directive('mdSwitch', MdSwitch);
 16624  
 16625  /**
 16626   * @private
 16627   * @ngdoc directive
 16628   * @module material.components.switch
 16629   * @name mdSwitch
 16630   * @restrict E
 16631   *
 16632   * The switch directive is used very much like the normal [angular checkbox](https://docs.angularjs.org/api/ng/input/input%5Bcheckbox%5D).
 16633   *
 16634   * As per the [material design spec](http://www.google.com/design/spec/style/color.html#color-ui-color-application)
 16635   * the switch is in the accent color by default. The primary color palette may be used with
 16636   * the `md-primary` class.
 16637   *
 16638   * @param {string} ng-model Assignable angular expression to data-bind to.
 16639   * @param {string=} name Property name of the form under which the control is published.
 16640   * @param {expression=} ng-true-value The value to which the expression should be set when selected.
 16641   * @param {expression=} ng-false-value The value to which the expression should be set when not selected.
 16642   * @param {string=} ng-change Angular expression to be executed when input changes due to user interaction with the input element.
 16643   * @param {expression=} ng-disabled En/Disable based on the expression.
 16644   * @param {boolean=} md-no-ink Use of attribute indicates use of ripple ink effects.
 16645   * @param {string=} aria-label Publish the button label used by screen-readers for accessibility. Defaults to the switch's text.
 16646   *
 16647   * @usage
 16648   * <hljs lang="html">
 16649   * <md-switch ng-model="isActive" aria-label="Finished?">
 16650   *   Finished ?
 16651   * </md-switch>
 16652   *
 16653   * <md-switch md-no-ink ng-model="hasInk" aria-label="No Ink Effects">
 16654   *   No Ink Effects
 16655   * </md-switch>
 16656   *
 16657   * <md-switch ng-disabled="true" ng-model="isDisabled" aria-label="Disabled">
 16658   *   Disabled
 16659   * </md-switch>
 16660   *
 16661   * </hljs>
 16662   */
 16663  function MdSwitch(mdCheckboxDirective, $mdUtil, $mdConstant, $parse, $$rAF, $mdGesture) {
 16664    var checkboxDirective = mdCheckboxDirective[0];
 16665  
 16666    return {
 16667      restrict: 'E',
 16668      priority: 210, // Run before ngAria
 16669      transclude: true,
 16670      template:
 16671        '<div class="md-container">' +
 16672          '<div class="md-bar"></div>' +
 16673          '<div class="md-thumb-container">' +
 16674            '<div class="md-thumb" md-ink-ripple md-ink-ripple-checkbox></div>' +
 16675          '</div>'+
 16676        '</div>' +
 16677        '<div ng-transclude class="md-label"></div>',
 16678      require: '?ngModel',
 16679      compile: mdSwitchCompile
 16680    };
 16681  
 16682    function mdSwitchCompile(element, attr) {
 16683      var checkboxLink = checkboxDirective.compile(element, attr);
 16684      // No transition on initial load.
 16685      element.addClass('md-dragging');
 16686  
 16687      return function (scope, element, attr, ngModel) {
 16688        ngModel = ngModel || $mdUtil.fakeNgModel();
 16689  
 16690        var disabledGetter = null;
 16691        if (attr.disabled != null) {
 16692          disabledGetter = function() { return true; };
 16693        } else if (attr.ngDisabled) {
 16694          disabledGetter = $parse(attr.ngDisabled);
 16695        }
 16696  
 16697        var thumbContainer = angular.element(element[0].querySelector('.md-thumb-container'));
 16698        var switchContainer = angular.element(element[0].querySelector('.md-container'));
 16699  
 16700        // no transition on initial load
 16701        $$rAF(function() {
 16702          element.removeClass('md-dragging');
 16703        });
 16704  
 16705        checkboxLink(scope, element, attr, ngModel);
 16706  
 16707        if (disabledGetter) {
 16708          scope.$watch(disabledGetter, function(isDisabled) {
 16709            element.attr('tabindex', isDisabled ? -1 : 0);
 16710          });
 16711        }
 16712  
 16713        // These events are triggered by setup drag
 16714        $mdGesture.register(switchContainer, 'drag');
 16715        switchContainer
 16716          .on('$md.dragstart', onDragStart)
 16717          .on('$md.drag', onDrag)
 16718          .on('$md.dragend', onDragEnd);
 16719  
 16720        var drag;
 16721        function onDragStart(ev) {
 16722          // Don't go if the switch is disabled.
 16723          if (disabledGetter && disabledGetter(scope)) return;
 16724          ev.stopPropagation();
 16725  
 16726          element.addClass('md-dragging');
 16727          drag = {width: thumbContainer.prop('offsetWidth')};
 16728          element.removeClass('transition');
 16729        }
 16730  
 16731        function onDrag(ev) {
 16732          if (!drag) return;
 16733          ev.stopPropagation();
 16734          ev.srcEvent && ev.srcEvent.preventDefault();
 16735  
 16736          var percent = ev.pointer.distanceX / drag.width;
 16737  
 16738          //if checked, start from right. else, start from left
 16739          var translate = ngModel.$viewValue ?  1 + percent : percent;
 16740          // Make sure the switch stays inside its bounds, 0-1%
 16741          translate = Math.max(0, Math.min(1, translate));
 16742  
 16743          thumbContainer.css($mdConstant.CSS.TRANSFORM, 'translate3d(' + (100*translate) + '%,0,0)');
 16744          drag.translate = translate;
 16745        }
 16746  
 16747        function onDragEnd(ev) {
 16748          if (!drag) return;
 16749          ev.stopPropagation();
 16750  
 16751          element.removeClass('md-dragging');
 16752          thumbContainer.css($mdConstant.CSS.TRANSFORM, '');
 16753  
 16754          // We changed if there is no distance (this is a click a click),
 16755          // or if the drag distance is >50% of the total.
 16756          var isChanged = ngModel.$viewValue ? drag.translate > 0.5 : drag.translate < 0.5;
 16757          if (isChanged) {
 16758            applyModelValue(!ngModel.$viewValue);
 16759          }
 16760          drag = null;
 16761        }
 16762  
 16763        function applyModelValue(newValue) {
 16764          scope.$apply(function() {
 16765            ngModel.$setViewValue(newValue);
 16766            ngModel.$render();
 16767          });
 16768        }
 16769  
 16770      };
 16771    }
 16772  
 16773  
 16774  }
 16775  MdSwitch.$inject = ["mdCheckboxDirective", "$mdUtil", "$mdConstant", "$parse", "$$rAF", "$mdGesture"];
 16776  
 16777  })();
 16778  (function(){
 16779  "use strict";
 16780  
 16781  /**
 16782   * @ngdoc module
 16783   * @name material.components.tabs
 16784   * @description
 16785   *
 16786   *  Tabs, created with the `<md-tabs>` directive provide *tabbed* navigation with different styles.
 16787   *  The Tabs component consists of clickable tabs that are aligned horizontally side-by-side.
 16788   *
 16789   *  Features include support for:
 16790   *
 16791   *  - static or dynamic tabs,
 16792   *  - responsive designs,
 16793   *  - accessibility support (ARIA),
 16794   *  - tab pagination,
 16795   *  - external or internal tab content,
 16796   *  - focus indicators and arrow-key navigations,
 16797   *  - programmatic lookup and access to tab controllers, and
 16798   *  - dynamic transitions through different tab contents.
 16799   *
 16800   */
 16801  /*
 16802   * @see js folder for tabs implementation
 16803   */
 16804  angular.module('material.components.tabs', [
 16805    'material.core',
 16806    'material.components.icon'
 16807  ]);
 16808  
 16809  })();
 16810  (function(){
 16811  "use strict";
 16812  
 16813  /**
 16814    * @ngdoc module
 16815    * @name material.components.toast
 16816    * @description
 16817    * Toast
 16818    */
 16819  angular.module('material.components.toast', [
 16820    'material.core',
 16821    'material.components.button'
 16822  ])
 16823    .directive('mdToast', MdToastDirective)
 16824    .provider('$mdToast', MdToastProvider);
 16825  
 16826  /* @ngInject */
 16827  function MdToastDirective($mdToast) {
 16828    return {
 16829      restrict: 'E',
 16830      link: function postLink(scope, element, attr) {
 16831        // When navigation force destroys an interimElement, then
 16832        // listen and $destroy() that interim instance...
 16833        scope.$on('$destroy', function() {
 16834          $mdToast.destroy();
 16835        });
 16836      }
 16837    };
 16838  }
 16839  MdToastDirective.$inject = ["$mdToast"];
 16840  
 16841  /**
 16842    * @ngdoc service
 16843    * @name $mdToast
 16844    * @module material.components.toast
 16845    *
 16846    * @description
 16847    * `$mdToast` is a service to build a toast notification on any position
 16848    * on the screen with an optional duration, and provides a simple promise API.
 16849    *
 16850    * The toast will be always positioned at the `bottom`, when the screen size is
 16851    * between `600px` and `959px` (`sm` breakpoint)
 16852    *
 16853    * ## Restrictions on custom toasts
 16854    * - The toast's template must have an outer `<md-toast>` element.
 16855    * - For a toast action, use element with class `md-action`.
 16856    * - Add the class `md-capsule` for curved corners.
 16857    *
 16858    * ## Parent container notes
 16859    *
 16860    * The toast is positioned using absolute positioning relative to it's first non-static parent
 16861    * container. Thus, if the requested parent container uses static positioning, we will temporarily
 16862    * set it's positioning to `relative` while the toast is visible and reset it when the toast is
 16863    * hidden.
 16864    *
 16865    * Because of this, it is usually best to ensure that the parent container has a fixed height and
 16866    * prevents scrolling by setting the `overflow: hidden;` style. Since the position is based off of
 16867    * the parent's height, the toast may be mispositioned if you allow the parent to scroll.
 16868    *
 16869    * You can, however, have a scrollable element inside of the container; just make sure the
 16870    * container itself does not scroll.
 16871    *
 16872    * <hljs lang="html">
 16873    * <div layout-fill id="toast-container">
 16874    *   <md-content>
 16875    *     I can have lots of content and scroll!
 16876    *   </md-content>
 16877    * </div>
 16878    * </hljs>
 16879    *
 16880    * @usage
 16881    * <hljs lang="html">
 16882    * <div ng-controller="MyController">
 16883    *   <md-button ng-click="openToast()">
 16884    *     Open a Toast!
 16885    *   </md-button>
 16886    * </div>
 16887    * </hljs>
 16888    *
 16889    * <hljs lang="js">
 16890    * var app = angular.module('app', ['ngMaterial']);
 16891    * app.controller('MyController', function($scope, $mdToast) {
 16892    *   $scope.openToast = function($event) {
 16893    *     $mdToast.show($mdToast.simple().textContent('Hello!'));
 16894    *     // Could also do $mdToast.showSimple('Hello');
 16895    *   };
 16896    * });
 16897    * </hljs>
 16898    */
 16899  
 16900  /**
 16901   * @ngdoc method
 16902   * @name $mdToast#showSimple
 16903   * 
 16904   * @param {string} message The message to display inside the toast
 16905   * @description
 16906   * Convenience method which builds and shows a simple toast.
 16907   *
 16908   * @returns {promise} A promise that can be resolved with `$mdToast.hide()` or
 16909   * rejected with `$mdToast.cancel()`.
 16910   *
 16911   */
 16912  
 16913   /**
 16914    * @ngdoc method
 16915    * @name $mdToast#simple
 16916    *
 16917    * @description
 16918    * Builds a preconfigured toast.
 16919    *
 16920    * @returns {obj} a `$mdToastPreset` with the following chainable configuration methods.
 16921    *
 16922    * _**Note:** These configuration methods are provided in addition to the methods provided by
 16923    *   the `build()` and `show()` methods below._
 16924    *
 16925    * - `.textContent(string)` - Sets the toast content to the specified string.
 16926    *
 16927    * - `.action(string)` - Adds an action button. If clicked, the promise (returned from `show()`)
 16928    * will resolve with the value `'ok'`; otherwise, it is resolved with `true` after a `hideDelay`
 16929    * timeout.
 16930    *
 16931    * - `.highlightAction(boolean)` - Whether or not the action button will have an additional
 16932    * highlight class.
 16933    *
 16934    * - `.capsule(boolean)` - Whether or not to add the `md-capsule` class to the toast to provide
 16935    * rounded corners.
 16936    *
 16937    * - `.theme(string)` - Sets the theme on the toast to the requested theme. Default is
 16938    * `$mdThemingProvider`'s default.
 16939    */
 16940  
 16941  /**
 16942    * @ngdoc method
 16943    * @name $mdToast#updateTextContent
 16944    *
 16945    * @description
 16946    * Updates the content of an existing toast. Useful for updating things like counts, etc.
 16947    *
 16948    */
 16949  
 16950   /**
 16951    * @ngdoc method
 16952    * @name $mdToast#build
 16953    *
 16954    * @description
 16955    * Creates a custom `$mdToastPreset` that you can configure.
 16956    *
 16957    * @returns {obj} a `$mdToastPreset` with the chainable configuration methods for shows' options (see below).
 16958    */
 16959  
 16960   /**
 16961    * @ngdoc method
 16962    * @name $mdToast#show
 16963    *
 16964    * @description Shows the toast.
 16965    *
 16966    * @param {object} optionsOrPreset Either provide an `$mdToastPreset` returned from `simple()`
 16967    * and `build()`, or an options object with the following properties:
 16968    *
 16969    *   - `templateUrl` - `{string=}`: The url of an html template file that will
 16970    *     be used as the content of the toast. Restrictions: the template must
 16971    *     have an outer `md-toast` element.
 16972    *   - `template` - `{string=}`: Same as templateUrl, except this is an actual
 16973    *     template string.
 16974    *   - `autoWrap` - `{boolean=}`: Whether or not to automatically wrap the template content with a
 16975    *     `<div class="md-toast-content">` if one is not provided. Defaults to true. Can be disabled if you provide a
 16976    *     custom toast directive.
 16977    *   - `scope` - `{object=}`: the scope to link the template / controller to. If none is specified, it will create a new child scope.
 16978    *     This scope will be destroyed when the toast is removed unless `preserveScope` is set to true.
 16979    *   - `preserveScope` - `{boolean=}`: whether to preserve the scope when the element is removed. Default is false
 16980    *   - `hideDelay` - `{number=}`: How many milliseconds the toast should stay
 16981    *     active before automatically closing.  Set to 0 or false to have the toast stay open until
 16982    *     closed manually. Default: 3000.
 16983    *   - `position` - `{string=}`: Where to place the toast. Available: any combination
 16984    *     of 'bottom', 'left', 'top', 'right'. Default: 'bottom left'.
 16985    *   - `controller` - `{string=}`: The controller to associate with this toast.
 16986    *     The controller will be injected the local `$mdToast.hide( )`, which is a function
 16987    *     used to hide the toast.
 16988    *   - `locals` - `{string=}`: An object containing key/value pairs. The keys will
 16989    *     be used as names of values to inject into the controller. For example,
 16990    *     `locals: {three: 3}` would inject `three` into the controller with the value
 16991    *     of 3.
 16992    *   - `bindToController` - `bool`: bind the locals to the controller, instead of passing them in.
 16993    *   - `resolve` - `{object=}`: Similar to locals, except it takes promises as values
 16994    *     and the toast will not open until the promises resolve.
 16995    *   - `controllerAs` - `{string=}`: An alias to assign the controller to on the scope.
 16996    *   - `parent` - `{element=}`: The element to append the toast to. Defaults to appending
 16997    *     to the root element of the application.
 16998    *
 16999    * @returns {promise} A promise that can be resolved with `$mdToast.hide()` or
 17000    * rejected with `$mdToast.cancel()`. `$mdToast.hide()` will resolve either with a Boolean
 17001    * value == 'true' or the value passed as an argument to `$mdToast.hide()`.
 17002    * And `$mdToast.cancel()` will resolve the promise with a Boolean value == 'false'
 17003    */
 17004  
 17005  /**
 17006    * @ngdoc method
 17007    * @name $mdToast#hide
 17008    *
 17009    * @description
 17010    * Hide an existing toast and resolve the promise returned from `$mdToast.show()`.
 17011    *
 17012    * @param {*=} response An argument for the resolved promise.
 17013    *
 17014    * @returns {promise} a promise that is called when the existing element is removed from the DOM.
 17015    * The promise is resolved with either a Boolean value == 'true' or the value passed as the
 17016    * argument to `.hide()`.
 17017    *
 17018    */
 17019  
 17020  /**
 17021    * @ngdoc method
 17022    * @name $mdToast#cancel
 17023    *
 17024    * @description
 17025    * `DEPRECATED` - The promise returned from opening a toast is used only to notify about the closing of the toast.
 17026    * As such, there isn't any reason to also allow that promise to be rejected,
 17027    * since it's not clear what the difference between resolve and reject would be.
 17028    *
 17029    * Hide the existing toast and reject the promise returned from
 17030    * `$mdToast.show()`.
 17031    *
 17032    * @param {*=} response An argument for the rejected promise.
 17033    *
 17034    * @returns {promise} a promise that is called when the existing element is removed from the DOM
 17035    * The promise is resolved with a Boolean value == 'false'.
 17036    *
 17037    */
 17038  
 17039  function MdToastProvider($$interimElementProvider) {
 17040    // Differentiate promise resolves: hide timeout (value == true) and hide action clicks (value == ok).
 17041    var ACTION_RESOLVE = 'ok';
 17042  
 17043    var activeToastContent;
 17044    var $mdToast = $$interimElementProvider('$mdToast')
 17045      .setDefaults({
 17046        methods: ['position', 'hideDelay', 'capsule', 'parent' ],
 17047        options: toastDefaultOptions
 17048      })
 17049      .addPreset('simple', {
 17050        argOption: 'textContent',
 17051        methods: ['textContent', 'content', 'action', 'highlightAction', 'theme', 'parent'],
 17052        options: /* @ngInject */ ["$mdToast", "$mdTheming", function($mdToast, $mdTheming) {
 17053          var opts = {
 17054            template:
 17055              '<md-toast md-theme="{{ toast.theme }}" ng-class="{\'md-capsule\': toast.capsule}">' +
 17056              '  <div class="md-toast-content">' +
 17057              '    <span flex role="alert" aria-relevant="all" aria-atomic="true">' +
 17058              '      {{ toast.content }}' +
 17059              '    </span>' +
 17060              '    <md-button class="md-action" ng-if="toast.action" ng-click="toast.resolve()" ng-class="{\'md-highlight\': toast.highlightAction}">' +
 17061              '      {{ toast.action }}' +
 17062              '    </md-button>' +
 17063              '  </div>' +
 17064              '</md-toast>',
 17065            controller: /* @ngInject */ ["$scope", function mdToastCtrl($scope) {
 17066              var self = this;
 17067              $scope.$watch(function() { return activeToastContent; }, function() {
 17068                self.content = activeToastContent;
 17069              });
 17070              this.resolve = function() {
 17071                $mdToast.hide( ACTION_RESOLVE );
 17072              };
 17073            }],
 17074            theme: $mdTheming.defaultTheme(),
 17075            controllerAs: 'toast',
 17076            bindToController: true
 17077          };
 17078          return opts;
 17079        }]
 17080      })
 17081      .addMethod('updateTextContent', updateTextContent)
 17082      .addMethod('updateContent', updateTextContent);
 17083  
 17084      function updateTextContent(newContent) {
 17085        activeToastContent = newContent;
 17086      }
 17087  
 17088    toastDefaultOptions.$inject = ["$animate", "$mdToast", "$mdUtil", "$mdMedia"];
 17089      return $mdToast;
 17090  
 17091    /* @ngInject */
 17092    function toastDefaultOptions($animate, $mdToast, $mdUtil, $mdMedia) {
 17093      var SWIPE_EVENTS = '$md.swipeleft $md.swiperight $md.swipeup $md.swipedown';
 17094      return {
 17095        onShow: onShow,
 17096        onRemove: onRemove,
 17097        position: 'bottom left',
 17098        themable: true,
 17099        hideDelay: 3000,
 17100        autoWrap: true,
 17101        transformTemplate: function(template, options) {
 17102          var shouldAddWrapper = options.autoWrap && template && !/md-toast-content/g.test(template);
 17103  
 17104          if (shouldAddWrapper) {
 17105            // Root element of template will be <md-toast>. We need to wrap all of its content inside of
 17106            // of <div class="md-toast-content">. All templates provided here should be static, developer-controlled
 17107            // content (meaning we're not attempting to guard against XSS).
 17108            var templateRoot = document.createElement('md-template');
 17109            templateRoot.innerHTML = template;
 17110  
 17111            for (var i = 0; i < templateRoot.children.length; i++) {
 17112              if (templateRoot.children[i].nodeName === 'MD-TOAST') {
 17113                var wrapper = angular.element('<div class="md-toast-content">');
 17114                wrapper.append(templateRoot.children[i].children);
 17115                templateRoot.children[i].appendChild(wrapper[0]);
 17116              }
 17117            }
 17118  
 17119  
 17120            return templateRoot.outerHTML;
 17121          }
 17122  
 17123          return template || '';
 17124        }
 17125      };
 17126  
 17127      function onShow(scope, element, options) {
 17128        activeToastContent = options.textContent || options.content; // support deprecated #content method
 17129  
 17130        var isSmScreen = !$mdMedia('gt-sm');
 17131  
 17132        element = $mdUtil.extractElementByName(element, 'md-toast', true);
 17133        options.element = element;
 17134  
 17135        options.onSwipe = function(ev, gesture) {
 17136          //Add the relevant swipe class to the element so it can animate correctly
 17137          var swipe = ev.type.replace('$md.','');
 17138          var direction = swipe.replace('swipe', '');
 17139  
 17140          // If the swipe direction is down/up but the toast came from top/bottom don't fade away
 17141          // Unless the screen is small, then the toast always on bottom
 17142          if ((direction === 'down' && options.position.indexOf('top') != -1 && !isSmScreen) ||
 17143              (direction === 'up' && (options.position.indexOf('bottom') != -1 || isSmScreen))) {
 17144            return;
 17145          }
 17146  
 17147          if ((direction === 'left' || direction === 'right') && isSmScreen) {
 17148            return;
 17149          }
 17150  
 17151          element.addClass('md-' + swipe);
 17152          $mdUtil.nextTick($mdToast.cancel);
 17153        };
 17154        options.openClass = toastOpenClass(options.position);
 17155  
 17156  
 17157        // 'top left' -> 'md-top md-left'
 17158        options.parent.addClass(options.openClass);
 17159  
 17160        // static is the default position
 17161        if ($mdUtil.hasComputedStyle(options.parent, 'position', 'static')) {
 17162          options.parent.css('position', 'relative');
 17163        }
 17164  
 17165        element.on(SWIPE_EVENTS, options.onSwipe);
 17166        element.addClass(isSmScreen ? 'md-bottom' : options.position.split(' ').map(function(pos) {
 17167          return 'md-' + pos;
 17168        }).join(' '));
 17169  
 17170        if (options.parent) options.parent.addClass('md-toast-animating');
 17171        return $animate.enter(element, options.parent).then(function() {
 17172          if (options.parent) options.parent.removeClass('md-toast-animating');
 17173        });
 17174      }
 17175  
 17176      function onRemove(scope, element, options) {
 17177        element.off(SWIPE_EVENTS, options.onSwipe);
 17178        if (options.parent) options.parent.addClass('md-toast-animating');
 17179        if (options.openClass) options.parent.removeClass(options.openClass);
 17180  
 17181        return ((options.$destroy == true) ? element.remove() : $animate.leave(element))
 17182          .then(function () {
 17183            if (options.parent) options.parent.removeClass('md-toast-animating');
 17184            if ($mdUtil.hasComputedStyle(options.parent, 'position', 'static')) {
 17185              options.parent.css('position', '');
 17186            }
 17187          });
 17188      }
 17189  
 17190      function toastOpenClass(position) {
 17191        if (!$mdMedia('gt-sm')) {
 17192          return 'md-toast-open-bottom';
 17193        }
 17194  
 17195        return 'md-toast-open-' +
 17196          (position.indexOf('top') > -1 ? 'top' : 'bottom');
 17197      }
 17198    }
 17199  
 17200  }
 17201  MdToastProvider.$inject = ["$$interimElementProvider"];
 17202  
 17203  })();
 17204  (function(){
 17205  "use strict";
 17206  
 17207  /**
 17208   * @ngdoc module
 17209   * @name material.components.toolbar
 17210   */
 17211  angular.module('material.components.toolbar', [
 17212    'material.core',
 17213    'material.components.content'
 17214  ])
 17215    .directive('mdToolbar', mdToolbarDirective);
 17216  
 17217  /**
 17218   * @ngdoc directive
 17219   * @name mdToolbar
 17220   * @module material.components.toolbar
 17221   * @restrict E
 17222   * @description
 17223   * `md-toolbar` is used to place a toolbar in your app.
 17224   *
 17225   * Toolbars are usually used above a content area to display the title of the
 17226   * current page, and show relevant action buttons for that page.
 17227   *
 17228   * You can change the height of the toolbar by adding either the
 17229   * `md-medium-tall` or `md-tall` class to the toolbar.
 17230   *
 17231   * @usage
 17232   * <hljs lang="html">
 17233   * <div layout="column" layout-fill>
 17234   *   <md-toolbar>
 17235   *
 17236   *     <div class="md-toolbar-tools">
 17237   *       <span>My App's Title</span>
 17238   *
 17239   *       <!-- fill up the space between left and right area -->
 17240   *       <span flex></span>
 17241   *
 17242   *       <md-button>
 17243   *         Right Bar Button
 17244   *       </md-button>
 17245   *     </div>
 17246   *
 17247   *   </md-toolbar>
 17248   *   <md-content>
 17249   *     Hello!
 17250   *   </md-content>
 17251   * </div>
 17252   * </hljs>
 17253   *
 17254   * @param {boolean=} md-scroll-shrink Whether the header should shrink away as
 17255   * the user scrolls down, and reveal itself as the user scrolls up.
 17256   *
 17257   * _**Note (1):** for scrollShrink to work, the toolbar must be a sibling of a
 17258   * `md-content` element, placed before it. See the scroll shrink demo._
 17259   *
 17260   * _**Note (2):** The `md-scroll-shrink` attribute is only parsed on component
 17261   * initialization, it does not watch for scope changes._
 17262   *
 17263   *
 17264   * @param {number=} md-shrink-speed-factor How much to change the speed of the toolbar's
 17265   * shrinking by. For example, if 0.25 is given then the toolbar will shrink
 17266   * at one fourth the rate at which the user scrolls down. Default 0.5.
 17267   */
 17268  
 17269  function mdToolbarDirective($$rAF, $mdConstant, $mdUtil, $mdTheming, $animate) {
 17270    var translateY = angular.bind(null, $mdUtil.supplant, 'translate3d(0,{0}px,0)');
 17271  
 17272    return {
 17273      template: '',
 17274  
 17275      restrict: 'E',
 17276  
 17277      link: function(scope, element, attr) {
 17278  
 17279        $mdTheming(element);
 17280  
 17281        if (angular.isDefined(attr.mdScrollShrink)) {
 17282          setupScrollShrink();
 17283        }
 17284  
 17285        function setupScrollShrink() {
 17286  
 17287          var toolbarHeight;
 17288          var contentElement;
 17289          var disableScrollShrink = angular.noop;
 17290  
 17291          // Current "y" position of scroll
 17292          // Store the last scroll top position
 17293          var y = 0;
 17294          var prevScrollTop = 0;
 17295          var shrinkSpeedFactor = attr.mdShrinkSpeedFactor || 0.5;
 17296  
 17297          var debouncedContentScroll = $$rAF.throttle(onContentScroll);
 17298          var debouncedUpdateHeight = $mdUtil.debounce(updateToolbarHeight, 5 * 1000);
 17299  
 17300          // Wait for $mdContentLoaded event from mdContent directive.
 17301          // If the mdContent element is a sibling of our toolbar, hook it up
 17302          // to scroll events.
 17303  
 17304          scope.$on('$mdContentLoaded', onMdContentLoad);
 17305  
 17306          // If the toolbar is used inside an ng-if statement, we may miss the
 17307          // $mdContentLoaded event, so we attempt to fake it if we have a
 17308          // md-content close enough.
 17309  
 17310          attr.$observe('mdScrollShrink', onChangeScrollShrink);
 17311  
 17312          // If the toolbar has ngShow or ngHide we need to update height immediately as it changed
 17313          // and not wait for $mdUtil.debounce to happen
 17314  
 17315          if (attr.ngShow) { scope.$watch(attr.ngShow, updateToolbarHeight); }
 17316          if (attr.ngHide) { scope.$watch(attr.ngHide, updateToolbarHeight); }
 17317  
 17318          // If the scope is destroyed (which could happen with ng-if), make sure
 17319          // to disable scroll shrinking again
 17320  
 17321          scope.$on('$destroy', disableScrollShrink);
 17322  
 17323          /**
 17324           *
 17325           */
 17326          function onChangeScrollShrink(shrinkWithScroll) {
 17327            var closestContent = element.parent().find('md-content');
 17328  
 17329            // If we have a content element, fake the call; this might still fail
 17330            // if the content element isn't a sibling of the toolbar
 17331  
 17332            if (!contentElement && closestContent.length) {
 17333              onMdContentLoad(null, closestContent);
 17334            }
 17335  
 17336            // Evaluate the expression
 17337            shrinkWithScroll = scope.$eval(shrinkWithScroll);
 17338  
 17339            // Disable only if the attribute's expression evaluates to false
 17340            if (shrinkWithScroll === false) {
 17341              disableScrollShrink();
 17342            } else {
 17343              disableScrollShrink = enableScrollShrink();
 17344            }
 17345          }
 17346  
 17347          /**
 17348           *
 17349           */
 17350          function onMdContentLoad($event, newContentEl) {
 17351            // Toolbar and content must be siblings
 17352            if (newContentEl && element.parent()[0] === newContentEl.parent()[0]) {
 17353              // unhook old content event listener if exists
 17354              if (contentElement) {
 17355                contentElement.off('scroll', debouncedContentScroll);
 17356              }
 17357  
 17358              contentElement = newContentEl;
 17359              disableScrollShrink = enableScrollShrink();
 17360            }
 17361          }
 17362  
 17363          /**
 17364           *
 17365           */
 17366          function onContentScroll(e) {
 17367            var scrollTop = e ? e.target.scrollTop : prevScrollTop;
 17368  
 17369            debouncedUpdateHeight();
 17370  
 17371            y = Math.min(
 17372              toolbarHeight / shrinkSpeedFactor,
 17373              Math.max(0, y + scrollTop - prevScrollTop)
 17374            );
 17375  
 17376            element.css($mdConstant.CSS.TRANSFORM, translateY([-y * shrinkSpeedFactor]));
 17377            contentElement.css($mdConstant.CSS.TRANSFORM, translateY([(toolbarHeight - y) * shrinkSpeedFactor]));
 17378  
 17379            prevScrollTop = scrollTop;
 17380  
 17381            $mdUtil.nextTick(function() {
 17382              var hasWhiteFrame = element.hasClass('md-whiteframe-z1');
 17383  
 17384              if (hasWhiteFrame && !y) {
 17385                $animate.removeClass(element, 'md-whiteframe-z1');
 17386              } else if (!hasWhiteFrame && y) {
 17387                $animate.addClass(element, 'md-whiteframe-z1');
 17388              }
 17389            });
 17390  
 17391          }
 17392  
 17393          /**
 17394           *
 17395           */
 17396          function enableScrollShrink() {
 17397            if (!contentElement)     return angular.noop;           // no md-content
 17398  
 17399            contentElement.on('scroll', debouncedContentScroll);
 17400            contentElement.attr('scroll-shrink', 'true');
 17401  
 17402            $$rAF(updateToolbarHeight);
 17403  
 17404            return function disableScrollShrink() {
 17405              contentElement.off('scroll', debouncedContentScroll);
 17406              contentElement.attr('scroll-shrink', 'false');
 17407  
 17408              $$rAF(updateToolbarHeight);
 17409            }
 17410          }
 17411  
 17412          /**
 17413           *
 17414           */
 17415          function updateToolbarHeight() {
 17416            toolbarHeight = element.prop('offsetHeight');
 17417            // Add a negative margin-top the size of the toolbar to the content el.
 17418            // The content will start transformed down the toolbarHeight amount,
 17419            // so everything looks normal.
 17420            //
 17421            // As the user scrolls down, the content will be transformed up slowly
 17422            // to put the content underneath where the toolbar was.
 17423            var margin = (-toolbarHeight * shrinkSpeedFactor) + 'px';
 17424  
 17425            contentElement.css({
 17426              "margin-top": margin,
 17427              "margin-bottom": margin
 17428            });
 17429  
 17430            onContentScroll();
 17431          }
 17432  
 17433        }
 17434  
 17435      }
 17436    };
 17437  
 17438  }
 17439  mdToolbarDirective.$inject = ["$$rAF", "$mdConstant", "$mdUtil", "$mdTheming", "$animate"];
 17440  
 17441  })();
 17442  (function(){
 17443  "use strict";
 17444  
 17445  /**
 17446   * @ngdoc module
 17447   * @name material.components.tooltip
 17448   */
 17449  angular
 17450      .module('material.components.tooltip', [ 'material.core' ])
 17451      .directive('mdTooltip', MdTooltipDirective);
 17452  
 17453  /**
 17454   * @ngdoc directive
 17455   * @name mdTooltip
 17456   * @module material.components.tooltip
 17457   * @description
 17458   * Tooltips are used to describe elements that are interactive and primarily graphical (not textual).
 17459   *
 17460   * Place a `<md-tooltip>` as a child of the element it describes.
 17461   *
 17462   * A tooltip will activate when the user focuses, hovers over, or touches the parent.
 17463   *
 17464   * @usage
 17465   * <hljs lang="html">
 17466   * <md-button class="md-fab md-accent" aria-label="Play">
 17467   *   <md-tooltip>
 17468   *     Play Music
 17469   *   </md-tooltip>
 17470   *   <md-icon icon="img/icons/ic_play_arrow_24px.svg"></md-icon>
 17471   * </md-button>
 17472   * </hljs>
 17473   *
 17474   * @param {expression=} md-visible Boolean bound to whether the tooltip is currently visible.
 17475   * @param {number=} md-delay How many milliseconds to wait to show the tooltip after the user focuses, hovers, or touches the parent. Defaults to 300ms.
 17476   * @param {boolean=} md-autohide If present or provided with a boolean value, the tooltip will hide on mouse leave, regardless of focus
 17477   * @param {string=} md-direction Which direction would you like the tooltip to go?  Supports left, right, top, and bottom.  Defaults to bottom.
 17478   */
 17479  function MdTooltipDirective($timeout, $window, $$rAF, $document, $mdUtil, $mdTheming, $rootElement,
 17480                              $animate, $q) {
 17481  
 17482    var TOOLTIP_SHOW_DELAY = 0;
 17483    var TOOLTIP_WINDOW_EDGE_SPACE = 8;
 17484  
 17485    return {
 17486      restrict: 'E',
 17487      transclude: true,
 17488      priority:210, // Before ngAria
 17489      template: '<div class="md-content" ng-transclude></div>',
 17490      scope: {
 17491        delay: '=?mdDelay',
 17492        visible: '=?mdVisible',
 17493        autohide: '=?mdAutohide',
 17494        direction: '@?mdDirection'    // only expect raw or interpolated string value; not expression
 17495      },
 17496      link: postLink
 17497    };
 17498  
 17499    function postLink(scope, element, attr) {
 17500  
 17501      $mdTheming(element);
 17502  
 17503      var parent        = $mdUtil.getParentWithPointerEvents(element),
 17504          content       = angular.element(element[0].getElementsByClassName('md-content')[0]),
 17505          tooltipParent = angular.element(document.body),
 17506          debouncedOnResize = $$rAF.throttle(function () { updatePosition(); });
 17507  
 17508      if ($animate.pin) $animate.pin(element, parent);
 17509  
 17510      // Initialize element
 17511  
 17512      setDefaults();
 17513      manipulateElement();
 17514      bindEvents();
 17515  
 17516      // Default origin transform point is 'center top'
 17517      // positionTooltip() is always relative to center top
 17518      updateContentOrigin();
 17519  
 17520      configureWatchers();
 17521      addAriaLabel();
 17522  
 17523  
 17524      function setDefaults () {
 17525        if (!angular.isDefined(attr.mdDelay)) scope.delay = TOOLTIP_SHOW_DELAY;
 17526      }
 17527  
 17528      function updateContentOrigin() {
 17529        var origin = 'center top';
 17530        switch (scope.direction) {
 17531          case 'left'  : origin =  'right center';  break;
 17532          case 'right' : origin =  'left center';   break;
 17533          case 'top'   : origin =  'center bottom'; break;
 17534          case 'bottom': origin =  'center top';    break;
 17535        }
 17536        content.css('transform-origin', origin);
 17537      }
 17538  
 17539      function configureWatchers () {
 17540        scope.$on('$destroy', function() {
 17541          scope.visible = false;
 17542          element.remove();
 17543          angular.element($window).off('resize', debouncedOnResize);
 17544        });
 17545  
 17546        scope.$watch('visible', function (isVisible) {
 17547          if (isVisible) showTooltip();
 17548          else hideTooltip();
 17549        });
 17550  
 17551        scope.$watch('direction', updatePosition );
 17552      }
 17553  
 17554      function addAriaLabel () {
 17555        if (!parent.attr('aria-label') && !parent.text().trim()) {
 17556          parent.attr('aria-label', element.text().trim());
 17557        }
 17558      }
 17559  
 17560      function manipulateElement () {
 17561        element.detach();
 17562        element.attr('role', 'tooltip');
 17563      }
 17564  
 17565      function bindEvents () {
 17566        var mouseActive = false;
 17567  
 17568        var ngWindow = angular.element($window);
 17569  
 17570        // add an mutationObserver when there is support for it
 17571        // and the need for it in the form of viable host(parent[0])
 17572        if (parent[0] && 'MutationObserver' in $window) {
 17573          // use an mutationObserver to tackle #2602
 17574          var attributeObserver = new MutationObserver(function(mutations) {
 17575            mutations
 17576              .forEach(function (mutation) {
 17577                if (mutation.attributeName === 'disabled' && parent[0].disabled) {
 17578                  setVisible(false);
 17579                  scope.$digest(); // make sure the elements gets updated
 17580                }
 17581              });
 17582          });
 17583  
 17584          attributeObserver.observe(parent[0], { attributes: true});
 17585        }
 17586  
 17587        // Store whether the element was focused when the window loses focus.
 17588        var windowBlurHandler = function() {
 17589          elementFocusedOnWindowBlur = document.activeElement === parent[0];
 17590        };
 17591        var elementFocusedOnWindowBlur = false;
 17592  
 17593        function windowScrollHandler() {
 17594          setVisible(false);
 17595        }
 17596        
 17597        ngWindow.on('blur', windowBlurHandler);
 17598        ngWindow.on('resize', debouncedOnResize);
 17599        document.addEventListener('scroll', windowScrollHandler, true);
 17600        scope.$on('$destroy', function() {
 17601          ngWindow.off('blur', windowBlurHandler);
 17602          ngWindow.off('resize', debouncedOnResize);
 17603          document.removeEventListener('scroll', windowScrollHandler, true);
 17604          attributeObserver && attributeObserver.disconnect();
 17605        });
 17606  
 17607        var enterHandler = function(e) {
 17608          // Prevent the tooltip from showing when the window is receiving focus.
 17609          if (e.type === 'focus' && elementFocusedOnWindowBlur) {
 17610            elementFocusedOnWindowBlur = false;
 17611            return;
 17612          }
 17613          parent.on('blur mouseleave touchend touchcancel', leaveHandler );
 17614          setVisible(true);
 17615        };
 17616        var leaveHandler = function () {
 17617          var autohide = scope.hasOwnProperty('autohide') ? scope.autohide : attr.hasOwnProperty('mdAutohide');
 17618          if (autohide || mouseActive || ($document[0].activeElement !== parent[0]) ) {
 17619            parent.off('blur mouseleave touchend touchcancel', leaveHandler );
 17620            parent.triggerHandler("blur");
 17621            setVisible(false);
 17622          }
 17623          mouseActive = false;
 17624        };
 17625  
 17626        // to avoid `synthetic clicks` we listen to mousedown instead of `click`
 17627        parent.on('mousedown', function() { mouseActive = true; });
 17628        parent.on('focus mouseenter touchstart', enterHandler );
 17629  
 17630  
 17631      }
 17632  
 17633      function setVisible (value) {
 17634        setVisible.value = !!value;
 17635        if (!setVisible.queued) {
 17636          if (value) {
 17637            setVisible.queued = true;
 17638            $timeout(function() {
 17639              scope.visible = setVisible.value;
 17640              setVisible.queued = false;
 17641            }, scope.delay);
 17642          } else {
 17643            $mdUtil.nextTick(function() { scope.visible = false; });
 17644          }
 17645        }
 17646      }
 17647  
 17648      function showTooltip() {
 17649        // Insert the element before positioning it, so we can get the position
 17650        // and check if we should display it
 17651        tooltipParent.append(element);
 17652  
 17653        // Check if we should display it or not.
 17654        // This handles hide-* and show-* along with any user defined css
 17655        if ( $mdUtil.hasComputedStyle(element, 'display', 'none')) {
 17656          scope.visible = false;
 17657          element.detach();
 17658          return;
 17659        }
 17660  
 17661        updatePosition();
 17662  
 17663        angular.forEach([element, content], function (element) {
 17664          $animate.addClass(element, 'md-show');
 17665        });
 17666      }
 17667  
 17668      function hideTooltip() {
 17669          var promises = [];
 17670          angular.forEach([element, content], function (it) {
 17671            if (it.parent() && it.hasClass('md-show')) {
 17672              promises.push($animate.removeClass(it, 'md-show'));
 17673            }
 17674          });
 17675  
 17676          $q.all(promises)
 17677            .then(function () {
 17678              if (!scope.visible) element.detach();
 17679            });
 17680      }
 17681  
 17682      function updatePosition() {
 17683        if ( !scope.visible ) return;
 17684  
 17685        updateContentOrigin();
 17686        positionTooltip();
 17687      }
 17688  
 17689      function positionTooltip() {
 17690        var tipRect = $mdUtil.offsetRect(element, tooltipParent);
 17691        var parentRect = $mdUtil.offsetRect(parent, tooltipParent);
 17692        var newPosition = getPosition(scope.direction);
 17693        var offsetParent = element.prop('offsetParent');
 17694  
 17695        // If the user provided a direction, just nudge the tooltip onto the screen
 17696        // Otherwise, recalculate based on 'top' since default is 'bottom'
 17697        if (scope.direction) {
 17698          newPosition = fitInParent(newPosition);
 17699        } else if (offsetParent && newPosition.top > offsetParent.scrollHeight - tipRect.height - TOOLTIP_WINDOW_EDGE_SPACE) {
 17700          newPosition = fitInParent(getPosition('top'));
 17701        }
 17702  
 17703        element.css({
 17704          left: newPosition.left + 'px',
 17705          top: newPosition.top + 'px'
 17706        });
 17707  
 17708        function fitInParent (pos) {
 17709          var newPosition = { left: pos.left, top: pos.top };
 17710          newPosition.left = Math.min( newPosition.left, tooltipParent.prop('scrollWidth') - tipRect.width - TOOLTIP_WINDOW_EDGE_SPACE );
 17711          newPosition.left = Math.max( newPosition.left, TOOLTIP_WINDOW_EDGE_SPACE );
 17712          newPosition.top  = Math.min( newPosition.top,  tooltipParent.prop('scrollHeight') - tipRect.height - TOOLTIP_WINDOW_EDGE_SPACE );
 17713          newPosition.top  = Math.max( newPosition.top,  TOOLTIP_WINDOW_EDGE_SPACE );
 17714          return newPosition;
 17715        }
 17716  
 17717        function getPosition (dir) {
 17718          return dir === 'left'
 17719            ? { left: parentRect.left - tipRect.width - TOOLTIP_WINDOW_EDGE_SPACE,
 17720                top: parentRect.top + parentRect.height / 2 - tipRect.height / 2 }
 17721            : dir === 'right'
 17722            ? { left: parentRect.left + parentRect.width + TOOLTIP_WINDOW_EDGE_SPACE,
 17723                top: parentRect.top + parentRect.height / 2 - tipRect.height / 2 }
 17724            : dir === 'top'
 17725            ? { left: parentRect.left + parentRect.width / 2 - tipRect.width / 2,
 17726                top: parentRect.top - tipRect.height - TOOLTIP_WINDOW_EDGE_SPACE }
 17727            : { left: parentRect.left + parentRect.width / 2 - tipRect.width / 2,
 17728                top: parentRect.top + parentRect.height + TOOLTIP_WINDOW_EDGE_SPACE };
 17729        }
 17730      }
 17731  
 17732    }
 17733  
 17734  }
 17735  MdTooltipDirective.$inject = ["$timeout", "$window", "$$rAF", "$document", "$mdUtil", "$mdTheming", "$rootElement", "$animate", "$q"];
 17736  
 17737  })();
 17738  (function(){
 17739  "use strict";
 17740  
 17741  /**
 17742   * @ngdoc module
 17743   * @name material.components.virtualRepeat
 17744   */
 17745  angular.module('material.components.virtualRepeat', [
 17746    'material.core',
 17747    'material.components.showHide'
 17748  ])
 17749  .directive('mdVirtualRepeatContainer', VirtualRepeatContainerDirective)
 17750  .directive('mdVirtualRepeat', VirtualRepeatDirective);
 17751  
 17752  
 17753  /**
 17754   * @ngdoc directive
 17755   * @name mdVirtualRepeatContainer
 17756   * @module material.components.virtualRepeat
 17757   * @restrict E
 17758   * @description
 17759   * `md-virtual-repeat-container` provides the scroll container for md-virtual-repeat.
 17760   *
 17761   * Virtual repeat is a limited substitute for ng-repeat that renders only
 17762   * enough dom nodes to fill the container and recycling them as the user scrolls.
 17763   *
 17764   * @usage
 17765   * <hljs lang="html">
 17766   *
 17767   * <md-virtual-repeat-container md-top-index="topIndex">
 17768   *   <div md-virtual-repeat="i in items" md-item-size="20">Hello {{i}}!</div>
 17769   * </md-virtual-repeat-container>
 17770   * </hljs>
 17771   *
 17772   * @param {number=} md-top-index Binds the index of the item that is at the top of the scroll
 17773   *     container to $scope. It can both read and set the scroll position.
 17774   * @param {boolean=} md-orient-horizontal Whether the container should scroll horizontally
 17775   *     (defaults to orientation and scrolling vertically).
 17776   * @param {boolean=} md-auto-shrink When present, the container will shrink to fit
 17777   *     the number of items when that number is less than its original size.
 17778   * @param {number=} md-auto-shrink-min Minimum number of items that md-auto-shrink
 17779   *     will shrink to (default: 0).
 17780   */
 17781  function VirtualRepeatContainerDirective() {
 17782    return {
 17783      controller: VirtualRepeatContainerController,
 17784      template: virtualRepeatContainerTemplate,
 17785      compile: function virtualRepeatContainerCompile($element, $attrs) {
 17786        $element
 17787            .addClass('md-virtual-repeat-container')
 17788            .addClass($attrs.hasOwnProperty('mdOrientHorizontal')
 17789                ? 'md-orient-horizontal'
 17790                : 'md-orient-vertical');
 17791      }
 17792    };
 17793  }
 17794  
 17795  
 17796  function virtualRepeatContainerTemplate($element) {
 17797    return '<div class="md-virtual-repeat-scroller">' +
 17798      '<div class="md-virtual-repeat-sizer"></div>' +
 17799      '<div class="md-virtual-repeat-offsetter">' +
 17800        $element[0].innerHTML +
 17801      '</div></div>';
 17802  }
 17803  
 17804  /**
 17805   * Maximum size, in pixels, that can be explicitly set to an element. The actual value varies
 17806   * between browsers, but IE11 has the very lowest size at a mere 1,533,917px. Ideally we could
 17807   * *compute* this value, but Firefox always reports an element to have a size of zero if it
 17808   * goes over the max, meaning that we'd have to binary search for the value.
 17809   * @const {number}
 17810   */
 17811  var MAX_ELEMENT_SIZE = 1533917;
 17812  
 17813  /**
 17814   * Number of additional elements to render above and below the visible area inside
 17815   * of the virtual repeat container. A higher number results in less flicker when scrolling
 17816   * very quickly in Safari, but comes with a higher rendering and dirty-checking cost.
 17817   * @const {number}
 17818   */
 17819  var NUM_EXTRA = 3;
 17820  
 17821  /** @ngInject */
 17822  function VirtualRepeatContainerController(
 17823      $$rAF, $mdUtil, $parse, $rootScope, $window, $scope, $element, $attrs) {
 17824    this.$rootScope = $rootScope;
 17825    this.$scope = $scope;
 17826    this.$element = $element;
 17827    this.$attrs = $attrs;
 17828  
 17829    /** @type {number} The width or height of the container */
 17830    this.size = 0;
 17831    /** @type {number} The scroll width or height of the scroller */
 17832    this.scrollSize = 0;
 17833    /** @type {number} The scrollLeft or scrollTop of the scroller */
 17834    this.scrollOffset = 0;
 17835    /** @type {boolean} Whether the scroller is oriented horizontally */
 17836    this.horizontal = this.$attrs.hasOwnProperty('mdOrientHorizontal');
 17837    /** @type {!VirtualRepeatController} The repeater inside of this container */
 17838    this.repeater = null;
 17839    /** @type {boolean} Whether auto-shrink is enabled */
 17840    this.autoShrink = this.$attrs.hasOwnProperty('mdAutoShrink');
 17841    /** @type {number} Minimum number of items to auto-shrink to */
 17842    this.autoShrinkMin = parseInt(this.$attrs.mdAutoShrinkMin, 10) || 0;
 17843    /** @type {?number} Original container size when shrank */
 17844    this.originalSize = null;
 17845    /** @type {number} Amount to offset the total scroll size by. */
 17846    this.offsetSize = parseInt(this.$attrs.mdOffsetSize, 10) || 0;
 17847    /** @type {?string} height or width element style on the container prior to auto-shrinking. */
 17848    this.oldElementSize = null;
 17849  
 17850    if (this.$attrs.mdTopIndex) {
 17851      /** @type {function(angular.Scope): number} Binds to topIndex on Angular scope */
 17852      this.bindTopIndex = $parse(this.$attrs.mdTopIndex);
 17853      /** @type {number} The index of the item that is at the top of the scroll container */
 17854      this.topIndex = this.bindTopIndex(this.$scope);
 17855  
 17856      if (!angular.isDefined(this.topIndex)) {
 17857        this.topIndex = 0;
 17858        this.bindTopIndex.assign(this.$scope, 0);
 17859      }
 17860  
 17861      this.$scope.$watch(this.bindTopIndex, angular.bind(this, function(newIndex) {
 17862        if (newIndex !== this.topIndex) {
 17863          this.scrollToIndex(newIndex);
 17864        }
 17865      }));
 17866    } else {
 17867      this.topIndex = 0;
 17868    }
 17869  
 17870    this.scroller = $element[0].getElementsByClassName('md-virtual-repeat-scroller')[0];
 17871    this.sizer = this.scroller.getElementsByClassName('md-virtual-repeat-sizer')[0];
 17872    this.offsetter = this.scroller.getElementsByClassName('md-virtual-repeat-offsetter')[0];
 17873  
 17874    // After the dom stablizes, measure the initial size of the container and
 17875    // make a best effort at re-measuring as it changes.
 17876    var boundUpdateSize = angular.bind(this, this.updateSize);
 17877  
 17878    $$rAF(angular.bind(this, function() {
 17879      boundUpdateSize();
 17880  
 17881      var debouncedUpdateSize = $mdUtil.debounce(boundUpdateSize, 10, null, false);
 17882      var jWindow = angular.element($window);
 17883  
 17884      // Make one more attempt to get the size if it is 0.
 17885      // This is not by any means a perfect approach, but there's really no
 17886      // silver bullet here.
 17887      if (!this.size) {
 17888        debouncedUpdateSize();
 17889      }
 17890  
 17891      jWindow.on('resize', debouncedUpdateSize);
 17892      $scope.$on('$destroy', function() {
 17893        jWindow.off('resize', debouncedUpdateSize);
 17894      });
 17895  
 17896      $scope.$emit('$md-resize-enable');
 17897      $scope.$on('$md-resize', boundUpdateSize);
 17898    }));
 17899  }
 17900  VirtualRepeatContainerController.$inject = ["$$rAF", "$mdUtil", "$parse", "$rootScope", "$window", "$scope", "$element", "$attrs"];
 17901  
 17902  
 17903  /** Called by the md-virtual-repeat inside of the container at startup. */
 17904  VirtualRepeatContainerController.prototype.register = function(repeaterCtrl) {
 17905    this.repeater = repeaterCtrl;
 17906  
 17907    angular.element(this.scroller)
 17908        .on('scroll wheel touchmove touchend', angular.bind(this, this.handleScroll_));
 17909  };
 17910  
 17911  
 17912  /** @return {boolean} Whether the container is configured for horizontal scrolling. */
 17913  VirtualRepeatContainerController.prototype.isHorizontal = function() {
 17914    return this.horizontal;
 17915  };
 17916  
 17917  
 17918  /** @return {number} The size (width or height) of the container. */
 17919  VirtualRepeatContainerController.prototype.getSize = function() {
 17920    return this.size;
 17921  };
 17922  
 17923  
 17924  /**
 17925   * Resizes the container.
 17926   * @private
 17927   * @param {number} The new size to set.
 17928   */
 17929  VirtualRepeatContainerController.prototype.setSize_ = function(size) {
 17930    var dimension = this.getDimensionName_();
 17931  
 17932    this.size = size;
 17933    this.$element[0].style[dimension] = size + 'px';
 17934  };
 17935  
 17936  
 17937  VirtualRepeatContainerController.prototype.unsetSize_ = function() {
 17938    this.$element[0].style[this.getDimensionName_()] = this.oldElementSize;
 17939    this.oldElementSize = null;
 17940  };
 17941  
 17942  
 17943  /** Instructs the container to re-measure its size. */
 17944  VirtualRepeatContainerController.prototype.updateSize = function() {
 17945    if (this.originalSize) return;
 17946  
 17947    this.size = this.isHorizontal()
 17948        ? this.$element[0].clientWidth
 17949        : this.$element[0].clientHeight;
 17950  
 17951    // Recheck the scroll position after updating the size. This resolves
 17952    // problems that can result if the scroll position was measured while the
 17953    // element was display: none or detached from the document.
 17954    this.handleScroll_();
 17955  
 17956    this.repeater && this.repeater.containerUpdated();
 17957  };
 17958  
 17959  
 17960  /** @return {number} The container's scrollHeight or scrollWidth. */
 17961  VirtualRepeatContainerController.prototype.getScrollSize = function() {
 17962    return this.scrollSize;
 17963  };
 17964  
 17965  
 17966  VirtualRepeatContainerController.prototype.getDimensionName_ = function() {
 17967    return this.isHorizontal() ? 'width' : 'height';
 17968  };
 17969  
 17970  
 17971  /**
 17972   * Sets the scroller element to the specified size.
 17973   * @private
 17974   * @param {number} size The new size.
 17975   */
 17976  VirtualRepeatContainerController.prototype.sizeScroller_ = function(size) {
 17977    var dimension =  this.getDimensionName_();
 17978    var crossDimension = this.isHorizontal() ? 'height' : 'width';
 17979  
 17980    // Clear any existing dimensions.
 17981    this.sizer.innerHTML = '';
 17982  
 17983    // If the size falls within the browser's maximum explicit size for a single element, we can
 17984    // set the size and be done. Otherwise, we have to create children that add up the the desired
 17985    // size.
 17986    if (size < MAX_ELEMENT_SIZE) {
 17987      this.sizer.style[dimension] = size + 'px';
 17988    } else {
 17989      this.sizer.style[dimension] = 'auto';
 17990      this.sizer.style[crossDimension] = 'auto';
 17991  
 17992      // Divide the total size we have to render into N max-size pieces.
 17993      var numChildren = Math.floor(size / MAX_ELEMENT_SIZE);
 17994  
 17995      // Element template to clone for each max-size piece.
 17996      var sizerChild = document.createElement('div');
 17997      sizerChild.style[dimension] = MAX_ELEMENT_SIZE + 'px';
 17998      sizerChild.style[crossDimension] = '1px';
 17999  
 18000      for (var i = 0; i < numChildren; i++) {
 18001        this.sizer.appendChild(sizerChild.cloneNode(false));
 18002      }
 18003  
 18004      // Re-use the element template for the remainder.
 18005      sizerChild.style[dimension] = (size - (numChildren * MAX_ELEMENT_SIZE)) + 'px';
 18006      this.sizer.appendChild(sizerChild);
 18007    }
 18008  };
 18009  
 18010  
 18011  /**
 18012   * If auto-shrinking is enabled, shrinks or unshrinks as appropriate.
 18013   * @private
 18014   * @param {number} size The new size.
 18015   */
 18016  VirtualRepeatContainerController.prototype.autoShrink_ = function(size) {
 18017    var shrinkSize = Math.max(size, this.autoShrinkMin * this.repeater.getItemSize());
 18018    if (this.autoShrink && shrinkSize !== this.size) {
 18019      if (this.oldElementSize === null) {
 18020        this.oldElementSize = this.$element[0].style[this.getDimensionName_()];
 18021      }
 18022  
 18023      var currentSize = this.originalSize || this.size;
 18024      if (!currentSize || shrinkSize < currentSize) {
 18025        if (!this.originalSize) {
 18026          this.originalSize = this.size;
 18027        }
 18028  
 18029        this.setSize_(shrinkSize);
 18030      } else if (this.originalSize !== null) {
 18031        this.unsetSize_();
 18032        this.originalSize = null;
 18033        this.updateSize();
 18034      }
 18035  
 18036      this.repeater.containerUpdated();
 18037    }
 18038  };
 18039  
 18040  
 18041  /**
 18042   * Sets the scrollHeight or scrollWidth. Called by the repeater based on
 18043   * its item count and item size.
 18044   * @param {number} itemsSize The total size of the items.
 18045   */
 18046  VirtualRepeatContainerController.prototype.setScrollSize = function(itemsSize) {
 18047    var size = itemsSize + this.offsetSize;
 18048    if (this.scrollSize === size) return;
 18049  
 18050    this.sizeScroller_(size);
 18051    this.autoShrink_(size);
 18052    this.scrollSize = size;
 18053  };
 18054  
 18055  
 18056  /** @return {number} The container's current scroll offset. */
 18057  VirtualRepeatContainerController.prototype.getScrollOffset = function() {
 18058    return this.scrollOffset;
 18059  };
 18060  
 18061  /**
 18062   * Scrolls to a given scrollTop position.
 18063   * @param {number} position
 18064   */
 18065  VirtualRepeatContainerController.prototype.scrollTo = function(position) {
 18066    this.scroller[this.isHorizontal() ? 'scrollLeft' : 'scrollTop'] = position;
 18067    this.handleScroll_();
 18068  };
 18069  
 18070  /**
 18071   * Scrolls the item with the given index to the top of the scroll container.
 18072   * @param {number} index
 18073   */
 18074  VirtualRepeatContainerController.prototype.scrollToIndex = function(index) {
 18075    var itemSize = this.repeater.getItemSize();
 18076    var itemsLength = this.repeater.itemsLength;
 18077    if(index > itemsLength) {
 18078      index = itemsLength - 1;
 18079    }
 18080    this.scrollTo(itemSize * index);
 18081  };
 18082  
 18083  VirtualRepeatContainerController.prototype.resetScroll = function() {
 18084    this.scrollTo(0);
 18085  };
 18086  
 18087  
 18088  VirtualRepeatContainerController.prototype.handleScroll_ = function() {
 18089    var offset = this.isHorizontal() ? this.scroller.scrollLeft : this.scroller.scrollTop;
 18090    if (offset === this.scrollOffset || offset > this.scrollSize - this.size) return;
 18091  
 18092    var itemSize = this.repeater.getItemSize();
 18093    if (!itemSize) return;
 18094  
 18095    var numItems = Math.max(0, Math.floor(offset / itemSize) - NUM_EXTRA);
 18096  
 18097    var transform = (this.isHorizontal() ? 'translateX(' : 'translateY(') +
 18098                    (numItems * itemSize) + 'px)';
 18099  
 18100    this.scrollOffset = offset;
 18101    this.offsetter.style.webkitTransform = transform;
 18102    this.offsetter.style.transform = transform;
 18103  
 18104    if (this.bindTopIndex) {
 18105      var topIndex = Math.floor(offset / itemSize);
 18106      if (topIndex !== this.topIndex && topIndex < this.repeater.getItemCount()) {
 18107        this.topIndex = topIndex;
 18108        this.bindTopIndex.assign(this.$scope, topIndex);
 18109        if (!this.$rootScope.$$phase) this.$scope.$digest();
 18110      }
 18111    }
 18112  
 18113    this.repeater.containerUpdated();
 18114  };
 18115  
 18116  
 18117  /**
 18118   * @ngdoc directive
 18119   * @name mdVirtualRepeat
 18120   * @module material.components.virtualRepeat
 18121   * @restrict A
 18122   * @priority 1000
 18123   * @description
 18124   * `md-virtual-repeat` specifies an element to repeat using virtual scrolling.
 18125   *
 18126   * Virtual repeat is a limited substitute for ng-repeat that renders only
 18127   * enough dom nodes to fill the container and recycling them as the user scrolls.
 18128   * Arrays, but not objects are supported for iteration.
 18129   * Track by, as alias, and (key, value) syntax are not supported.
 18130   *
 18131   * @usage
 18132   * <hljs lang="html">
 18133   * <md-virtual-repeat-container>
 18134   *   <div md-virtual-repeat="i in items">Hello {{i}}!</div>
 18135   * </md-virtual-repeat-container>
 18136   *
 18137   * <md-virtual-repeat-container md-orient-horizontal>
 18138   *   <div md-virtual-repeat="i in items" md-item-size="20">Hello {{i}}!</div>
 18139   * </md-virtual-repeat-container>
 18140   * </hljs>
 18141   *
 18142   * @param {number=} md-item-size The height or width of the repeated elements (which must be
 18143   *   identical for each element). Optional. Will attempt to read the size from the dom if missing,
 18144   *   but still assumes that all repeated nodes have same height or width.
 18145   * @param {string=} md-extra-name Evaluates to an additional name to which the current iterated item
 18146   *   can be assigned on the repeated scope (needed for use in `md-autocomplete`).
 18147   * @param {boolean=} md-on-demand When present, treats the md-virtual-repeat argument as an object
 18148   *   that can fetch rows rather than an array.
 18149   *
 18150   *   **NOTE:** This object must implement the following interface with two (2) methods:
 18151   *
 18152   *   - `getItemAtIndex: function(index) [object]` The item at that index or null if it is not yet
 18153   *     loaded (it should start downloading the item in that case).
 18154   *   - `getLength: function() [number]` The data length to which the repeater container
 18155   *     should be sized. Ideally, when the count is known, this method should return it.
 18156   *     Otherwise, return a higher number than the currently loaded items to produce an
 18157   *     infinite-scroll behavior.
 18158   */
 18159  function VirtualRepeatDirective($parse) {
 18160    return {
 18161      controller: VirtualRepeatController,
 18162      priority: 1000,
 18163      require: ['mdVirtualRepeat', '^^mdVirtualRepeatContainer'],
 18164      restrict: 'A',
 18165      terminal: true,
 18166      transclude: 'element',
 18167      compile: function VirtualRepeatCompile($element, $attrs) {
 18168        var expression = $attrs.mdVirtualRepeat;
 18169        var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)\s*$/);
 18170        var repeatName = match[1];
 18171        var repeatListExpression = $parse(match[2]);
 18172        var extraName = $attrs.mdExtraName && $parse($attrs.mdExtraName);
 18173  
 18174        return function VirtualRepeatLink($scope, $element, $attrs, ctrl, $transclude) {
 18175          ctrl[0].link_(ctrl[1], $transclude, repeatName, repeatListExpression, extraName);
 18176        };
 18177      }
 18178    };
 18179  }
 18180  VirtualRepeatDirective.$inject = ["$parse"];
 18181  
 18182  
 18183  /** @ngInject */
 18184  function VirtualRepeatController($scope, $element, $attrs, $browser, $document, $rootScope,
 18185      $$rAF) {
 18186    this.$scope = $scope;
 18187    this.$element = $element;
 18188    this.$attrs = $attrs;
 18189    this.$browser = $browser;
 18190    this.$document = $document;
 18191    this.$rootScope = $rootScope;
 18192    this.$$rAF = $$rAF;
 18193  
 18194    /** @type {boolean} Whether we are in on-demand mode. */
 18195    this.onDemand = $attrs.hasOwnProperty('mdOnDemand');
 18196    /** @type {!Function} Backup reference to $browser.$$checkUrlChange */
 18197    this.browserCheckUrlChange = $browser.$$checkUrlChange;
 18198    /** @type {number} Most recent starting repeat index (based on scroll offset) */
 18199    this.newStartIndex = 0;
 18200    /** @type {number} Most recent ending repeat index (based on scroll offset) */
 18201    this.newEndIndex = 0;
 18202    /** @type {number} Most recent end visible index (based on scroll offset) */
 18203    this.newVisibleEnd = 0;
 18204    /** @type {number} Previous starting repeat index (based on scroll offset) */
 18205    this.startIndex = 0;
 18206    /** @type {number} Previous ending repeat index (based on scroll offset) */
 18207    this.endIndex = 0;
 18208    // TODO: measure width/height of first element from dom if not provided.
 18209    // getComputedStyle?
 18210    /** @type {?number} Height/width of repeated elements. */
 18211    this.itemSize = $scope.$eval($attrs.mdItemSize) || null;
 18212  
 18213    /** @type {boolean} Whether this is the first time that items are rendered. */
 18214    this.isFirstRender = true;
 18215  
 18216    /**
 18217     * @private {boolean} Whether the items in the list are already being updated. Used to prevent
 18218     *     nested calls to virtualRepeatUpdate_.
 18219     */
 18220    this.isVirtualRepeatUpdating_ = false;
 18221  
 18222    /** @type {number} Most recently seen length of items. */
 18223    this.itemsLength = 0;
 18224  
 18225    /**
 18226     * @type {!Function} Unwatch callback for item size (when md-items-size is
 18227     *     not specified), or angular.noop otherwise.
 18228     */
 18229    this.unwatchItemSize_ = angular.noop;
 18230  
 18231    /**
 18232     * Presently rendered blocks by repeat index.
 18233     * @type {Object<number, !VirtualRepeatController.Block}
 18234     */
 18235    this.blocks = {};
 18236    /** @type {Array<!VirtualRepeatController.Block>} A pool of presently unused blocks. */
 18237    this.pooledBlocks = [];
 18238  }
 18239  VirtualRepeatController.$inject = ["$scope", "$element", "$attrs", "$browser", "$document", "$rootScope", "$$rAF"];
 18240  
 18241  
 18242  /**
 18243   * An object representing a repeated item.
 18244   * @typedef {{element: !jqLite, new: boolean, scope: !angular.Scope}}
 18245   */
 18246  VirtualRepeatController.Block;
 18247  
 18248  
 18249  /**
 18250   * Called at startup by the md-virtual-repeat postLink function.
 18251   * @param {!VirtualRepeatContainerController} container The container's controller.
 18252   * @param {!Function} transclude The repeated element's bound transclude function.
 18253   * @param {string} repeatName The left hand side of the repeat expression, indicating
 18254   *     the name for each item in the array.
 18255   * @param {!Function} repeatListExpression A compiled expression based on the right hand side
 18256   *     of the repeat expression. Points to the array to repeat over.
 18257   * @param {string|undefined} extraName The optional extra repeatName.
 18258   */
 18259  VirtualRepeatController.prototype.link_ =
 18260      function(container, transclude, repeatName, repeatListExpression, extraName) {
 18261    this.container = container;
 18262    this.transclude = transclude;
 18263    this.repeatName = repeatName;
 18264    this.rawRepeatListExpression = repeatListExpression;
 18265    this.extraName = extraName;
 18266    this.sized = false;
 18267  
 18268    this.repeatListExpression = angular.bind(this, this.repeatListExpression_);
 18269  
 18270    this.container.register(this);
 18271  };
 18272  
 18273  
 18274  /** @private Attempts to set itemSize by measuring a repeated element in the dom */
 18275  VirtualRepeatController.prototype.readItemSize_ = function() {
 18276    if (this.itemSize) {
 18277      // itemSize was successfully read in a different asynchronous call.
 18278      return;
 18279    }
 18280  
 18281    this.items = this.repeatListExpression(this.$scope);
 18282    this.parentNode = this.$element[0].parentNode;
 18283    var block = this.getBlock_(0);
 18284    if (!block.element[0].parentNode) {
 18285      this.parentNode.appendChild(block.element[0]);
 18286    }
 18287  
 18288    this.itemSize = block.element[0][
 18289        this.container.isHorizontal() ? 'offsetWidth' : 'offsetHeight'] || null;
 18290  
 18291    this.blocks[0] = block;
 18292    this.poolBlock_(0);
 18293  
 18294    if (this.itemSize) {
 18295      this.containerUpdated();
 18296    }
 18297  };
 18298  
 18299  
 18300  /**
 18301   * Returns the user-specified repeat list, transforming it into an array-like
 18302   * object in the case of infinite scroll/dynamic load mode.
 18303   * @param {!angular.Scope} The scope.
 18304   * @return {!Array|!Object} An array or array-like object for iteration.
 18305   */
 18306  VirtualRepeatController.prototype.repeatListExpression_ = function(scope) {
 18307    var repeatList = this.rawRepeatListExpression(scope);
 18308  
 18309    if (this.onDemand && repeatList) {
 18310      var virtualList = new VirtualRepeatModelArrayLike(repeatList);
 18311      virtualList.$$includeIndexes(this.newStartIndex, this.newVisibleEnd);
 18312      return virtualList;
 18313    } else {
 18314      return repeatList;
 18315    }
 18316  };
 18317  
 18318  
 18319  /**
 18320   * Called by the container. Informs us that the containers scroll or size has
 18321   * changed.
 18322   */
 18323  VirtualRepeatController.prototype.containerUpdated = function() {
 18324    // If itemSize is unknown, attempt to measure it.
 18325    if (!this.itemSize) {
 18326      this.unwatchItemSize_ = this.$scope.$watchCollection(
 18327          this.repeatListExpression,
 18328          angular.bind(this, function(items) {
 18329            if (items && items.length) {
 18330              this.$$rAF(angular.bind(this, this.readItemSize_));
 18331            }
 18332          }));
 18333      if (!this.$rootScope.$$phase) this.$scope.$digest();
 18334  
 18335      return;
 18336    } else if (!this.sized) {
 18337      this.items = this.repeatListExpression(this.$scope);
 18338    }
 18339  
 18340    if (!this.sized) {
 18341      this.unwatchItemSize_();
 18342      this.sized = true;
 18343      this.$scope.$watchCollection(this.repeatListExpression,
 18344          angular.bind(this, function(items, oldItems) {
 18345            if (!this.isVirtualRepeatUpdating_) {
 18346              this.virtualRepeatUpdate_(items, oldItems);
 18347            }
 18348          }));
 18349    }
 18350  
 18351    this.updateIndexes_();
 18352  
 18353    if (this.newStartIndex !== this.startIndex ||
 18354        this.newEndIndex !== this.endIndex ||
 18355        this.container.getScrollOffset() > this.container.getScrollSize()) {
 18356      if (this.items instanceof VirtualRepeatModelArrayLike) {
 18357        this.items.$$includeIndexes(this.newStartIndex, this.newEndIndex);
 18358      }
 18359      this.virtualRepeatUpdate_(this.items, this.items);
 18360    }
 18361  };
 18362  
 18363  
 18364  /**
 18365   * Called by the container. Returns the size of a single repeated item.
 18366   * @return {?number} Size of a repeated item.
 18367   */
 18368  VirtualRepeatController.prototype.getItemSize = function() {
 18369    return this.itemSize;
 18370  };
 18371  
 18372  
 18373  /**
 18374   * Called by the container. Returns the size of a single repeated item.
 18375   * @return {?number} Size of a repeated item.
 18376   */
 18377  VirtualRepeatController.prototype.getItemCount = function() {
 18378    return this.itemsLength;
 18379  };
 18380  
 18381  
 18382  /**
 18383   * Updates the order and visible offset of repeated blocks in response to scrolling
 18384   * or items updates.
 18385   * @private
 18386   */
 18387  VirtualRepeatController.prototype.virtualRepeatUpdate_ = function(items, oldItems) {
 18388    this.isVirtualRepeatUpdating_ = true;
 18389  
 18390    var itemsLength = items && items.length || 0;
 18391    var lengthChanged = false;
 18392  
 18393    // If the number of items shrank, scroll up to the top.
 18394    if (this.items && itemsLength < this.items.length && this.container.getScrollOffset() !== 0) {
 18395      this.items = items;
 18396      this.container.resetScroll();
 18397      return;
 18398    }
 18399  
 18400    if (itemsLength !== this.itemsLength) {
 18401      lengthChanged = true;
 18402      this.itemsLength = itemsLength;
 18403    }
 18404  
 18405    this.items = items;
 18406    if (items !== oldItems || lengthChanged) {
 18407      this.updateIndexes_();
 18408    }
 18409  
 18410    this.parentNode = this.$element[0].parentNode;
 18411  
 18412    if (lengthChanged) {
 18413      this.container.setScrollSize(itemsLength * this.itemSize);
 18414    }
 18415  
 18416    if (this.isFirstRender) {
 18417      this.isFirstRender = false;
 18418      var startIndex = this.$attrs.mdStartIndex ?
 18419        this.$scope.$eval(this.$attrs.mdStartIndex) :
 18420        this.container.topIndex;
 18421      this.container.scrollToIndex(startIndex);
 18422    }
 18423  
 18424    // Detach and pool any blocks that are no longer in the viewport.
 18425    Object.keys(this.blocks).forEach(function(blockIndex) {
 18426      var index = parseInt(blockIndex, 10);
 18427      if (index < this.newStartIndex || index >= this.newEndIndex) {
 18428        this.poolBlock_(index);
 18429      }
 18430    }, this);
 18431  
 18432    // Add needed blocks.
 18433    // For performance reasons, temporarily block browser url checks as we digest
 18434    // the restored block scopes ($$checkUrlChange reads window.location to
 18435    // check for changes and trigger route change, etc, which we don't need when
 18436    // trying to scroll at 60fps).
 18437    this.$browser.$$checkUrlChange = angular.noop;
 18438  
 18439    var i, block,
 18440        newStartBlocks = [],
 18441        newEndBlocks = [];
 18442  
 18443    // Collect blocks at the top.
 18444    for (i = this.newStartIndex; i < this.newEndIndex && this.blocks[i] == null; i++) {
 18445      block = this.getBlock_(i);
 18446      this.updateBlock_(block, i);
 18447      newStartBlocks.push(block);
 18448    }
 18449  
 18450    // Update blocks that are already rendered.
 18451    for (; this.blocks[i] != null; i++) {
 18452      this.updateBlock_(this.blocks[i], i);
 18453    }
 18454    var maxIndex = i - 1;
 18455  
 18456    // Collect blocks at the end.
 18457    for (; i < this.newEndIndex; i++) {
 18458      block = this.getBlock_(i);
 18459      this.updateBlock_(block, i);
 18460      newEndBlocks.push(block);
 18461    }
 18462  
 18463    // Attach collected blocks to the document.
 18464    if (newStartBlocks.length) {
 18465      this.parentNode.insertBefore(
 18466          this.domFragmentFromBlocks_(newStartBlocks),
 18467          this.$element[0].nextSibling);
 18468    }
 18469    if (newEndBlocks.length) {
 18470      this.parentNode.insertBefore(
 18471          this.domFragmentFromBlocks_(newEndBlocks),
 18472          this.blocks[maxIndex] && this.blocks[maxIndex].element[0].nextSibling);
 18473    }
 18474  
 18475    // Restore $$checkUrlChange.
 18476    this.$browser.$$checkUrlChange = this.browserCheckUrlChange;
 18477  
 18478    this.startIndex = this.newStartIndex;
 18479    this.endIndex = this.newEndIndex;
 18480  
 18481    this.isVirtualRepeatUpdating_ = false;
 18482  };
 18483  
 18484  
 18485  /**
 18486   * @param {number} index Where the block is to be in the repeated list.
 18487   * @return {!VirtualRepeatController.Block} A new or pooled block to place at the specified index.
 18488   * @private
 18489   */
 18490  VirtualRepeatController.prototype.getBlock_ = function(index) {
 18491    if (this.pooledBlocks.length) {
 18492      return this.pooledBlocks.pop();
 18493    }
 18494  
 18495    var block;
 18496    this.transclude(angular.bind(this, function(clone, scope) {
 18497      block = {
 18498        element: clone,
 18499        new: true,
 18500        scope: scope
 18501      };
 18502  
 18503      this.updateScope_(scope, index);
 18504      this.parentNode.appendChild(clone[0]);
 18505    }));
 18506  
 18507    return block;
 18508  };
 18509  
 18510  
 18511  /**
 18512   * Updates and if not in a digest cycle, digests the specified block's scope to the data
 18513   * at the specified index.
 18514   * @param {!VirtualRepeatController.Block} block The block whose scope should be updated.
 18515   * @param {number} index The index to set.
 18516   * @private
 18517   */
 18518  VirtualRepeatController.prototype.updateBlock_ = function(block, index) {
 18519    this.blocks[index] = block;
 18520  
 18521    if (!block.new &&
 18522        (block.scope.$index === index && block.scope[this.repeatName] === this.items[index])) {
 18523      return;
 18524    }
 18525    block.new = false;
 18526  
 18527    // Update and digest the block's scope.
 18528    this.updateScope_(block.scope, index);
 18529  
 18530    // Perform digest before reattaching the block.
 18531    // Any resulting synchronous dom mutations should be much faster as a result.
 18532    // This might break some directives, but I'm going to try it for now.
 18533    if (!this.$rootScope.$$phase) {
 18534      block.scope.$digest();
 18535    }
 18536  };
 18537  
 18538  
 18539  /**
 18540   * Updates scope to the data at the specified index.
 18541   * @param {!angular.Scope} scope The scope which should be updated.
 18542   * @param {number} index The index to set.
 18543   * @private
 18544   */
 18545  VirtualRepeatController.prototype.updateScope_ = function(scope, index) {
 18546    scope.$index = index;
 18547    scope[this.repeatName] = this.items && this.items[index];
 18548    if (this.extraName) scope[this.extraName(this.$scope)] = this.items[index];
 18549  };
 18550  
 18551  
 18552  /**
 18553   * Pools the block at the specified index (Pulls its element out of the dom and stores it).
 18554   * @param {number} index The index at which the block to pool is stored.
 18555   * @private
 18556   */
 18557  VirtualRepeatController.prototype.poolBlock_ = function(index) {
 18558    this.pooledBlocks.push(this.blocks[index]);
 18559    this.parentNode.removeChild(this.blocks[index].element[0]);
 18560    delete this.blocks[index];
 18561  };
 18562  
 18563  
 18564  /**
 18565   * Produces a dom fragment containing the elements from the list of blocks.
 18566   * @param {!Array<!VirtualRepeatController.Block>} blocks The blocks whose elements
 18567   *     should be added to the document fragment.
 18568   * @return {DocumentFragment}
 18569   * @private
 18570   */
 18571  VirtualRepeatController.prototype.domFragmentFromBlocks_ = function(blocks) {
 18572    var fragment = this.$document[0].createDocumentFragment();
 18573    blocks.forEach(function(block) {
 18574      fragment.appendChild(block.element[0]);
 18575    });
 18576    return fragment;
 18577  };
 18578  
 18579  
 18580  /**
 18581   * Updates start and end indexes based on length of repeated items and container size.
 18582   * @private
 18583   */
 18584  VirtualRepeatController.prototype.updateIndexes_ = function() {
 18585    var itemsLength = this.items ? this.items.length : 0;
 18586    var containerLength = Math.ceil(this.container.getSize() / this.itemSize);
 18587  
 18588    this.newStartIndex = Math.max(0, Math.min(
 18589        itemsLength - containerLength,
 18590        Math.floor(this.container.getScrollOffset() / this.itemSize)));
 18591    this.newVisibleEnd = this.newStartIndex + containerLength + NUM_EXTRA;
 18592    this.newEndIndex = Math.min(itemsLength, this.newVisibleEnd);
 18593    this.newStartIndex = Math.max(0, this.newStartIndex - NUM_EXTRA);
 18594  };
 18595  
 18596  /**
 18597   * This VirtualRepeatModelArrayLike class enforces the interface requirements
 18598   * for infinite scrolling within a mdVirtualRepeatContainer. An object with this
 18599   * interface must implement the following interface with two (2) methods:
 18600   *
 18601   * getItemAtIndex: function(index) -> item at that index or null if it is not yet
 18602   *     loaded (It should start downloading the item in that case).
 18603   *
 18604   * getLength: function() -> number The data legnth to which the repeater container
 18605   *     should be sized. Ideally, when the count is known, this method should return it.
 18606   *     Otherwise, return a higher number than the currently loaded items to produce an
 18607   *     infinite-scroll behavior.
 18608   *
 18609   * @usage
 18610   * <hljs lang="html">
 18611   *  <md-virtual-repeat-container md-orient-horizontal>
 18612   *    <div md-virtual-repeat="i in items" md-on-demand>
 18613   *      Hello {{i}}!
 18614   *    </div>
 18615   *  </md-virtual-repeat-container>
 18616   * </hljs>
 18617   *
 18618   */
 18619  function VirtualRepeatModelArrayLike(model) {
 18620    if (!angular.isFunction(model.getItemAtIndex) ||
 18621        !angular.isFunction(model.getLength)) {
 18622      throw Error('When md-on-demand is enabled, the Object passed to md-virtual-repeat must implement ' +
 18623          'functions getItemAtIndex() and getLength() ');
 18624    }
 18625  
 18626    this.model = model;
 18627  }
 18628  
 18629  
 18630  VirtualRepeatModelArrayLike.prototype.$$includeIndexes = function(start, end) {
 18631    for (var i = start; i < end; i++) {
 18632      if (!this.hasOwnProperty(i)) {
 18633        this[i] = this.model.getItemAtIndex(i);
 18634      }
 18635    }
 18636    this.length = this.model.getLength();
 18637  };
 18638  
 18639  
 18640  function abstractMethod() {
 18641    throw Error('Non-overridden abstract method called.');
 18642  }
 18643  
 18644  })();
 18645  (function(){
 18646  "use strict";
 18647  
 18648  /**
 18649   * @ngdoc module
 18650   * @name material.components.whiteframe
 18651   */
 18652  angular
 18653    .module('material.components.whiteframe', ['material.core'])
 18654    .directive('mdWhiteframe', MdWhiteframeDirective);
 18655  
 18656  /**
 18657   * @private
 18658   * @ngdoc directive
 18659   * @module material.components.whiteframe
 18660   * @name mdWhiteframe
 18661   * @restrict A
 18662   *
 18663   * @description
 18664   * The md-whiteframe directive allows you to apply an elevation shadow to an element.
 18665   *
 18666   * The attribute values needs to be a number between 1 and 24.
 18667   *
 18668   * ### Notes
 18669   * - If there is no value specified it defaults to 4dp.
 18670   * - If the value is not valid it defaults to 4dp.
 18671  
 18672   * @usage
 18673   * <hljs lang="html">
 18674   * <div md-whiteframe="3">
 18675   *   <span>Elevation of 3dp</span>
 18676   * </div>
 18677   * </hljs>
 18678   */
 18679  function MdWhiteframeDirective($log) {
 18680    var MIN_DP = 1;
 18681    var MAX_DP = 24;
 18682    var DEFAULT_DP = 4;
 18683  
 18684    return {
 18685      restrict: 'A',
 18686      link: postLink
 18687    };
 18688  
 18689    function postLink(scope, element, attr) {
 18690      var elevation = parseInt(attr.mdWhiteframe, 10) || DEFAULT_DP;
 18691  
 18692      if (elevation > MAX_DP || elevation < MIN_DP) {
 18693        $log.warn('md-whiteframe attribute value is invalid. It should be a number between ' + MIN_DP + ' and ' + MAX_DP, element[0]);
 18694        elevation = DEFAULT_DP;
 18695      }
 18696  
 18697      element.addClass('md-whiteframe-' + elevation + 'dp');
 18698    }
 18699  }
 18700  MdWhiteframeDirective.$inject = ["$log"];
 18701  
 18702  
 18703  })();
 18704  (function(){
 18705  "use strict";
 18706  
 18707  angular
 18708      .module('material.components.autocomplete')
 18709      .controller('MdAutocompleteCtrl', MdAutocompleteCtrl);
 18710  
 18711  var ITEM_HEIGHT   = 41,
 18712      MAX_HEIGHT    = 5.5 * ITEM_HEIGHT,
 18713      MENU_PADDING  = 8,
 18714      INPUT_PADDING = 2; // Padding provided by `md-input-container`
 18715  
 18716  function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming, $window,
 18717                               $animate, $rootElement, $attrs, $q) {
 18718    //-- private variables
 18719    var ctrl                 = this,
 18720        itemParts            = $scope.itemsExpr.split(/ in /i),
 18721        itemExpr             = itemParts[ 1 ],
 18722        elements             = null,
 18723        cache                = {},
 18724        noBlur               = false,
 18725        selectedItemWatchers = [],
 18726        hasFocus             = false,
 18727        lastCount            = 0,
 18728        fetchesInProgress    = 0;
 18729  
 18730    //-- public variables with handlers
 18731    defineProperty('hidden', handleHiddenChange, true);
 18732  
 18733    //-- public variables
 18734    ctrl.scope      = $scope;
 18735    ctrl.parent     = $scope.$parent;
 18736    ctrl.itemName   = itemParts[ 0 ];
 18737    ctrl.matches    = [];
 18738    ctrl.loading    = false;
 18739    ctrl.hidden     = true;
 18740    ctrl.index      = null;
 18741    ctrl.messages   = [];
 18742    ctrl.id         = $mdUtil.nextUid();
 18743    ctrl.isDisabled = null;
 18744    ctrl.isRequired = null;
 18745    ctrl.isReadonly = null;
 18746    ctrl.hasNotFound = false;
 18747  
 18748    //-- public methods
 18749    ctrl.keydown                       = keydown;
 18750    ctrl.blur                          = blur;
 18751    ctrl.focus                         = focus;
 18752    ctrl.clear                         = clearValue;
 18753    ctrl.select                        = select;
 18754    ctrl.listEnter                     = onListEnter;
 18755    ctrl.listLeave                     = onListLeave;
 18756    ctrl.mouseUp                       = onMouseup;
 18757    ctrl.getCurrentDisplayValue        = getCurrentDisplayValue;
 18758    ctrl.registerSelectedItemWatcher   = registerSelectedItemWatcher;
 18759    ctrl.unregisterSelectedItemWatcher = unregisterSelectedItemWatcher;
 18760    ctrl.notFoundVisible               = notFoundVisible;
 18761    ctrl.loadingIsVisible              = loadingIsVisible;
 18762  
 18763    return init();
 18764  
 18765    //-- initialization methods
 18766  
 18767    /**
 18768     * Initialize the controller, setup watchers, gather elements
 18769     */
 18770    function init () {
 18771      $mdUtil.initOptionalProperties($scope, $attrs, { searchText: null, selectedItem: null });
 18772      $mdTheming($element);
 18773      configureWatchers();
 18774      $mdUtil.nextTick(function () {
 18775        gatherElements();
 18776        moveDropdown();
 18777        focusElement();
 18778        $element.on('focus', focusElement);
 18779      });
 18780    }
 18781  
 18782    /**
 18783     * Calculates the dropdown's position and applies the new styles to the menu element
 18784     * @returns {*}
 18785     */
 18786    function positionDropdown () {
 18787      if (!elements) return $mdUtil.nextTick(positionDropdown, false, $scope);
 18788      var hrect  = elements.wrap.getBoundingClientRect(),
 18789          vrect  = elements.snap.getBoundingClientRect(),
 18790          root   = elements.root.getBoundingClientRect(),
 18791          top    = vrect.bottom - root.top,
 18792          bot    = root.bottom - vrect.top,
 18793          left   = hrect.left - root.left,
 18794          width  = hrect.width,
 18795          offset = getVerticalOffset(),
 18796          styles;
 18797      // Adjust the width to account for the padding provided by `md-input-container`
 18798      if ($attrs.mdFloatingLabel) {
 18799        left += INPUT_PADDING;
 18800        width -= INPUT_PADDING * 2;
 18801      }
 18802      styles = {
 18803        left:     left + 'px',
 18804        minWidth: width + 'px',
 18805        maxWidth: Math.max(hrect.right - root.left, root.right - hrect.left) - MENU_PADDING + 'px'
 18806      };
 18807      if (top > bot && root.height - hrect.bottom - MENU_PADDING < MAX_HEIGHT) {
 18808        styles.top       = 'auto';
 18809        styles.bottom    = bot + 'px';
 18810        styles.maxHeight = Math.min(MAX_HEIGHT, hrect.top - root.top - MENU_PADDING) + 'px';
 18811      } else {
 18812        styles.top       = (top - offset) + 'px';
 18813        styles.bottom    = 'auto';
 18814        styles.maxHeight = Math.min(MAX_HEIGHT, root.bottom + $mdUtil.scrollTop() - hrect.bottom - MENU_PADDING) + 'px';
 18815      }
 18816  
 18817      elements.$.scrollContainer.css(styles);
 18818      $mdUtil.nextTick(correctHorizontalAlignment, false);
 18819  
 18820      /**
 18821       * Calculates the vertical offset for floating label examples to account for ngMessages
 18822       * @returns {number}
 18823       */
 18824      function getVerticalOffset () {
 18825        var offset = 0;
 18826        var inputContainer = $element.find('md-input-container');
 18827        if (inputContainer.length) {
 18828          var input = inputContainer.find('input');
 18829          offset = inputContainer.prop('offsetHeight');
 18830          offset -= input.prop('offsetTop');
 18831          offset -= input.prop('offsetHeight');
 18832          // add in the height left up top for the floating label text
 18833          offset += inputContainer.prop('offsetTop');
 18834        }
 18835        return offset;
 18836      }
 18837  
 18838      /**
 18839       * Makes sure that the menu doesn't go off of the screen on either side.
 18840       */
 18841      function correctHorizontalAlignment () {
 18842        var dropdown = elements.scrollContainer.getBoundingClientRect(),
 18843            styles   = {};
 18844        if (dropdown.right > root.right - MENU_PADDING) {
 18845          styles.left = (hrect.right - dropdown.width) + 'px';
 18846        }
 18847        elements.$.scrollContainer.css(styles);
 18848      }
 18849    }
 18850  
 18851    /**
 18852     * Moves the dropdown menu to the body tag in order to avoid z-index and overflow issues.
 18853     */
 18854    function moveDropdown () {
 18855      if (!elements.$.root.length) return;
 18856      $mdTheming(elements.$.scrollContainer);
 18857      elements.$.scrollContainer.detach();
 18858      elements.$.root.append(elements.$.scrollContainer);
 18859      if ($animate.pin) $animate.pin(elements.$.scrollContainer, $rootElement);
 18860    }
 18861  
 18862    /**
 18863     * Sends focus to the input element.
 18864     */
 18865    function focusElement () {
 18866      if ($scope.autofocus) elements.input.focus();
 18867    }
 18868  
 18869    /**
 18870     * Sets up any watchers used by autocomplete
 18871     */
 18872    function configureWatchers () {
 18873      var wait = parseInt($scope.delay, 10) || 0;
 18874      $attrs.$observe('disabled', function (value) { ctrl.isDisabled = $mdUtil.parseAttributeBoolean(value, false); });
 18875      $attrs.$observe('required', function (value) { ctrl.isRequired = $mdUtil.parseAttributeBoolean(value, false); });
 18876      $attrs.$observe('readonly', function (value) { ctrl.isReadonly = $mdUtil.parseAttributeBoolean(value, false); });
 18877      $scope.$watch('searchText', wait ? $mdUtil.debounce(handleSearchText, wait) : handleSearchText);
 18878      $scope.$watch('selectedItem', selectedItemChange);
 18879      angular.element($window).on('resize', positionDropdown);
 18880      $scope.$on('$destroy', cleanup);
 18881    }
 18882  
 18883    /**
 18884     * Removes any events or leftover elements created by this controller
 18885     */
 18886    function cleanup () {
 18887      if(!ctrl.hidden) {
 18888        $mdUtil.enableScrolling();
 18889      }
 18890  
 18891      angular.element($window).off('resize', positionDropdown);
 18892      if ( elements ){
 18893        var items = 'ul scroller scrollContainer input'.split(' ');
 18894        angular.forEach(items, function(key){
 18895          elements.$[key].remove();
 18896        });
 18897      }
 18898    }
 18899  
 18900    /**
 18901     * Gathers all of the elements needed for this controller
 18902     */
 18903    function gatherElements () {
 18904      elements = {
 18905        main:  $element[0],
 18906        scrollContainer: $element[0].getElementsByClassName('md-virtual-repeat-container')[0],
 18907        scroller: $element[0].getElementsByClassName('md-virtual-repeat-scroller')[0],
 18908        ul:    $element.find('ul')[0],
 18909        input: $element.find('input')[0],
 18910        wrap:  $element.find('md-autocomplete-wrap')[0],
 18911        root:  document.body
 18912      };
 18913      elements.li   = elements.ul.getElementsByTagName('li');
 18914      elements.snap = getSnapTarget();
 18915      elements.$    = getAngularElements(elements);
 18916    }
 18917  
 18918    /**
 18919     * Finds the element that the menu will base its position on
 18920     * @returns {*}
 18921     */
 18922    function getSnapTarget () {
 18923      for (var element = $element; element.length; element = element.parent()) {
 18924        if (angular.isDefined(element.attr('md-autocomplete-snap'))) return element[ 0 ];
 18925      }
 18926      return elements.wrap;
 18927    }
 18928  
 18929    /**
 18930     * Gathers angular-wrapped versions of each element
 18931     * @param elements
 18932     * @returns {{}}
 18933     */
 18934    function getAngularElements (elements) {
 18935      var obj = {};
 18936      for (var key in elements) {
 18937        if (elements.hasOwnProperty(key)) obj[ key ] = angular.element(elements[ key ]);
 18938      }
 18939      return obj;
 18940    }
 18941  
 18942    //-- event/change handlers
 18943  
 18944    /**
 18945     * Handles changes to the `hidden` property.
 18946     * @param hidden
 18947     * @param oldHidden
 18948     */
 18949    function handleHiddenChange (hidden, oldHidden) {
 18950      if (!hidden && oldHidden) {
 18951        positionDropdown();
 18952  
 18953        if (elements) {
 18954          $mdUtil.nextTick(function () {
 18955            $mdUtil.disableScrollAround(elements.ul);
 18956          }, false, $scope);
 18957        }
 18958      } else if (hidden && !oldHidden) {
 18959        $mdUtil.nextTick(function () {
 18960          $mdUtil.enableScrolling();
 18961        }, false, $scope);
 18962      }
 18963    }
 18964  
 18965    /**
 18966     * When the user mouses over the dropdown menu, ignore blur events.
 18967     */
 18968    function onListEnter () {
 18969      noBlur = true;
 18970    }
 18971  
 18972    /**
 18973     * When the user's mouse leaves the menu, blur events may hide the menu again.
 18974     */
 18975    function onListLeave () {
 18976      if (!hasFocus) elements.input.focus();
 18977      noBlur = false;
 18978      ctrl.hidden = shouldHide();
 18979    }
 18980  
 18981    /**
 18982     * When the mouse button is released, send focus back to the input field.
 18983     */
 18984    function onMouseup () {
 18985      elements.input.focus();
 18986    }
 18987  
 18988    /**
 18989     * Handles changes to the selected item.
 18990     * @param selectedItem
 18991     * @param previousSelectedItem
 18992     */
 18993    function selectedItemChange (selectedItem, previousSelectedItem) {
 18994      if (selectedItem) {
 18995        getDisplayValue(selectedItem).then(function (val) {
 18996          $scope.searchText = val;
 18997          handleSelectedItemChange(selectedItem, previousSelectedItem);
 18998        });
 18999      }
 19000  
 19001      if (selectedItem !== previousSelectedItem) announceItemChange();
 19002    }
 19003  
 19004    /**
 19005     * Use the user-defined expression to announce changes each time a new item is selected
 19006     */
 19007    function announceItemChange () {
 19008      angular.isFunction($scope.itemChange) && $scope.itemChange(getItemAsNameVal($scope.selectedItem));
 19009    }
 19010  
 19011    /**
 19012     * Use the user-defined expression to announce changes each time the search text is changed
 19013     */
 19014    function announceTextChange () {
 19015      angular.isFunction($scope.textChange) && $scope.textChange();
 19016    }
 19017  
 19018    /**
 19019     * Calls any external watchers listening for the selected item.  Used in conjunction with
 19020     * `registerSelectedItemWatcher`.
 19021     * @param selectedItem
 19022     * @param previousSelectedItem
 19023     */
 19024    function handleSelectedItemChange (selectedItem, previousSelectedItem) {
 19025      selectedItemWatchers.forEach(function (watcher) { watcher(selectedItem, previousSelectedItem); });
 19026    }
 19027  
 19028    /**
 19029     * Register a function to be called when the selected item changes.
 19030     * @param cb
 19031     */
 19032    function registerSelectedItemWatcher (cb) {
 19033      if (selectedItemWatchers.indexOf(cb) == -1) {
 19034        selectedItemWatchers.push(cb);
 19035      }
 19036    }
 19037  
 19038    /**
 19039     * Unregister a function previously registered for selected item changes.
 19040     * @param cb
 19041     */
 19042    function unregisterSelectedItemWatcher (cb) {
 19043      var i = selectedItemWatchers.indexOf(cb);
 19044      if (i != -1) {
 19045        selectedItemWatchers.splice(i, 1);
 19046      }
 19047    }
 19048  
 19049    /**
 19050     * Handles changes to the searchText property.
 19051     * @param searchText
 19052     * @param previousSearchText
 19053     */
 19054    function handleSearchText (searchText, previousSearchText) {
 19055      ctrl.index = getDefaultIndex();
 19056      // do nothing on init
 19057      if (searchText === previousSearchText) return;
 19058  
 19059      getDisplayValue($scope.selectedItem).then(function (val) {
 19060        // clear selected item if search text no longer matches it
 19061        if (searchText !== val) {
 19062          $scope.selectedItem = null;
 19063  
 19064          // trigger change event if available
 19065          if (searchText !== previousSearchText) announceTextChange();
 19066  
 19067          // cancel results if search text is not long enough
 19068          if (!isMinLengthMet()) {
 19069            ctrl.matches = [];
 19070            setLoading(false);
 19071            updateMessages();
 19072          } else {
 19073            handleQuery();
 19074          }
 19075        }
 19076      });
 19077  
 19078    }
 19079  
 19080    /**
 19081     * Handles input blur event, determines if the dropdown should hide.
 19082     */
 19083    function blur () {
 19084      hasFocus = false;
 19085      if (!noBlur) {
 19086        ctrl.hidden = shouldHide();
 19087      }
 19088    }
 19089  
 19090    /**
 19091     * Force blur on input element
 19092     * @param forceBlur
 19093     */
 19094    function doBlur(forceBlur) {
 19095      if (forceBlur) {
 19096        noBlur = false;
 19097        hasFocus = false;
 19098      }
 19099      elements.input.blur();
 19100    }
 19101  
 19102    /**
 19103     * Handles input focus event, determines if the dropdown should show.
 19104     */
 19105    function focus () {
 19106      hasFocus = true;
 19107      //-- if searchText is null, let's force it to be a string
 19108      if (!angular.isString($scope.searchText)) $scope.searchText = '';
 19109      ctrl.hidden = shouldHide();
 19110      if (!ctrl.hidden) handleQuery();
 19111    }
 19112  
 19113    /**
 19114     * Handles keyboard input.
 19115     * @param event
 19116     */
 19117    function keydown (event) {
 19118      switch (event.keyCode) {
 19119        case $mdConstant.KEY_CODE.DOWN_ARROW:
 19120          if (ctrl.loading) return;
 19121          event.stopPropagation();
 19122          event.preventDefault();
 19123          ctrl.index   = Math.min(ctrl.index + 1, ctrl.matches.length - 1);
 19124          updateScroll();
 19125          updateMessages();
 19126          break;
 19127        case $mdConstant.KEY_CODE.UP_ARROW:
 19128          if (ctrl.loading) return;
 19129          event.stopPropagation();
 19130          event.preventDefault();
 19131          ctrl.index   = ctrl.index < 0 ? ctrl.matches.length - 1 : Math.max(0, ctrl.index - 1);
 19132          updateScroll();
 19133          updateMessages();
 19134          break;
 19135        case $mdConstant.KEY_CODE.TAB:
 19136          // If we hit tab, assume that we've left the list so it will close
 19137          onListLeave();
 19138  
 19139          if (ctrl.hidden || ctrl.loading || ctrl.index < 0 || ctrl.matches.length < 1) return;
 19140          select(ctrl.index);
 19141          break;
 19142        case $mdConstant.KEY_CODE.ENTER:
 19143          if (ctrl.hidden || ctrl.loading || ctrl.index < 0 || ctrl.matches.length < 1) return;
 19144          if (hasSelection()) return;
 19145          event.stopPropagation();
 19146          event.preventDefault();
 19147          select(ctrl.index);
 19148          break;
 19149        case $mdConstant.KEY_CODE.ESCAPE:
 19150          event.stopPropagation();
 19151          event.preventDefault();
 19152          clearValue();
 19153  
 19154          // Force the component to blur if they hit escape
 19155          doBlur(true);
 19156  
 19157          break;
 19158        default:
 19159      }
 19160    }
 19161  
 19162    //-- getters
 19163  
 19164    /**
 19165     * Returns the minimum length needed to display the dropdown.
 19166     * @returns {*}
 19167     */
 19168    function getMinLength () {
 19169      return angular.isNumber($scope.minLength) ? $scope.minLength : 1;
 19170    }
 19171  
 19172    /**
 19173     * Returns the display value for an item.
 19174     * @param item
 19175     * @returns {*}
 19176     */
 19177    function getDisplayValue (item) {
 19178      return $q.when(getItemText(item) || item);
 19179  
 19180      /**
 19181       * Getter function to invoke user-defined expression (in the directive)
 19182       * to convert your object to a single string.
 19183       */
 19184      function getItemText (item) {
 19185        return (item && $scope.itemText) ? $scope.itemText(getItemAsNameVal(item)) : null;
 19186      }
 19187    }
 19188  
 19189    /**
 19190     * Returns the locals object for compiling item templates.
 19191     * @param item
 19192     * @returns {{}}
 19193     */
 19194    function getItemAsNameVal (item) {
 19195      if (!item) return undefined;
 19196  
 19197      var locals = {};
 19198      if (ctrl.itemName) locals[ ctrl.itemName ] = item;
 19199  
 19200      return locals;
 19201    }
 19202  
 19203    /**
 19204     * Returns the default index based on whether or not autoselect is enabled.
 19205     * @returns {number}
 19206     */
 19207    function getDefaultIndex () {
 19208      return $scope.autoselect ? 0 : -1;
 19209    }
 19210  
 19211    /**
 19212     * Sets the loading parameter and updates the hidden state.
 19213     * @param value {boolean} Whether or not the component is currently loading.
 19214     */
 19215    function setLoading(value) {
 19216      if (ctrl.loading != value) {
 19217        ctrl.loading = value;
 19218      }
 19219  
 19220      // Always refresh the hidden variable as something else might have changed
 19221      ctrl.hidden = shouldHide();
 19222    }
 19223  
 19224    /**
 19225     * Determines if the menu should be hidden.
 19226     * @returns {boolean}
 19227     */
 19228    function shouldHide () {
 19229      if (ctrl.loading && !hasMatches()) return true; // Hide while loading initial matches
 19230      else if (hasSelection()) return true;           // Hide if there is already a selection
 19231      else if (!hasFocus) return true;                // Hide if the input does not have focus
 19232      else return !shouldShow();                      // Defer to standard show logic
 19233    }
 19234  
 19235    /**
 19236     * Determines if the menu should be shown.
 19237     * @returns {boolean}
 19238     */
 19239    function shouldShow() {
 19240      return (isMinLengthMet() && hasMatches()) || notFoundVisible();
 19241    }
 19242  
 19243    /**
 19244     * Returns true if the search text has matches.
 19245     * @returns {boolean}
 19246     */
 19247    function hasMatches() {
 19248      return ctrl.matches.length ? true : false;
 19249    }
 19250  
 19251    /**
 19252     * Returns true if the autocomplete has a valid selection.
 19253     * @returns {boolean}
 19254     */
 19255    function hasSelection() {
 19256      return ctrl.scope.selectedItem ? true : false;
 19257    }
 19258  
 19259    /**
 19260     * Returns true if the loading indicator is, or should be, visible.
 19261     * @returns {boolean}
 19262     */
 19263    function loadingIsVisible() {
 19264      return ctrl.loading && !hasSelection();
 19265    }
 19266  
 19267    /**
 19268     * Returns the display value of the current item.
 19269     * @returns {*}
 19270     */
 19271    function getCurrentDisplayValue () {
 19272      return getDisplayValue(ctrl.matches[ ctrl.index ]);
 19273    }
 19274  
 19275    /**
 19276     * Determines if the minimum length is met by the search text.
 19277     * @returns {*}
 19278     */
 19279    function isMinLengthMet () {
 19280      return ($scope.searchText || '').length >= getMinLength();
 19281    }
 19282  
 19283    //-- actions
 19284  
 19285    /**
 19286     * Defines a public property with a handler and a default value.
 19287     * @param key
 19288     * @param handler
 19289     * @param value
 19290     */
 19291    function defineProperty (key, handler, value) {
 19292      Object.defineProperty(ctrl, key, {
 19293        get: function () { return value; },
 19294        set: function (newValue) {
 19295          var oldValue = value;
 19296          value        = newValue;
 19297          handler(newValue, oldValue);
 19298        }
 19299      });
 19300    }
 19301  
 19302    /**
 19303     * Selects the item at the given index.
 19304     * @param index
 19305     */
 19306    function select (index) {
 19307      //-- force form to update state for validation
 19308      $mdUtil.nextTick(function () {
 19309        getDisplayValue(ctrl.matches[ index ]).then(function (val) {
 19310          var ngModel = elements.$.input.controller('ngModel');
 19311          ngModel.$setViewValue(val);
 19312          ngModel.$render();
 19313        }).finally(function () {
 19314          $scope.selectedItem = ctrl.matches[ index ];
 19315          setLoading(false);
 19316        });
 19317      }, false);
 19318    }
 19319  
 19320    /**
 19321     * Clears the searchText value and selected item.
 19322     */
 19323    function clearValue () {
 19324      // Set the loading to true so we don't see flashes of content.
 19325      // The flashing will only occur when an async request is running.
 19326      // So the loading process will stop when the results had been retrieved.
 19327      setLoading(true);
 19328  
 19329      // Reset our variables
 19330      ctrl.index = 0;
 19331      ctrl.matches = [];
 19332      $scope.searchText = '';
 19333  
 19334      // Per http://www.w3schools.com/jsref/event_oninput.asp
 19335      var eventObj = document.createEvent('CustomEvent');
 19336      eventObj.initCustomEvent('input', true, true, { value: $scope.searchText });
 19337      elements.input.dispatchEvent(eventObj);
 19338  
 19339      elements.input.focus();
 19340    }
 19341  
 19342    /**
 19343     * Fetches the results for the provided search text.
 19344     * @param searchText
 19345     */
 19346    function fetchResults (searchText) {
 19347      var items = $scope.$parent.$eval(itemExpr),
 19348          term  = searchText.toLowerCase(),
 19349          isList = angular.isArray(items),
 19350          isPromise = !!items.then; // Every promise should contain a `then` property
 19351  
 19352      if (isList) handleResults(items);
 19353      else if (isPromise) handleAsyncResults(items);
 19354  
 19355      function handleAsyncResults(items) {
 19356        if ( !items ) return;
 19357  
 19358        items = $q.when(items);
 19359        fetchesInProgress++;
 19360        setLoading(true);
 19361  
 19362        $mdUtil.nextTick(function () {
 19363            items
 19364              .then(handleResults)
 19365              .finally(function(){
 19366                if (--fetchesInProgress === 0) {
 19367                  setLoading(false);
 19368                }
 19369              });
 19370        },true, $scope);
 19371      }
 19372  
 19373      function handleResults (matches) {
 19374        cache[ term ] = matches;
 19375        if ((searchText || '') !== ($scope.searchText || '')) return; //-- just cache the results if old request
 19376  
 19377        ctrl.matches = matches;
 19378        ctrl.hidden  = shouldHide();
 19379  
 19380        // If loading is in progress, then we'll end the progress. This is needed for example,
 19381        // when the `clear` button was clicked, because there we always show the loading process, to prevent flashing.
 19382        if (ctrl.loading) setLoading(false);
 19383  
 19384        if ($scope.selectOnMatch) selectItemOnMatch();
 19385  
 19386        updateMessages();
 19387        positionDropdown();
 19388      }
 19389    }
 19390  
 19391    /**
 19392     * Updates the ARIA messages
 19393     */
 19394    function updateMessages () {
 19395      getCurrentDisplayValue().then(function (msg) {
 19396        ctrl.messages = [ getCountMessage(), msg ];
 19397      });
 19398    }
 19399  
 19400    /**
 19401     * Returns the ARIA message for how many results match the current query.
 19402     * @returns {*}
 19403     */
 19404    function getCountMessage () {
 19405      if (lastCount === ctrl.matches.length) return '';
 19406      lastCount = ctrl.matches.length;
 19407      switch (ctrl.matches.length) {
 19408        case 0:
 19409          return 'There are no matches available.';
 19410        case 1:
 19411          return 'There is 1 match available.';
 19412        default:
 19413          return 'There are ' + ctrl.matches.length + ' matches available.';
 19414      }
 19415    }
 19416  
 19417    /**
 19418     * Makes sure that the focused element is within view.
 19419     */
 19420    function updateScroll () {
 19421      if (!elements.li[0]) return;
 19422      var height = elements.li[0].offsetHeight,
 19423          top = height * ctrl.index,
 19424          bot = top + height,
 19425          hgt = elements.scroller.clientHeight,
 19426          scrollTop = elements.scroller.scrollTop;
 19427      if (top < scrollTop) {
 19428        scrollTo(top);
 19429      } else if (bot > scrollTop + hgt) {
 19430        scrollTo(bot - hgt);
 19431      }
 19432    }
 19433  
 19434    function isPromiseFetching() {
 19435      return fetchesInProgress !== 0;
 19436    }
 19437  
 19438    function scrollTo (offset) {
 19439      elements.$.scrollContainer.controller('mdVirtualRepeatContainer').scrollTo(offset);
 19440    }
 19441  
 19442    function notFoundVisible () {
 19443      var textLength = (ctrl.scope.searchText || '').length;
 19444  
 19445      return ctrl.hasNotFound && !hasMatches() && (!ctrl.loading || isPromiseFetching()) && textLength >= getMinLength() && (hasFocus || noBlur) && !hasSelection();
 19446    }
 19447  
 19448    /**
 19449     * Starts the query to gather the results for the current searchText.  Attempts to return cached
 19450     * results first, then forwards the process to `fetchResults` if necessary.
 19451     */
 19452    function handleQuery () {
 19453      var searchText = $scope.searchText || '',
 19454          term       = searchText.toLowerCase();
 19455      //-- if results are cached, pull in cached results
 19456      if (!$scope.noCache && cache[ term ]) {
 19457        ctrl.matches = cache[ term ];
 19458        updateMessages();
 19459      } else {
 19460        fetchResults(searchText);
 19461      }
 19462  
 19463      ctrl.hidden = shouldHide();
 19464    }
 19465  
 19466    /**
 19467     * If there is only one matching item and the search text matches its display value exactly,
 19468     * automatically select that item.  Note: This function is only called if the user uses the
 19469     * `md-select-on-match` flag.
 19470     */
 19471    function selectItemOnMatch () {
 19472      var searchText = $scope.searchText,
 19473          matches    = ctrl.matches,
 19474          item       = matches[ 0 ];
 19475      if (matches.length === 1) getDisplayValue(item).then(function (displayValue) {
 19476        var isMatching = searchText == displayValue;
 19477        if ($scope.matchInsensitive && !isMatching) {
 19478          isMatching = searchText.toLowerCase() == displayValue.toLowerCase();
 19479        }
 19480  
 19481        if (isMatching) select(0);
 19482      });
 19483    }
 19484  
 19485  }
 19486  MdAutocompleteCtrl.$inject = ["$scope", "$element", "$mdUtil", "$mdConstant", "$mdTheming", "$window", "$animate", "$rootElement", "$attrs", "$q"];
 19487  
 19488  })();
 19489  (function(){
 19490  "use strict";
 19491  
 19492  angular
 19493      .module('material.components.autocomplete')
 19494      .directive('mdAutocomplete', MdAutocomplete);
 19495  
 19496  /**
 19497   * @ngdoc directive
 19498   * @name mdAutocomplete
 19499   * @module material.components.autocomplete
 19500   *
 19501   * @description
 19502   * `<md-autocomplete>` is a special input component with a drop-down of all possible matches to a
 19503   *     custom query. This component allows you to provide real-time suggestions as the user types
 19504   *     in the input area.
 19505   *
 19506   * To start, you will need to specify the required parameters and provide a template for your
 19507   *     results. The content inside `md-autocomplete` will be treated as a template.
 19508   *
 19509   * In more complex cases, you may want to include other content such as a message to display when
 19510   *     no matches were found.  You can do this by wrapping your template in `md-item-template` and
 19511   *     adding a tag for `md-not-found`.  An example of this is shown below.
 19512   *
 19513   * ### Validation
 19514   *
 19515   * You can use `ng-messages` to include validation the same way that you would normally validate;
 19516   *     however, if you want to replicate a standard input with a floating label, you will have to
 19517   *     do the following:
 19518   *
 19519   * - Make sure that your template is wrapped in `md-item-template`
 19520   * - Add your `ng-messages` code inside of `md-autocomplete`
 19521   * - Add your validation properties to `md-autocomplete` (ie. `required`)
 19522   * - Add a `name` to `md-autocomplete` (to be used on the generated `input`)
 19523   *
 19524   * There is an example below of how this should look.
 19525   *
 19526   *
 19527   * @param {expression} md-items An expression in the format of `item in items` to iterate over
 19528   *     matches for your search.
 19529   * @param {expression=} md-selected-item-change An expression to be run each time a new item is
 19530   *     selected
 19531   * @param {expression=} md-search-text-change An expression to be run each time the search text
 19532   *     updates
 19533   * @param {expression=} md-search-text A model to bind the search query text to
 19534   * @param {object=} md-selected-item A model to bind the selected item to
 19535   * @param {expression=} md-item-text An expression that will convert your object to a single string.
 19536   * @param {string=} placeholder Placeholder text that will be forwarded to the input.
 19537   * @param {boolean=} md-no-cache Disables the internal caching that happens in autocomplete
 19538   * @param {boolean=} ng-disabled Determines whether or not to disable the input field
 19539   * @param {number=} md-min-length Specifies the minimum length of text before autocomplete will
 19540   *     make suggestions
 19541   * @param {number=} md-delay Specifies the amount of time (in milliseconds) to wait before looking
 19542   *     for results
 19543   * @param {boolean=} md-autofocus If true, the autocomplete will be automatically focused when a `$mdDialog`,
 19544   *     `$mdBottomsheet` or `$mdSidenav`, which contains the autocomplete, is opening. <br/><br/>
 19545   *     Also the autocomplete will immediately focus the input element.
 19546   * @param {boolean=} md-no-asterisk When present, asterisk will not be appended to the floating label
 19547   * @param {boolean=} md-autoselect If true, the first item will be selected by default
 19548   * @param {string=} md-menu-class This will be applied to the dropdown menu for styling
 19549   * @param {string=} md-floating-label This will add a floating label to autocomplete and wrap it in
 19550   *     `md-input-container`
 19551   * @param {string=} md-input-name The name attribute given to the input element to be used with
 19552   *     FormController
 19553   * @param {string=} md-select-on-focus When present the inputs text will be automatically selected
 19554   *     on focus.
 19555   * @param {string=} md-input-id An ID to be added to the input element
 19556   * @param {number=} md-input-minlength The minimum length for the input's value for validation
 19557   * @param {number=} md-input-maxlength The maximum length for the input's value for validation
 19558   * @param {boolean=} md-select-on-match When set, autocomplete will automatically select exact
 19559   *     the item if the search text is an exact match
 19560   * @param {boolean=} md-match-case-insensitive When set and using `md-select-on-match`, autocomplete
 19561   *     will select on case-insensitive match
 19562   *
 19563   * @usage
 19564   * ### Basic Example
 19565   * <hljs lang="html">
 19566   *   <md-autocomplete
 19567   *       md-selected-item="selectedItem"
 19568   *       md-search-text="searchText"
 19569   *       md-items="item in getMatches(searchText)"
 19570   *       md-item-text="item.display">
 19571   *     <span md-highlight-text="searchText">{{item.display}}</span>
 19572   *   </md-autocomplete>
 19573   * </hljs>
 19574   *
 19575   * ### Example with "not found" message
 19576   * <hljs lang="html">
 19577   * <md-autocomplete
 19578   *     md-selected-item="selectedItem"
 19579   *     md-search-text="searchText"
 19580   *     md-items="item in getMatches(searchText)"
 19581   *     md-item-text="item.display">
 19582   *   <md-item-template>
 19583   *     <span md-highlight-text="searchText">{{item.display}}</span>
 19584   *   </md-item-template>
 19585   *   <md-not-found>
 19586   *     No matches found.
 19587   *   </md-not-found>
 19588   * </md-autocomplete>
 19589   * </hljs>
 19590   *
 19591   * In this example, our code utilizes `md-item-template` and `md-not-found` to specify the
 19592   *     different parts that make up our component.
 19593   *
 19594   * ### Example with validation
 19595   * <hljs lang="html">
 19596   * <form name="autocompleteForm">
 19597   *   <md-autocomplete
 19598   *       required
 19599   *       md-input-name="autocomplete"
 19600   *       md-selected-item="selectedItem"
 19601   *       md-search-text="searchText"
 19602   *       md-items="item in getMatches(searchText)"
 19603   *       md-item-text="item.display">
 19604   *     <md-item-template>
 19605   *       <span md-highlight-text="searchText">{{item.display}}</span>
 19606   *     </md-item-template>
 19607   *     <div ng-messages="autocompleteForm.autocomplete.$error">
 19608   *       <div ng-message="required">This field is required</div>
 19609   *     </div>
 19610   *   </md-autocomplete>
 19611   * </form>
 19612   * </hljs>
 19613   *
 19614   * In this example, our code utilizes `md-item-template` and `md-not-found` to specify the
 19615   *     different parts that make up our component.
 19616   */
 19617  
 19618  function MdAutocomplete () {
 19619  
 19620    return {
 19621      controller:   'MdAutocompleteCtrl',
 19622      controllerAs: '$mdAutocompleteCtrl',
 19623      scope:        {
 19624        inputName:        '@mdInputName',
 19625        inputMinlength:   '@mdInputMinlength',
 19626        inputMaxlength:   '@mdInputMaxlength',
 19627        searchText:       '=?mdSearchText',
 19628        selectedItem:     '=?mdSelectedItem',
 19629        itemsExpr:        '@mdItems',
 19630        itemText:         '&mdItemText',
 19631        placeholder:      '@placeholder',
 19632        noCache:          '=?mdNoCache',
 19633        selectOnMatch:    '=?mdSelectOnMatch',
 19634        matchInsensitive: '=?mdMatchCaseInsensitive',
 19635        itemChange:       '&?mdSelectedItemChange',
 19636        textChange:       '&?mdSearchTextChange',
 19637        minLength:        '=?mdMinLength',
 19638        delay:            '=?mdDelay',
 19639        autofocus:        '=?mdAutofocus',
 19640        floatingLabel:    '@?mdFloatingLabel',
 19641        autoselect:       '=?mdAutoselect',
 19642        menuClass:        '@?mdMenuClass',
 19643        inputId:          '@?mdInputId'
 19644      },
 19645      link: function(scope, element, attrs, controller) {
 19646        // Retrieve the state of using a md-not-found template by using our attribute, which will
 19647        // be added to the element in the template function.
 19648        controller.hasNotFound = !!element.attr('md-has-not-found');
 19649      },
 19650      template:     function (element, attr) {
 19651        var noItemsTemplate = getNoItemsTemplate(),
 19652            itemTemplate    = getItemTemplate(),
 19653            leftover        = element.html(),
 19654            tabindex        = attr.tabindex;
 19655  
 19656        // Set our attribute for the link function above which runs later.
 19657        // We will set an attribute, because otherwise the stored variables will be trashed when
 19658        // removing the element is hidden while retrieving the template. For example when using ngIf.
 19659        if (noItemsTemplate) element.attr('md-has-not-found', true);
 19660  
 19661        // Always set our tabindex of the autocomplete directive to -1, because our input
 19662        // will hold the actual tabindex.
 19663        element.attr('tabindex', '-1');
 19664  
 19665        return '\
 19666          <md-autocomplete-wrap\
 19667              layout="row"\
 19668              ng-class="{ \'md-whiteframe-z1\': !floatingLabel, \'md-menu-showing\': !$mdAutocompleteCtrl.hidden }"\
 19669              role="listbox">\
 19670            ' + getInputElement() + '\
 19671            <md-progress-linear\
 19672                class="' + (attr.mdFloatingLabel ? 'md-inline' : '') + '"\
 19673                ng-if="$mdAutocompleteCtrl.loadingIsVisible()"\
 19674                md-mode="indeterminate"></md-progress-linear>\
 19675            <md-virtual-repeat-container\
 19676                md-auto-shrink\
 19677                md-auto-shrink-min="1"\
 19678                ng-mouseenter="$mdAutocompleteCtrl.listEnter()"\
 19679                ng-mouseleave="$mdAutocompleteCtrl.listLeave()"\
 19680                ng-mouseup="$mdAutocompleteCtrl.mouseUp()"\
 19681                ng-hide="$mdAutocompleteCtrl.hidden"\
 19682                class="md-autocomplete-suggestions-container md-whiteframe-z1"\
 19683                ng-class="{ \'md-not-found\': $mdAutocompleteCtrl.notFoundVisible() }"\
 19684                role="presentation">\
 19685              <ul class="md-autocomplete-suggestions"\
 19686                  ng-class="::menuClass"\
 19687                  id="ul-{{$mdAutocompleteCtrl.id}}">\
 19688                <li md-virtual-repeat="item in $mdAutocompleteCtrl.matches"\
 19689                    ng-class="{ selected: $index === $mdAutocompleteCtrl.index }"\
 19690                    ng-click="$mdAutocompleteCtrl.select($index)"\
 19691                    md-extra-name="$mdAutocompleteCtrl.itemName">\
 19692                    ' + itemTemplate + '\
 19693                    </li>' + noItemsTemplate + '\
 19694              </ul>\
 19695            </md-virtual-repeat-container>\
 19696          </md-autocomplete-wrap>\
 19697          <aria-status\
 19698              class="md-visually-hidden"\
 19699              role="status"\
 19700              aria-live="assertive">\
 19701            <p ng-repeat="message in $mdAutocompleteCtrl.messages track by $index" ng-if="message">{{message}}</p>\
 19702          </aria-status>';
 19703  
 19704        function getItemTemplate() {
 19705          var templateTag = element.find('md-item-template').detach(),
 19706              html = templateTag.length ? templateTag.html() : element.html();
 19707          if (!templateTag.length) element.empty();
 19708          return '<md-autocomplete-parent-scope md-autocomplete-replace>' + html + '</md-autocomplete-parent-scope>';
 19709        }
 19710  
 19711        function getNoItemsTemplate() {
 19712          var templateTag = element.find('md-not-found').detach(),
 19713              template = templateTag.length ? templateTag.html() : '';
 19714          return template
 19715              ? '<li ng-if="$mdAutocompleteCtrl.notFoundVisible()"\
 19716                           md-autocomplete-parent-scope>' + template + '</li>'
 19717              : '';
 19718  
 19719        }
 19720  
 19721        function getInputElement () {
 19722          if (attr.mdFloatingLabel) {
 19723            return '\
 19724              <md-input-container flex ng-if="floatingLabel">\
 19725                <label>{{floatingLabel}}</label>\
 19726                <input type="search"\
 19727                    ' + (tabindex != null ? 'tabindex="' + tabindex + '"' : '') + '\
 19728                    id="{{ inputId || \'fl-input-\' + $mdAutocompleteCtrl.id }}"\
 19729                    name="{{inputName}}"\
 19730                    autocomplete="off"\
 19731                    ng-required="$mdAutocompleteCtrl.isRequired"\
 19732                    ng-readonly="$mdAutocompleteCtrl.isReadonly"\
 19733                    ng-minlength="inputMinlength"\
 19734                    ng-maxlength="inputMaxlength"\
 19735                    ng-disabled="$mdAutocompleteCtrl.isDisabled"\
 19736                    ng-model="$mdAutocompleteCtrl.scope.searchText"\
 19737                    ng-keydown="$mdAutocompleteCtrl.keydown($event)"\
 19738                    ng-blur="$mdAutocompleteCtrl.blur()"\
 19739                    ' + (attr.mdNoAsterisk != null ? 'md-no-asterisk="' + attr.mdNoAsterisk + '"' : '') + '\
 19740                    ng-focus="$mdAutocompleteCtrl.focus()"\
 19741                    aria-owns="ul-{{$mdAutocompleteCtrl.id}}"\
 19742                    ' + (attr.mdSelectOnFocus != null ? 'md-select-on-focus=""' : '') + '\
 19743                    aria-label="{{floatingLabel}}"\
 19744                    aria-autocomplete="list"\
 19745                    aria-haspopup="true"\
 19746                    aria-activedescendant=""\
 19747                    aria-expanded="{{!$mdAutocompleteCtrl.hidden}}"/>\
 19748                <div md-autocomplete-parent-scope md-autocomplete-replace>' + leftover + '</div>\
 19749              </md-input-container>';
 19750          } else {
 19751            return '\
 19752              <input flex type="search"\
 19753                  ' + (tabindex != null ? 'tabindex="' + tabindex + '"' : '') + '\
 19754                  id="{{ inputId || \'input-\' + $mdAutocompleteCtrl.id }}"\
 19755                  name="{{inputName}}"\
 19756                  ng-if="!floatingLabel"\
 19757                  autocomplete="off"\
 19758                  ng-required="$mdAutocompleteCtrl.isRequired"\
 19759                  ng-disabled="$mdAutocompleteCtrl.isDisabled"\
 19760                  ng-readonly="$mdAutocompleteCtrl.isReadonly"\
 19761                  ng-model="$mdAutocompleteCtrl.scope.searchText"\
 19762                  ng-keydown="$mdAutocompleteCtrl.keydown($event)"\
 19763                  ng-blur="$mdAutocompleteCtrl.blur()"\
 19764                  ng-focus="$mdAutocompleteCtrl.focus()"\
 19765                  placeholder="{{placeholder}}"\
 19766                  aria-owns="ul-{{$mdAutocompleteCtrl.id}}"\
 19767                  ' + (attr.mdSelectOnFocus != null ? 'md-select-on-focus=""' : '') + '\
 19768                  aria-label="{{placeholder}}"\
 19769                  aria-autocomplete="list"\
 19770                  aria-haspopup="true"\
 19771                  aria-activedescendant=""\
 19772                  aria-expanded="{{!$mdAutocompleteCtrl.hidden}}"/>\
 19773              <button\
 19774                  type="button"\
 19775                  tabindex="-1"\
 19776                  ng-if="$mdAutocompleteCtrl.scope.searchText && !$mdAutocompleteCtrl.isDisabled"\
 19777                  ng-click="$mdAutocompleteCtrl.clear()">\
 19778                <md-icon md-svg-icon="md-close"></md-icon>\
 19779                <span class="md-visually-hidden">Clear</span>\
 19780              </button>\
 19781                  ';
 19782          }
 19783        }
 19784      }
 19785    };
 19786  }
 19787  
 19788  })();
 19789  (function(){
 19790  "use strict";
 19791  
 19792  angular
 19793    .module('material.components.autocomplete')
 19794    .directive('mdAutocompleteParentScope', MdAutocompleteItemScopeDirective);
 19795  
 19796  function MdAutocompleteItemScopeDirective($compile, $mdUtil) {
 19797    return {
 19798      restrict: 'AE',
 19799      compile: compile,
 19800      terminal: true,
 19801      transclude: 'element'
 19802    };
 19803  
 19804    function compile(tElement, tAttr, transclude) {
 19805      return function postLink(scope, element, attr) {
 19806        var ctrl = scope.$mdAutocompleteCtrl;
 19807        var newScope = ctrl.parent.$new();
 19808        var itemName = ctrl.itemName;
 19809  
 19810        // Watch for changes to our scope's variables and copy them to the new scope
 19811        watchVariable('$index', '$index');
 19812        watchVariable('item', itemName);
 19813  
 19814        // Ensure that $digest calls on our scope trigger $digest on newScope.
 19815        connectScopes();
 19816  
 19817        // Link the element against newScope.
 19818        transclude(newScope, function(clone) {
 19819          element.after(clone);
 19820        });
 19821  
 19822        /**
 19823         * Creates a watcher for variables that are copied from the parent scope
 19824         * @param variable
 19825         * @param alias
 19826         */
 19827        function watchVariable(variable, alias) {
 19828          newScope[alias] = scope[variable];
 19829  
 19830          scope.$watch(variable, function(value) {
 19831            $mdUtil.nextTick(function() {
 19832              newScope[alias] = value;
 19833            });
 19834          });
 19835        }
 19836  
 19837        /**
 19838         * Creates watchers on scope and newScope that ensure that for any
 19839         * $digest of scope, newScope is also $digested.
 19840         */
 19841        function connectScopes() {
 19842          var scopeDigesting = false;
 19843          var newScopeDigesting = false;
 19844  
 19845          scope.$watch(function() {
 19846            if (newScopeDigesting || scopeDigesting) {
 19847              return;
 19848            }
 19849  
 19850            scopeDigesting = true;
 19851            scope.$$postDigest(function() {
 19852              if (!newScopeDigesting) {
 19853                newScope.$digest();
 19854              }
 19855  
 19856              scopeDigesting = newScopeDigesting = false;
 19857            });
 19858          });
 19859  
 19860          newScope.$watch(function() {
 19861            newScopeDigesting = true;
 19862          });
 19863        }
 19864      };
 19865    }
 19866  }
 19867  MdAutocompleteItemScopeDirective.$inject = ["$compile", "$mdUtil"];
 19868  })();
 19869  (function(){
 19870  "use strict";
 19871  
 19872  angular
 19873      .module('material.components.autocomplete')
 19874      .controller('MdHighlightCtrl', MdHighlightCtrl);
 19875  
 19876  function MdHighlightCtrl ($scope, $element, $attrs) {
 19877    this.init = init;
 19878  
 19879    function init (termExpr, unsafeTextExpr) {
 19880      var text = null,
 19881          regex = null,
 19882          flags = $attrs.mdHighlightFlags || '',
 19883          watcher = $scope.$watch(function($scope) {
 19884            return {
 19885              term: termExpr($scope),
 19886              unsafeText: unsafeTextExpr($scope)
 19887            };
 19888          }, function (state, prevState) {
 19889            if (text === null || state.unsafeText !== prevState.unsafeText) {
 19890              text = angular.element('<div>').text(state.unsafeText).html()
 19891            }
 19892            if (regex === null || state.term !== prevState.term) {
 19893              regex = getRegExp(state.term, flags);
 19894            }
 19895  
 19896            $element.html(text.replace(regex, '<span class="highlight">$&</span>'));
 19897          }, true);
 19898      $element.on('$destroy', watcher);
 19899    }
 19900  
 19901    function sanitize (term) {
 19902      return term && term.replace(/[\\\^\$\*\+\?\.\(\)\|\{}\[\]]/g, '\\$&');
 19903    }
 19904  
 19905    function getRegExp (text, flags) {
 19906      var str = '';
 19907      if (flags.indexOf('^') >= 1) str += '^';
 19908      str += text;
 19909      if (flags.indexOf('$') >= 1) str += '$';
 19910      return new RegExp(sanitize(str), flags.replace(/[\$\^]/g, ''));
 19911    }
 19912  }
 19913  MdHighlightCtrl.$inject = ["$scope", "$element", "$attrs"];
 19914  
 19915  })();
 19916  (function(){
 19917  "use strict";
 19918  
 19919  angular
 19920      .module('material.components.autocomplete')
 19921      .directive('mdHighlightText', MdHighlight);
 19922  
 19923  /**
 19924   * @ngdoc directive
 19925   * @name mdHighlightText
 19926   * @module material.components.autocomplete
 19927   *
 19928   * @description
 19929   * The `md-highlight-text` directive allows you to specify text that should be highlighted within
 19930   *     an element.  Highlighted text will be wrapped in `<span class="highlight"></span>` which can
 19931   *     be styled through CSS.  Please note that child elements may not be used with this directive.
 19932   *
 19933   * @param {string} md-highlight-text A model to be searched for
 19934   * @param {string=} md-highlight-flags A list of flags (loosely based on JavaScript RexExp flags).
 19935   * #### **Supported flags**:
 19936   * - `g`: Find all matches within the provided text
 19937   * - `i`: Ignore case when searching for matches
 19938   * - `$`: Only match if the text ends with the search term
 19939   * - `^`: Only match if the text begins with the search term
 19940   *
 19941   * @usage
 19942   * <hljs lang="html">
 19943   * <input placeholder="Enter a search term..." ng-model="searchTerm" type="text" />
 19944   * <ul>
 19945   *   <li ng-repeat="result in results" md-highlight-text="searchTerm">
 19946   *     {{result.text}}
 19947   *   </li>
 19948   * </ul>
 19949   * </hljs>
 19950   */
 19951  
 19952  function MdHighlight ($interpolate, $parse) {
 19953    return {
 19954      terminal: true,
 19955      controller: 'MdHighlightCtrl',
 19956      compile: function mdHighlightCompile(tElement, tAttr) {
 19957        var termExpr = $parse(tAttr.mdHighlightText);
 19958        var unsafeTextExpr = $interpolate(tElement.html());
 19959  
 19960        return function mdHighlightLink(scope, element, attr, ctrl) {
 19961          ctrl.init(termExpr, unsafeTextExpr);
 19962        };
 19963      }
 19964    };
 19965  }
 19966  MdHighlight.$inject = ["$interpolate", "$parse"];
 19967  
 19968  })();
 19969  (function(){
 19970  "use strict";
 19971  
 19972  angular
 19973      .module('material.components.chips')
 19974      .directive('mdChip', MdChip);
 19975  
 19976  /**
 19977   * @ngdoc directive
 19978   * @name mdChip
 19979   * @module material.components.chips
 19980   *
 19981   * @description
 19982   * `<md-chip>` is a component used within `<md-chips>` and is responsible for rendering individual
 19983   * chips.
 19984   *
 19985   *
 19986   * @usage
 19987   * <hljs lang="html">
 19988   *   <md-chip>{{$chip}}</md-chip>
 19989   * </hljs>
 19990   *
 19991   */
 19992  
 19993  // This hint text is hidden within a chip but used by screen readers to
 19994  // inform the user how they can interact with a chip.
 19995  var DELETE_HINT_TEMPLATE = '\
 19996      <span ng-if="!$mdChipsCtrl.readonly" class="md-visually-hidden">\
 19997        {{$mdChipsCtrl.deleteHint}}\
 19998      </span>';
 19999  
 20000  /**
 20001   * MDChip Directive Definition
 20002   *
 20003   * @param $mdTheming
 20004   * @param $mdInkRipple
 20005   * @ngInject
 20006   */
 20007  function MdChip($mdTheming, $mdUtil) {
 20008    var hintTemplate = $mdUtil.processTemplate(DELETE_HINT_TEMPLATE);
 20009  
 20010    return {
 20011      restrict: 'E',
 20012      require: '^?mdChips',
 20013      compile:  compile
 20014    };
 20015  
 20016    function compile(element, attr) {
 20017      // Append the delete template
 20018      element.append($mdUtil.processTemplate(hintTemplate));
 20019  
 20020      return function postLink(scope, element, attr, ctrl) {
 20021        element.addClass('md-chip');
 20022        $mdTheming(element);
 20023  
 20024        if (ctrl) angular.element(element[0].querySelector('.md-chip-content'))
 20025            .on('blur', function () {
 20026              ctrl.resetSelectedChip();
 20027              ctrl.$scope.$applyAsync();
 20028            });
 20029      };
 20030    }
 20031  }
 20032  MdChip.$inject = ["$mdTheming", "$mdUtil"];
 20033  
 20034  })();
 20035  (function(){
 20036  "use strict";
 20037  
 20038  angular
 20039      .module('material.components.chips')
 20040      .directive('mdChipRemove', MdChipRemove);
 20041  
 20042  /**
 20043   * @ngdoc directive
 20044   * @name mdChipRemove
 20045   * @module material.components.chips
 20046   *
 20047   * @description
 20048   * `<md-chip-remove>`
 20049   * Designates an element to be used as the delete button for a chip. This
 20050   * element is passed as a child of the `md-chips` element.
 20051   *
 20052   * @usage
 20053   * <hljs lang="html">
 20054   *   <md-chips><button md-chip-remove>DEL</button></md-chips>
 20055   * </hljs>
 20056   */
 20057  
 20058  
 20059  /**
 20060   * MdChipRemove Directive Definition.
 20061   * 
 20062   * @param $compile
 20063   * @param $timeout
 20064   * @returns {{restrict: string, require: string[], link: Function, scope: boolean}}
 20065   * @constructor
 20066   */
 20067  function MdChipRemove ($timeout) {
 20068    return {
 20069      restrict: 'A',
 20070      require: '^mdChips',
 20071      scope: false,
 20072      link: postLink
 20073    };
 20074  
 20075    function postLink(scope, element, attr, ctrl) {
 20076      element.on('click', function(event) {
 20077        scope.$apply(function() {
 20078          ctrl.removeChip(scope.$$replacedScope.$index);
 20079        });
 20080      });
 20081  
 20082      // Child elements aren't available until after a $timeout tick as they are hidden by an
 20083      // `ng-if`. see http://goo.gl/zIWfuw
 20084      $timeout(function() {
 20085        element.attr({ tabindex: -1, 'aria-hidden': true });
 20086        element.find('button').attr('tabindex', '-1');
 20087      });
 20088    }
 20089  }
 20090  MdChipRemove.$inject = ["$timeout"];
 20091  
 20092  })();
 20093  (function(){
 20094  "use strict";
 20095  
 20096  angular
 20097      .module('material.components.chips')
 20098      .directive('mdChipTransclude', MdChipTransclude);
 20099  
 20100  function MdChipTransclude ($compile) {
 20101    return {
 20102      restrict: 'EA',
 20103      terminal: true,
 20104      link: link,
 20105      scope: false
 20106    };
 20107    function link (scope, element, attr) {
 20108      var ctrl = scope.$parent.$mdChipsCtrl,
 20109          newScope = ctrl.parent.$new(false, ctrl.parent);
 20110      newScope.$$replacedScope = scope;
 20111      newScope.$chip = scope.$chip;
 20112      newScope.$index = scope.$index;
 20113      newScope.$mdChipsCtrl = ctrl;
 20114  
 20115      var newHtml = ctrl.$scope.$eval(attr.mdChipTransclude);
 20116  
 20117      element.html(newHtml);
 20118      $compile(element.contents())(newScope);
 20119    }
 20120  }
 20121  MdChipTransclude.$inject = ["$compile"];
 20122  
 20123  })();
 20124  (function(){
 20125  "use strict";
 20126  
 20127  angular
 20128      .module('material.components.chips')
 20129      .controller('MdChipsCtrl', MdChipsCtrl);
 20130  
 20131  /**
 20132   * Controller for the MdChips component. Responsible for adding to and
 20133   * removing from the list of chips, marking chips as selected, and binding to
 20134   * the models of various input components.
 20135   *
 20136   * @param $scope
 20137   * @param $mdConstant
 20138   * @param $log
 20139   * @param $element
 20140   * @param $mdUtil
 20141   * @constructor
 20142   */
 20143  function MdChipsCtrl ($scope, $mdConstant, $log, $element, $timeout, $mdUtil) {
 20144    /** @type {$timeout} **/
 20145    this.$timeout = $timeout;
 20146  
 20147    /** @type {Object} */
 20148    this.$mdConstant = $mdConstant;
 20149  
 20150    /** @type {angular.$scope} */
 20151    this.$scope = $scope;
 20152  
 20153    /** @type {angular.$scope} */
 20154    this.parent = $scope.$parent;
 20155  
 20156    /** @type {$log} */
 20157    this.$log = $log;
 20158  
 20159    /** @type {$element} */
 20160    this.$element = $element;
 20161  
 20162    /** @type {angular.NgModelController} */
 20163    this.ngModelCtrl = null;
 20164  
 20165    /** @type {angular.NgModelController} */
 20166    this.userInputNgModelCtrl = null;
 20167  
 20168    /** @type {Element} */
 20169    this.userInputElement = null;
 20170  
 20171    /** @type {Array.<Object>} */
 20172    this.items = [];
 20173  
 20174    /** @type {number} */
 20175    this.selectedChip = -1;
 20176  
 20177    /** @type {boolean} */
 20178    this.hasAutocomplete = false;
 20179  
 20180    /** @type {string} */
 20181    this.enableChipEdit = $mdUtil.parseAttributeBoolean(this.mdEnableChipEdit);
 20182  
 20183    /**
 20184     * Hidden hint text for how to delete a chip. Used to give context to screen readers.
 20185     * @type {string}
 20186     */
 20187    this.deleteHint = 'Press delete to remove this chip.';
 20188  
 20189    /**
 20190     * Hidden label for the delete button. Used to give context to screen readers.
 20191     * @type {string}
 20192     */
 20193    this.deleteButtonLabel = 'Remove';
 20194  
 20195    /**
 20196     * Model used by the input element.
 20197     * @type {string}
 20198     */
 20199    this.chipBuffer = '';
 20200  
 20201    /**
 20202     * Whether to use the onAppend expression to transform the chip buffer
 20203     * before appending it to the list.
 20204     * @type {boolean}
 20205     *
 20206     *
 20207     * @deprecated Will remove in 1.0.
 20208     */
 20209    this.useOnAppend = false;
 20210  
 20211    /**
 20212     * Whether to use the transformChip expression to transform the chip buffer
 20213     * before appending it to the list.
 20214     * @type {boolean}
 20215     */
 20216    this.useTransformChip = false;
 20217  
 20218    /**
 20219     * Whether to use the onAdd expression to notify of chip additions.
 20220     * @type {boolean}
 20221     */
 20222    this.useOnAdd = false;
 20223  
 20224    /**
 20225     * Whether to use the onRemove expression to notify of chip removals.
 20226     * @type {boolean}
 20227     */
 20228    this.useOnRemove = false;
 20229  
 20230    /**
 20231     * Whether to use the onSelect expression to notify the component's user
 20232     * after selecting a chip from the list.
 20233     * @type {boolean}
 20234     */
 20235    this.useOnSelect = false;
 20236  }
 20237  MdChipsCtrl.$inject = ["$scope", "$mdConstant", "$log", "$element", "$timeout", "$mdUtil"];
 20238  
 20239  /**
 20240   * Handles the keydown event on the input element: by default <enter> appends
 20241   * the buffer to the chip list, while backspace removes the last chip in the
 20242   * list if the current buffer is empty.
 20243   * @param event
 20244   */
 20245  MdChipsCtrl.prototype.inputKeydown = function(event) {
 20246    var chipBuffer = this.getChipBuffer();
 20247  
 20248    // If we have an autocomplete, and it handled the event, we have nothing to do
 20249    if (this.hasAutocomplete && event.isDefaultPrevented && event.isDefaultPrevented()) {
 20250      return;
 20251    }
 20252  
 20253    if (event.keyCode === this.$mdConstant.KEY_CODE.BACKSPACE) {
 20254      if (chipBuffer) return;
 20255      event.preventDefault();
 20256      event.stopPropagation();
 20257      if (this.items.length) this.selectAndFocusChipSafe(this.items.length - 1);
 20258      return;
 20259    }
 20260  
 20261    // By default <enter> appends the buffer to the chip list.
 20262    if (!this.separatorKeys || this.separatorKeys.length < 1) {
 20263      this.separatorKeys = [this.$mdConstant.KEY_CODE.ENTER];
 20264    }
 20265  
 20266    // Support additional separator key codes in an array of `md-separator-keys`.
 20267    if (this.separatorKeys.indexOf(event.keyCode) !== -1) {
 20268      if ((this.hasAutocomplete && this.requireMatch) || !chipBuffer) return;
 20269      event.preventDefault();
 20270  
 20271      // Only append the chip and reset the chip buffer if the max chips limit isn't reached.
 20272      if (this.hasMaxChipsReached()) return;
 20273  
 20274      this.appendChip(chipBuffer.trim());
 20275      this.resetChipBuffer();
 20276    }
 20277  };
 20278  
 20279  
 20280  /**
 20281   * Updates the content of the chip at given index
 20282   * @param chipIndex
 20283   * @param chipContents
 20284   */
 20285  MdChipsCtrl.prototype.updateChipContents = function(chipIndex, chipContents){
 20286    if(chipIndex >= 0 && chipIndex < this.items.length) {
 20287      this.items[chipIndex] = chipContents;
 20288      this.ngModelCtrl.$setDirty();
 20289    }
 20290  };
 20291  
 20292  
 20293  /**
 20294   * Returns true if a chip is currently being edited. False otherwise.
 20295   * @return {boolean}
 20296   */
 20297  MdChipsCtrl.prototype.isEditingChip = function(){
 20298    return !!this.$element[0].getElementsByClassName('_md-chip-editing').length;
 20299  };
 20300  
 20301  
 20302  /**
 20303   * Handles the keydown event on the chip elements: backspace removes the selected chip, arrow
 20304   * keys switch which chips is active
 20305   * @param event
 20306   */
 20307  MdChipsCtrl.prototype.chipKeydown = function (event) {
 20308    if (this.getChipBuffer()) return;
 20309    if (this.isEditingChip()) return;
 20310    
 20311    switch (event.keyCode) {
 20312      case this.$mdConstant.KEY_CODE.BACKSPACE:
 20313      case this.$mdConstant.KEY_CODE.DELETE:
 20314        if (this.selectedChip < 0) return;
 20315        event.preventDefault();
 20316        this.removeAndSelectAdjacentChip(this.selectedChip);
 20317        break;
 20318      case this.$mdConstant.KEY_CODE.LEFT_ARROW:
 20319        event.preventDefault();
 20320        if (this.selectedChip < 0) this.selectedChip = this.items.length;
 20321        if (this.items.length) this.selectAndFocusChipSafe(this.selectedChip - 1);
 20322        break;
 20323      case this.$mdConstant.KEY_CODE.RIGHT_ARROW:
 20324        event.preventDefault();
 20325        this.selectAndFocusChipSafe(this.selectedChip + 1);
 20326        break;
 20327      case this.$mdConstant.KEY_CODE.ESCAPE:
 20328      case this.$mdConstant.KEY_CODE.TAB:
 20329        if (this.selectedChip < 0) return;
 20330        event.preventDefault();
 20331        this.onFocus();
 20332        break;
 20333    }
 20334  };
 20335  
 20336  /**
 20337   * Get the input's placeholder - uses `placeholder` when list is empty and `secondary-placeholder`
 20338   * when the list is non-empty. If `secondary-placeholder` is not provided, `placeholder` is used
 20339   * always.
 20340   */
 20341  MdChipsCtrl.prototype.getPlaceholder = function() {
 20342    // Allow `secondary-placeholder` to be blank.
 20343    var useSecondary = (this.items.length &&
 20344        (this.secondaryPlaceholder == '' || this.secondaryPlaceholder));
 20345    return useSecondary ? this.secondaryPlaceholder : this.placeholder;
 20346  };
 20347  
 20348  /**
 20349   * Removes chip at {@code index} and selects the adjacent chip.
 20350   * @param index
 20351   */
 20352  MdChipsCtrl.prototype.removeAndSelectAdjacentChip = function(index) {
 20353    var selIndex = this.getAdjacentChipIndex(index);
 20354    this.removeChip(index);
 20355    this.$timeout(angular.bind(this, function () {
 20356        this.selectAndFocusChipSafe(selIndex);
 20357    }));
 20358  };
 20359  
 20360  /**
 20361   * Sets the selected chip index to -1.
 20362   */
 20363  MdChipsCtrl.prototype.resetSelectedChip = function() {
 20364    this.selectedChip = -1;
 20365  };
 20366  
 20367  /**
 20368   * Gets the index of an adjacent chip to select after deletion. Adjacency is
 20369   * determined as the next chip in the list, unless the target chip is the
 20370   * last in the list, then it is the chip immediately preceding the target. If
 20371   * there is only one item in the list, -1 is returned (select none).
 20372   * The number returned is the index to select AFTER the target has been
 20373   * removed.
 20374   * If the current chip is not selected, then -1 is returned to select none.
 20375   */
 20376  MdChipsCtrl.prototype.getAdjacentChipIndex = function(index) {
 20377    var len = this.items.length - 1;
 20378    return (len == 0) ? -1 :
 20379        (index == len) ? index -1 : index;
 20380  };
 20381  
 20382  /**
 20383   * Append the contents of the buffer to the chip list. This method will first
 20384   * call out to the md-transform-chip method, if provided.
 20385   *
 20386   * @param newChip
 20387   */
 20388  MdChipsCtrl.prototype.appendChip = function(newChip) {
 20389    if (this.useTransformChip && this.transformChip) {
 20390      var transformedChip = this.transformChip({'$chip': newChip});
 20391  
 20392      // Check to make sure the chip is defined before assigning it, otherwise, we'll just assume
 20393      // they want the string version.
 20394      if (angular.isDefined(transformedChip)) {
 20395        newChip = transformedChip;
 20396      }
 20397    }
 20398  
 20399    // If items contains an identical object to newChip, do not append
 20400    if (angular.isObject(newChip)){
 20401      var identical = this.items.some(function(item){
 20402        return angular.equals(newChip, item);
 20403      });
 20404      if (identical) return;
 20405    }
 20406  
 20407    // Check for a null (but not undefined), or existing chip and cancel appending
 20408    if (newChip == null || this.items.indexOf(newChip) + 1) return;
 20409  
 20410    // Append the new chip onto our list
 20411    var index = this.items.push(newChip);
 20412  
 20413    // Update model validation
 20414    this.ngModelCtrl.$setDirty();
 20415    this.validateModel();
 20416  
 20417    // If they provide the md-on-add attribute, notify them of the chip addition
 20418    if (this.useOnAdd && this.onAdd) {
 20419      this.onAdd({ '$chip': newChip, '$index': index });
 20420    }
 20421  };
 20422  
 20423  /**
 20424   * Sets whether to use the md-on-append expression. This expression is
 20425   * bound to scope and controller in {@code MdChipsDirective} as
 20426   * {@code onAppend}. Due to the nature of directive scope bindings, the
 20427   * controller cannot know on its own/from the scope whether an expression was
 20428   * actually provided.
 20429   *
 20430   * @deprecated
 20431   *
 20432   * TODO: Remove deprecated md-on-append functionality in 1.0
 20433   */
 20434  MdChipsCtrl.prototype.useOnAppendExpression = function() {
 20435    this.$log.warn("md-on-append is deprecated; please use md-transform-chip or md-on-add instead");
 20436    if (!this.useTransformChip || !this.transformChip) {
 20437      this.useTransformChip = true;
 20438      this.transformChip = this.onAppend;
 20439    }
 20440  };
 20441  
 20442  /**
 20443   * Sets whether to use the md-transform-chip expression. This expression is
 20444   * bound to scope and controller in {@code MdChipsDirective} as
 20445   * {@code transformChip}. Due to the nature of directive scope bindings, the
 20446   * controller cannot know on its own/from the scope whether an expression was
 20447   * actually provided.
 20448   */
 20449  MdChipsCtrl.prototype.useTransformChipExpression = function() {
 20450    this.useTransformChip = true;
 20451  };
 20452  
 20453  /**
 20454   * Sets whether to use the md-on-add expression. This expression is
 20455   * bound to scope and controller in {@code MdChipsDirective} as
 20456   * {@code onAdd}. Due to the nature of directive scope bindings, the
 20457   * controller cannot know on its own/from the scope whether an expression was
 20458   * actually provided.
 20459   */
 20460  MdChipsCtrl.prototype.useOnAddExpression = function() {
 20461    this.useOnAdd = true;
 20462  };
 20463  
 20464  /**
 20465   * Sets whether to use the md-on-remove expression. This expression is
 20466   * bound to scope and controller in {@code MdChipsDirective} as
 20467   * {@code onRemove}. Due to the nature of directive scope bindings, the
 20468   * controller cannot know on its own/from the scope whether an expression was
 20469   * actually provided.
 20470   */
 20471  MdChipsCtrl.prototype.useOnRemoveExpression = function() {
 20472    this.useOnRemove = true;
 20473  };
 20474  
 20475  /*
 20476   * Sets whether to use the md-on-select expression. This expression is
 20477   * bound to scope and controller in {@code MdChipsDirective} as
 20478   * {@code onSelect}. Due to the nature of directive scope bindings, the
 20479   * controller cannot know on its own/from the scope whether an expression was
 20480   * actually provided.
 20481   */
 20482  MdChipsCtrl.prototype.useOnSelectExpression = function() {
 20483    this.useOnSelect = true;
 20484  };
 20485  
 20486  /**
 20487   * Gets the input buffer. The input buffer can be the model bound to the
 20488   * default input item {@code this.chipBuffer}, the {@code selectedItem}
 20489   * model of an {@code md-autocomplete}, or, through some magic, the model
 20490   * bound to any inpput or text area element found within a
 20491   * {@code md-input-container} element.
 20492   * @return {Object|string}
 20493   */
 20494  MdChipsCtrl.prototype.getChipBuffer = function() {
 20495    return !this.userInputElement ? this.chipBuffer :
 20496        this.userInputNgModelCtrl ? this.userInputNgModelCtrl.$viewValue :
 20497            this.userInputElement[0].value;
 20498  };
 20499  
 20500  /**
 20501   * Resets the input buffer for either the internal input or user provided input element.
 20502   */
 20503  MdChipsCtrl.prototype.resetChipBuffer = function() {
 20504    if (this.userInputElement) {
 20505      if (this.userInputNgModelCtrl) {
 20506        this.userInputNgModelCtrl.$setViewValue('');
 20507        this.userInputNgModelCtrl.$render();
 20508      } else {
 20509        this.userInputElement[0].value = '';
 20510      }
 20511    } else {
 20512      this.chipBuffer = '';
 20513    }
 20514  };
 20515  
 20516  MdChipsCtrl.prototype.hasMaxChipsReached = function() {
 20517    if (angular.isString(this.maxChips)) this.maxChips = parseInt(this.maxChips, 10) || 0;
 20518  
 20519    return this.maxChips > 0 && this.items.length >= this.maxChips;
 20520  };
 20521  
 20522  /**
 20523   * Updates the validity properties for the ngModel.
 20524   */
 20525  MdChipsCtrl.prototype.validateModel = function() {
 20526    this.ngModelCtrl.$setValidity('md-max-chips', !this.hasMaxChipsReached());
 20527  };
 20528  
 20529  /**
 20530   * Removes the chip at the given index.
 20531   * @param index
 20532   */
 20533  MdChipsCtrl.prototype.removeChip = function(index) {
 20534    var removed = this.items.splice(index, 1);
 20535  
 20536    // Update model validation
 20537    this.ngModelCtrl.$setDirty();
 20538    this.validateModel();
 20539  
 20540    if (removed && removed.length && this.useOnRemove && this.onRemove) {
 20541      this.onRemove({ '$chip': removed[0], '$index': index });
 20542    }
 20543  };
 20544  
 20545  MdChipsCtrl.prototype.removeChipAndFocusInput = function (index) {
 20546    this.removeChip(index);
 20547    this.onFocus();
 20548  };
 20549  /**
 20550   * Selects the chip at `index`,
 20551   * @param index
 20552   */
 20553  MdChipsCtrl.prototype.selectAndFocusChipSafe = function(index) {
 20554    if (!this.items.length) {
 20555      this.selectChip(-1);
 20556      this.onFocus();
 20557      return;
 20558    }
 20559    if (index === this.items.length) return this.onFocus();
 20560    index = Math.max(index, 0);
 20561    index = Math.min(index, this.items.length - 1);
 20562    this.selectChip(index);
 20563    this.focusChip(index);
 20564  };
 20565  
 20566  /**
 20567   * Marks the chip at the given index as selected.
 20568   * @param index
 20569   */
 20570  MdChipsCtrl.prototype.selectChip = function(index) {
 20571    if (index >= -1 && index <= this.items.length) {
 20572      this.selectedChip = index;
 20573  
 20574      // Fire the onSelect if provided
 20575      if (this.useOnSelect && this.onSelect) {
 20576        this.onSelect({'$chip': this.items[this.selectedChip] });
 20577      }
 20578    } else {
 20579      this.$log.warn('Selected Chip index out of bounds; ignoring.');
 20580    }
 20581  };
 20582  
 20583  /**
 20584   * Selects the chip at `index` and gives it focus.
 20585   * @param index
 20586   */
 20587  MdChipsCtrl.prototype.selectAndFocusChip = function(index) {
 20588    this.selectChip(index);
 20589    if (index != -1) {
 20590      this.focusChip(index);
 20591    }
 20592  };
 20593  
 20594  /**
 20595   * Call `focus()` on the chip at `index`
 20596   */
 20597  MdChipsCtrl.prototype.focusChip = function(index) {
 20598    this.$element[0].querySelector('md-chip[index="' + index + '"] .md-chip-content').focus();
 20599  };
 20600  
 20601  /**
 20602   * Configures the required interactions with the ngModel Controller.
 20603   * Specifically, set {@code this.items} to the {@code NgModelCtrl#$viewVale}.
 20604   * @param ngModelCtrl
 20605   */
 20606  MdChipsCtrl.prototype.configureNgModel = function(ngModelCtrl) {
 20607    this.ngModelCtrl = ngModelCtrl;
 20608  
 20609    var self = this;
 20610    ngModelCtrl.$render = function() {
 20611      // model is updated. do something.
 20612      self.items = self.ngModelCtrl.$viewValue;
 20613    };
 20614  };
 20615  
 20616  MdChipsCtrl.prototype.onFocus = function () {
 20617    var input = this.$element[0].querySelector('input');
 20618    input && input.focus();
 20619    this.resetSelectedChip();
 20620  };
 20621  
 20622  MdChipsCtrl.prototype.onInputFocus = function () {
 20623    this.inputHasFocus = true;
 20624    this.resetSelectedChip();
 20625  };
 20626  
 20627  MdChipsCtrl.prototype.onInputBlur = function () {
 20628    this.inputHasFocus = false;
 20629  };
 20630  
 20631  /**
 20632   * Configure event bindings on a user-provided input element.
 20633   * @param inputElement
 20634   */
 20635  MdChipsCtrl.prototype.configureUserInput = function(inputElement) {
 20636    this.userInputElement = inputElement;
 20637  
 20638    // Find the NgModelCtrl for the input element
 20639    var ngModelCtrl = inputElement.controller('ngModel');
 20640    // `.controller` will look in the parent as well.
 20641    if (ngModelCtrl != this.ngModelCtrl) {
 20642      this.userInputNgModelCtrl = ngModelCtrl;
 20643    }
 20644  
 20645    var scope = this.$scope;
 20646    var ctrl = this;
 20647  
 20648    // Run all of the events using evalAsync because a focus may fire a blur in the same digest loop
 20649    var scopeApplyFn = function(event, fn) {
 20650      scope.$evalAsync(angular.bind(ctrl, fn, event));
 20651    };
 20652  
 20653    // Bind to keydown and focus events of input
 20654    inputElement
 20655        .attr({ tabindex: 0 })
 20656        .on('keydown', function(event) { scopeApplyFn(event, ctrl.inputKeydown) })
 20657        .on('focus', function(event) { scopeApplyFn(event, ctrl.onInputFocus) })
 20658        .on('blur', function(event) { scopeApplyFn(event, ctrl.onInputBlur) })
 20659  };
 20660  
 20661  MdChipsCtrl.prototype.configureAutocomplete = function(ctrl) {
 20662    if ( ctrl ) {
 20663      this.hasAutocomplete = true;
 20664  
 20665      ctrl.registerSelectedItemWatcher(angular.bind(this, function (item) {
 20666        if (item) {
 20667          // Only append the chip and reset the chip buffer if the max chips limit isn't reached.
 20668          if (this.hasMaxChipsReached()) return;
 20669  
 20670          this.appendChip(item);
 20671          this.resetChipBuffer();
 20672        }
 20673      }));
 20674  
 20675      this.$element.find('input')
 20676          .on('focus',angular.bind(this, this.onInputFocus) )
 20677          .on('blur', angular.bind(this, this.onInputBlur) );
 20678    }
 20679  };
 20680  
 20681  MdChipsCtrl.prototype.hasFocus = function () {
 20682    return this.inputHasFocus || this.selectedChip >= 0;
 20683  };
 20684  
 20685  })();
 20686  (function(){
 20687  "use strict";
 20688  
 20689    angular
 20690        .module('material.components.chips')
 20691        .directive('mdChips', MdChips);
 20692  
 20693    /**
 20694     * @ngdoc directive
 20695     * @name mdChips
 20696     * @module material.components.chips
 20697     *
 20698     * @description
 20699     * `<md-chips>` is an input component for building lists of strings or objects. The list items are
 20700     * displayed as 'chips'. This component can make use of an `<input>` element or an 
 20701     * `<md-autocomplete>` element.
 20702     *
 20703     * ### Custom templates
 20704     * A custom template may be provided to render the content of each chip. This is achieved by
 20705     * specifying an `<md-chip-template>` element containing the custom content as a child of
 20706     * `<md-chips>`.
 20707     *
 20708     * Note: Any attributes on
 20709     * `<md-chip-template>` will be dropped as only the innerHTML is used for the chip template. The
 20710     * variables `$chip` and `$index` are available in the scope of `<md-chip-template>`, representing
 20711     * the chip object and its index in the list of chips, respectively.
 20712     * To override the chip delete control, include an element (ideally a button) with the attribute
 20713     * `md-chip-remove`. A click listener to remove the chip will be added automatically. The element
 20714     * is also placed as a sibling to the chip content (on which there are also click listeners) to
 20715     * avoid a nested ng-click situation.
 20716     *
 20717     * <h3> Pending Features </h3>
 20718     * <ul style="padding-left:20px;">
 20719     *
 20720     *   <ul>Style
 20721     *     <li>Colours for hover, press states (ripple?).</li>
 20722     *   </ul>
 20723     *
 20724     *   <ul>Validation
 20725     *     <li>allow a validation callback</li>
 20726     *     <li>highlighting style for invalid chips</li>
 20727     *   </ul>
 20728     *
 20729     *   <ul>Item mutation
 20730     *     <li>Support `
 20731     *       <md-chip-edit>` template, show/hide the edit element on tap/click? double tap/double
 20732     *       click?
 20733     *     </li>
 20734     *   </ul>
 20735     *
 20736     *   <ul>Truncation and Disambiguation (?)
 20737     *     <li>Truncate chip text where possible, but do not truncate entries such that two are
 20738     *     indistinguishable.</li>
 20739     *   </ul>
 20740     *
 20741     *   <ul>Drag and Drop
 20742     *     <li>Drag and drop chips between related `<md-chips>` elements.
 20743     *     </li>
 20744     *   </ul>
 20745     * </ul>
 20746     *
 20747     *  <span style="font-size:.8em;text-align:center">
 20748     *    Warning: This component is a WORK IN PROGRESS. If you use it now,
 20749     *    it will probably break on you in the future.
 20750     *  </span>
 20751     *
 20752     * @param {string=|object=} ng-model A model to bind the list of items to
 20753     * @param {string=} placeholder Placeholder text that will be forwarded to the input.
 20754     * @param {string=} secondary-placeholder Placeholder text that will be forwarded to the input,
 20755     *    displayed when there is at least one item in the list
 20756     * @param {boolean=} readonly Disables list manipulation (deleting or adding list items), hiding
 20757     *    the input and delete buttons
 20758     * @param {number=} md-max-chips The maximum number of chips allowed to add through user input.
 20759     *    <br/><br/>The validation property `md-max-chips` can be used when the max chips
 20760     *    amount is reached.
 20761     * @param {expression} md-transform-chip An expression of form `myFunction($chip)` that when called
 20762     *    expects one of the following return values:
 20763     *    - an object representing the `$chip` input string
 20764     *    - `undefined` to simply add the `$chip` input string, or
 20765     *    - `null` to prevent the chip from being appended
 20766     * @param {expression=} md-on-add An expression which will be called when a chip has been
 20767     *    added.
 20768     * @param {expression=} md-on-remove An expression which will be called when a chip has been
 20769     *    removed.
 20770     * @param {expression=} md-on-select An expression which will be called when a chip is selected.
 20771     * @param {boolean} md-require-match If true, and the chips template contains an autocomplete,
 20772     *    only allow selection of pre-defined chips (i.e. you cannot add new ones).
 20773     * @param {string=} delete-hint A string read by screen readers instructing users that pressing
 20774     *    the delete key will remove the chip.
 20775     * @param {string=} delete-button-label A label for the delete button. Also hidden and read by
 20776     *    screen readers.
 20777     * @param {expression=} md-separator-keys An array of key codes used to separate chips.
 20778     *
 20779     * @usage
 20780     * <hljs lang="html">
 20781     *   <md-chips
 20782     *       ng-model="myItems"
 20783     *       placeholder="Add an item"
 20784     *       readonly="isReadOnly">
 20785     *   </md-chips>
 20786     * </hljs>
 20787     *
 20788     * <h3>Validation</h3>
 20789     * When using [ngMessages](https://docs.angularjs.org/api/ngMessages), you can show errors based
 20790     * on our custom validators.
 20791     * <hljs lang="html">
 20792     *   <form name="userForm">
 20793     *     <md-chips
 20794     *       name="fruits"
 20795     *       ng-model="myItems"
 20796     *       placeholder="Add an item"
 20797     *       md-max-chips="5">
 20798     *     </md-chips>
 20799     *     <div ng-messages="userForm.fruits.$error" ng-if="userForm.$dirty">
 20800     *       <div ng-message="md-max-chips">You reached the maximum amount of chips</div>
 20801     *    </div>
 20802     *   </form>
 20803     * </hljs>
 20804     *
 20805     */
 20806  
 20807  
 20808    var MD_CHIPS_TEMPLATE = '\
 20809        <md-chips-wrap\
 20810            ng-if="!$mdChipsCtrl.readonly || $mdChipsCtrl.items.length > 0"\
 20811            ng-keydown="$mdChipsCtrl.chipKeydown($event)"\
 20812            ng-class="{ \'md-focused\': $mdChipsCtrl.hasFocus(), \'md-readonly\': !$mdChipsCtrl.ngModelCtrl }"\
 20813            class="md-chips">\
 20814          <md-chip ng-repeat="$chip in $mdChipsCtrl.items"\
 20815              index="{{$index}}"\
 20816              ng-class="{\'md-focused\': $mdChipsCtrl.selectedChip == $index, \'md-readonly\': $mdChipsCtrl.readonly}">\
 20817            <div class="md-chip-content"\
 20818                tabindex="-1"\
 20819                aria-hidden="true"\
 20820                ng-click="!$mdChipsCtrl.readonly && $mdChipsCtrl.focusChip($index)"\
 20821                ng-focus="!$mdChipsCtrl.readonly && $mdChipsCtrl.selectChip($index)"\
 20822                md-chip-transclude="$mdChipsCtrl.chipContentsTemplate"></div>\
 20823            <div ng-if="!$mdChipsCtrl.readonly"\
 20824                 class="md-chip-remove-container"\
 20825                 md-chip-transclude="$mdChipsCtrl.chipRemoveTemplate"></div>\
 20826          </md-chip>\
 20827          <div ng-if="!$mdChipsCtrl.readonly && $mdChipsCtrl.ngModelCtrl"\
 20828              class="md-chip-input-container"\
 20829              md-chip-transclude="$mdChipsCtrl.chipInputTemplate"></div>\
 20830          </div>\
 20831        </md-chips-wrap>';
 20832  
 20833    var CHIP_INPUT_TEMPLATE = '\
 20834          <input\
 20835              class="md-input"\
 20836              tabindex="0"\
 20837              placeholder="{{$mdChipsCtrl.getPlaceholder()}}"\
 20838              aria-label="{{$mdChipsCtrl.getPlaceholder()}}"\
 20839              ng-model="$mdChipsCtrl.chipBuffer"\
 20840              ng-focus="$mdChipsCtrl.onInputFocus()"\
 20841              ng-blur="$mdChipsCtrl.onInputBlur()"\
 20842              ng-trim="false"\
 20843              ng-keydown="$mdChipsCtrl.inputKeydown($event)">';
 20844  
 20845    var CHIP_DEFAULT_TEMPLATE = '\
 20846        <span>{{$chip}}</span>';
 20847  
 20848    var CHIP_REMOVE_TEMPLATE = '\
 20849        <button\
 20850            class="md-chip-remove"\
 20851            ng-if="!$mdChipsCtrl.readonly"\
 20852            ng-click="$mdChipsCtrl.removeChipAndFocusInput($$replacedScope.$index)"\
 20853            type="button"\
 20854            aria-hidden="true"\
 20855            tabindex="-1">\
 20856          <md-icon md-svg-icon="md-close"></md-icon>\
 20857          <span class="md-visually-hidden">\
 20858            {{$mdChipsCtrl.deleteButtonLabel}}\
 20859          </span>\
 20860        </button>';
 20861  
 20862    /**
 20863     * MDChips Directive Definition
 20864     */
 20865    function MdChips ($mdTheming, $mdUtil, $compile, $log, $timeout) {
 20866      // Run our templates through $mdUtil.processTemplate() to allow custom start/end symbols
 20867      var templates = getTemplates();
 20868  
 20869      return {
 20870        template: function(element, attrs) {
 20871          // Clone the element into an attribute. By prepending the attribute
 20872          // name with '$', Angular won't write it into the DOM. The cloned
 20873          // element propagates to the link function via the attrs argument,
 20874          // where various contained-elements can be consumed.
 20875          attrs['$mdUserTemplate'] = element.clone();
 20876          return templates.chips;
 20877        },
 20878        require: ['mdChips'],
 20879        restrict: 'E',
 20880        controller: 'MdChipsCtrl',
 20881        controllerAs: '$mdChipsCtrl',
 20882        bindToController: true,
 20883        compile: compile,
 20884        scope: {
 20885          readonly: '=readonly',
 20886          placeholder: '@',
 20887          secondaryPlaceholder: '@',
 20888          maxChips: '@mdMaxChips',
 20889          transformChip: '&mdTransformChip',
 20890          onAppend: '&mdOnAppend',
 20891          onAdd: '&mdOnAdd',
 20892          onRemove: '&mdOnRemove',
 20893          onSelect: '&mdOnSelect',
 20894          deleteHint: '@',
 20895          deleteButtonLabel: '@',
 20896          separatorKeys: '=?mdSeparatorKeys',
 20897          requireMatch: '=?mdRequireMatch'
 20898        }
 20899      };
 20900  
 20901      /**
 20902       * Builds the final template for `md-chips` and returns the postLink function.
 20903       *
 20904       * Building the template involves 3 key components:
 20905       * static chips
 20906       * chip template
 20907       * input control
 20908       *
 20909       * If no `ng-model` is provided, only the static chip work needs to be done.
 20910       *
 20911       * If no user-passed `md-chip-template` exists, the default template is used. This resulting
 20912       * template is appended to the chip content element.
 20913       *
 20914       * The remove button may be overridden by passing an element with an md-chip-remove attribute.
 20915       *
 20916       * If an `input` or `md-autocomplete` element is provided by the caller, it is set aside for
 20917       * transclusion later. The transclusion happens in `postLink` as the parent scope is required.
 20918       * If no user input is provided, a default one is appended to the input container node in the
 20919       * template.
 20920       *
 20921       * Static Chips (i.e. `md-chip` elements passed from the caller) are gathered and set aside for
 20922       * transclusion in the `postLink` function.
 20923       *
 20924       *
 20925       * @param element
 20926       * @param attr
 20927       * @returns {Function}
 20928       */
 20929      function compile(element, attr) {
 20930        // Grab the user template from attr and reset the attribute to null.
 20931        var userTemplate = attr['$mdUserTemplate'];
 20932        attr['$mdUserTemplate'] = null;
 20933  
 20934        // Set the chip remove, chip contents and chip input templates. The link function will put
 20935        // them on the scope for transclusion later.
 20936        var chipRemoveTemplate   = getTemplateByQuery('md-chips>*[md-chip-remove]') || templates.remove,
 20937            chipContentsTemplate = getTemplateByQuery('md-chips>md-chip-template') || templates.default,
 20938            chipInputTemplate    = getTemplateByQuery('md-chips>md-autocomplete')
 20939                || getTemplateByQuery('md-chips>input')
 20940                || templates.input,
 20941            staticChips = userTemplate.find('md-chip');
 20942  
 20943        // Warn of malformed template. See #2545
 20944        if (userTemplate[0].querySelector('md-chip-template>*[md-chip-remove]')) {
 20945          $log.warn('invalid placement of md-chip-remove within md-chip-template.');
 20946        }
 20947  
 20948        function getTemplateByQuery (query) {
 20949          if (!attr.ngModel) return;
 20950          var element = userTemplate[0].querySelector(query);
 20951          return element && element.outerHTML;
 20952        }
 20953  
 20954        /**
 20955         * Configures controller and transcludes.
 20956         */
 20957        return function postLink(scope, element, attrs, controllers) {
 20958          $mdUtil.initOptionalProperties(scope, attr);
 20959  
 20960          $mdTheming(element);
 20961          var mdChipsCtrl = controllers[0];
 20962          mdChipsCtrl.chipContentsTemplate = chipContentsTemplate;
 20963          mdChipsCtrl.chipRemoveTemplate   = chipRemoveTemplate;
 20964          mdChipsCtrl.chipInputTemplate    = chipInputTemplate;
 20965  
 20966          element
 20967              .attr({ 'aria-hidden': true, tabindex: -1 })
 20968              .on('focus', function () { mdChipsCtrl.onFocus(); });
 20969  
 20970          if (attr.ngModel) {
 20971            mdChipsCtrl.configureNgModel(element.controller('ngModel'));
 20972  
 20973            // If an `md-transform-chip` attribute was set, tell the controller to use the expression
 20974            // before appending chips.
 20975            if (attrs.mdTransformChip) mdChipsCtrl.useTransformChipExpression();
 20976  
 20977            // If an `md-on-append` attribute was set, tell the controller to use the expression
 20978            // when appending chips.
 20979            //
 20980            // DEPRECATED: Will remove in official 1.0 release
 20981            if (attrs.mdOnAppend) mdChipsCtrl.useOnAppendExpression();
 20982  
 20983            // If an `md-on-add` attribute was set, tell the controller to use the expression
 20984            // when adding chips.
 20985            if (attrs.mdOnAdd) mdChipsCtrl.useOnAddExpression();
 20986  
 20987            // If an `md-on-remove` attribute was set, tell the controller to use the expression
 20988            // when removing chips.
 20989            if (attrs.mdOnRemove) mdChipsCtrl.useOnRemoveExpression();
 20990  
 20991            // If an `md-on-select` attribute was set, tell the controller to use the expression
 20992            // when selecting chips.
 20993            if (attrs.mdOnSelect) mdChipsCtrl.useOnSelectExpression();
 20994  
 20995            // The md-autocomplete and input elements won't be compiled until after this directive
 20996            // is complete (due to their nested nature). Wait a tick before looking for them to
 20997            // configure the controller.
 20998            if (chipInputTemplate != templates.input) {
 20999              // The autocomplete will not appear until the readonly attribute is not true (i.e.
 21000              // false or undefined), so we have to watch the readonly and then on the next tick
 21001              // after the chip transclusion has run, we can configure the autocomplete and user
 21002              // input.
 21003              scope.$watch('$mdChipsCtrl.readonly', function(readonly) {
 21004                if (!readonly) {
 21005                  $mdUtil.nextTick(function(){
 21006                    if (chipInputTemplate.indexOf('<md-autocomplete') === 0)
 21007                      mdChipsCtrl
 21008                          .configureAutocomplete(element.find('md-autocomplete')
 21009                              .controller('mdAutocomplete'));
 21010                    mdChipsCtrl.configureUserInput(element.find('input'));
 21011                  });
 21012                }
 21013              });
 21014            }
 21015  
 21016            // At the next tick, if we find an input, make sure it has the md-input class
 21017            $mdUtil.nextTick(function() {
 21018              var input = element.find('input');
 21019  
 21020              input && input.toggleClass('md-input', true);
 21021            });
 21022          }
 21023  
 21024          // Compile with the parent's scope and prepend any static chips to the wrapper.
 21025          if (staticChips.length > 0) {
 21026            var compiledStaticChips = $compile(staticChips.clone())(scope.$parent);
 21027            $timeout(function() { element.find('md-chips-wrap').prepend(compiledStaticChips); });
 21028          }
 21029        };
 21030      }
 21031  
 21032      function getTemplates() {
 21033        return {
 21034          chips: $mdUtil.processTemplate(MD_CHIPS_TEMPLATE),
 21035          input: $mdUtil.processTemplate(CHIP_INPUT_TEMPLATE),
 21036          default: $mdUtil.processTemplate(CHIP_DEFAULT_TEMPLATE),
 21037          remove: $mdUtil.processTemplate(CHIP_REMOVE_TEMPLATE)
 21038        };
 21039      }
 21040    }
 21041    MdChips.$inject = ["$mdTheming", "$mdUtil", "$compile", "$log", "$timeout"];
 21042  
 21043  })();
 21044  (function(){
 21045  "use strict";
 21046  
 21047  angular
 21048      .module('material.components.chips')
 21049      .controller('MdContactChipsCtrl', MdContactChipsCtrl);
 21050  
 21051  
 21052  
 21053  /**
 21054   * Controller for the MdContactChips component
 21055   * @constructor
 21056   */
 21057  function MdContactChipsCtrl () {
 21058    /** @type {Object} */
 21059    this.selectedItem = null;
 21060  
 21061    /** @type {string} */
 21062    this.searchText = '';
 21063  }
 21064  
 21065  
 21066  MdContactChipsCtrl.prototype.queryContact = function(searchText) {
 21067    var results = this.contactQuery({'$query': searchText});
 21068    return this.filterSelected ?
 21069        results.filter(angular.bind(this, this.filterSelectedContacts)) : results;
 21070  };
 21071  
 21072  
 21073  MdContactChipsCtrl.prototype.itemName = function(item) {
 21074    return item[this.contactName];
 21075  };
 21076  
 21077  
 21078  MdContactChipsCtrl.prototype.filterSelectedContacts = function(contact) {
 21079    return this.contacts.indexOf(contact) == -1;
 21080  };
 21081  
 21082  })();
 21083  (function(){
 21084  "use strict";
 21085  
 21086  angular
 21087    .module('material.components.chips')
 21088    .directive('mdContactChips', MdContactChips);
 21089  
 21090  /**
 21091   * @ngdoc directive
 21092   * @name mdContactChips
 21093   * @module material.components.chips
 21094   *
 21095   * @description
 21096   * `<md-contact-chips>` is an input component based on `md-chips` and makes use of an
 21097   * `md-autocomplete` element. The component allows the caller to supply a query expression which
 21098   * returns  a list of possible contacts. The user can select one of these and add it to the list of
 21099   * chips.
 21100   *
 21101   * You may also use the `md-highlight-text` directive along with its parameters to control the
 21102   * appearance of the matched text inside of the contacts' autocomplete popup.
 21103   *
 21104   * @param {string=|object=} ng-model A model to bind the list of items to
 21105   * @param {string=} placeholder Placeholder text that will be forwarded to the input.
 21106   * @param {string=} secondary-placeholder Placeholder text that will be forwarded to the input,
 21107   *    displayed when there is at least on item in the list
 21108   * @param {expression} md-contacts An expression expected to return contacts matching the search
 21109   *    test, `$query`. If this expression involves a promise, a loading bar is displayed while
 21110   *    waiting for it to resolve.
 21111   * @param {string} md-contact-name The field name of the contact object representing the
 21112   *    contact's name.
 21113   * @param {string} md-contact-email The field name of the contact object representing the
 21114   *    contact's email address.
 21115   * @param {string} md-contact-image The field name of the contact object representing the
 21116   *    contact's image.
 21117   *
 21118   *
 21119   * @param {expression=} filter-selected Whether to filter selected contacts from the list of
 21120   *    suggestions shown in the autocomplete. This attribute has been removed but may come back.
 21121   *
 21122   *
 21123   *
 21124   * @usage
 21125   * <hljs lang="html">
 21126   *   <md-contact-chips
 21127   *       ng-model="ctrl.contacts"
 21128   *       md-contacts="ctrl.querySearch($query)"
 21129   *       md-contact-name="name"
 21130   *       md-contact-image="image"
 21131   *       md-contact-email="email"
 21132   *       placeholder="To">
 21133   *   </md-contact-chips>
 21134   * </hljs>
 21135   *
 21136   */
 21137  
 21138  
 21139  var MD_CONTACT_CHIPS_TEMPLATE = '\
 21140        <md-chips class="md-contact-chips"\
 21141            ng-model="$mdContactChipsCtrl.contacts"\
 21142            md-require-match="$mdContactChipsCtrl.requireMatch"\
 21143            md-autocomplete-snap>\
 21144            <md-autocomplete\
 21145                md-menu-class="md-contact-chips-suggestions"\
 21146                md-selected-item="$mdContactChipsCtrl.selectedItem"\
 21147                md-search-text="$mdContactChipsCtrl.searchText"\
 21148                md-items="item in $mdContactChipsCtrl.queryContact($mdContactChipsCtrl.searchText)"\
 21149                md-item-text="$mdContactChipsCtrl.itemName(item)"\
 21150                md-no-cache="true"\
 21151                md-autoselect\
 21152                placeholder="{{$mdContactChipsCtrl.contacts.length == 0 ?\
 21153                    $mdContactChipsCtrl.placeholder : $mdContactChipsCtrl.secondaryPlaceholder}}">\
 21154              <div class="md-contact-suggestion">\
 21155                <img \
 21156                    ng-src="{{item[$mdContactChipsCtrl.contactImage]}}"\
 21157                    alt="{{item[$mdContactChipsCtrl.contactName]}}"\
 21158                    ng-if="item[$mdContactChipsCtrl.contactImage]" />\
 21159                <span class="md-contact-name" md-highlight-text="$mdContactChipsCtrl.searchText"\
 21160                      md-highlight-flags="{{$mdContactChipsCtrl.highlightFlags}}">\
 21161                  {{item[$mdContactChipsCtrl.contactName]}}\
 21162                </span>\
 21163                <span class="md-contact-email" >{{item[$mdContactChipsCtrl.contactEmail]}}</span>\
 21164              </div>\
 21165            </md-autocomplete>\
 21166            <md-chip-template>\
 21167              <div class="md-contact-avatar">\
 21168                <img \
 21169                    ng-src="{{$chip[$mdContactChipsCtrl.contactImage]}}"\
 21170                    alt="{{$chip[$mdContactChipsCtrl.contactName]}}"\
 21171                    ng-if="$chip[$mdContactChipsCtrl.contactImage]" />\
 21172              </div>\
 21173              <div class="md-contact-name">\
 21174                {{$chip[$mdContactChipsCtrl.contactName]}}\
 21175              </div>\
 21176            </md-chip-template>\
 21177        </md-chips>';
 21178  
 21179  
 21180  /**
 21181   * MDContactChips Directive Definition
 21182   *
 21183   * @param $mdTheming
 21184   * @returns {*}
 21185   * @ngInject
 21186   */
 21187  function MdContactChips($mdTheming, $mdUtil) {
 21188    return {
 21189      template: function(element, attrs) {
 21190        return MD_CONTACT_CHIPS_TEMPLATE;
 21191      },
 21192      restrict: 'E',
 21193      controller: 'MdContactChipsCtrl',
 21194      controllerAs: '$mdContactChipsCtrl',
 21195      bindToController: true,
 21196      compile: compile,
 21197      scope: {
 21198        contactQuery: '&mdContacts',
 21199        placeholder: '@',
 21200        secondaryPlaceholder: '@',
 21201        contactName: '@mdContactName',
 21202        contactImage: '@mdContactImage',
 21203        contactEmail: '@mdContactEmail',
 21204        contacts: '=ngModel',
 21205        requireMatch: '=?mdRequireMatch',
 21206        highlightFlags: '@?mdHighlightFlags'
 21207      }
 21208    };
 21209  
 21210    function compile(element, attr) {
 21211      return function postLink(scope, element, attrs, controllers) {
 21212  
 21213        $mdUtil.initOptionalProperties(scope, attr);
 21214        $mdTheming(element);
 21215  
 21216        element.attr('tabindex', '-1');
 21217      };
 21218    }
 21219  }
 21220  MdContactChips.$inject = ["$mdTheming", "$mdUtil"];
 21221  
 21222  })();
 21223  (function(){
 21224  "use strict";
 21225  
 21226  angular
 21227    .module('material.components.icon')
 21228    .directive('mdIcon', ['$mdIcon', '$mdTheming', '$mdAria', mdIconDirective]);
 21229  
 21230  /**
 21231   * @ngdoc directive
 21232   * @name mdIcon
 21233   * @module material.components.icon
 21234   *
 21235   * @restrict E
 21236   *
 21237   * @description
 21238   * The `md-icon` directive makes it easier to use vector-based icons in your app (as opposed to
 21239   * raster-based icons types like PNG). The directive supports both icon fonts and SVG icons.
 21240   *
 21241   * Icons should be consider view-only elements that should not be used directly as buttons; instead nest a `<md-icon>`
 21242   * inside a `md-button` to add hover and click features.
 21243   *
 21244   * ### Icon fonts
 21245   * Icon fonts are a technique in which you use a font where the glyphs in the font are
 21246   * your icons instead of text. Benefits include a straightforward way to bundle everything into a
 21247   * single HTTP request, simple scaling, easy color changing, and more.
 21248   *
 21249   * `md-icon` lets you consume an icon font by letting you reference specific icons in that font
 21250   * by name rather than character code.
 21251   *
 21252   * ### SVG
 21253   * For SVGs, the problem with using `<img>` or a CSS `background-image` is that you can't take
 21254   * advantage of some SVG features, such as styling specific parts of the icon with CSS or SVG
 21255   * animation.
 21256   *
 21257   * `md-icon` makes it easier to use SVG icons by *inlining* the SVG into an `<svg>` element in the
 21258   * document. The most straightforward way of referencing an SVG icon is via URL, just like a
 21259   * traditional `<img>`. `$mdIconProvider`, as a convenience, lets you _name_ an icon so you can
 21260   * reference it by name instead of URL throughout your templates.
 21261   *
 21262   * Additionally, you may not want to make separate HTTP requests for every icon, so you can bundle
 21263   * your SVG icons together and pre-load them with $mdIconProvider as an icon set. An icon set can
 21264   * also be given a name, which acts as a namespace for individual icons, so you can reference them
 21265   * like `"social:cake"`.
 21266   *
 21267   * When using SVGs, both external SVGs (via URLs) or sets of SVGs [from icon sets] can be
 21268   * easily loaded and used.When use font-icons, developers must following three (3) simple steps:
 21269   *
 21270   * <ol>
 21271   * <li>Load the font library. e.g.<br/>
 21272   *    &lt;link href="https://fonts.googleapis.com/icon?family=Material+Icons"
 21273   *    rel="stylesheet"&gt;
 21274   * </li>
 21275   * <li> Use either (a) font-icon class names or (b) font ligatures to render the font glyph by using its textual name</li>
 21276   * <li> Use &lt;md-icon md-font-icon="classname" /&gt; or <br/>
 21277   *     use &lt;md-icon md-font-set="font library classname or alias"&gt; textual_name &lt;/md-icon&gt; or <br/>
 21278   *     use &lt;md-icon md-font-set="font library classname or alias"&gt; numerical_character_reference &lt;/md-icon&gt;
 21279   * </li>
 21280   * </ol>
 21281   *
 21282   * Full details for these steps can be found:
 21283   *
 21284   * <ul>
 21285   * <li>http://google.github.io/material-design-icons/</li>
 21286   * <li>http://google.github.io/material-design-icons/#icon-font-for-the-web</li>
 21287   * </ul>
 21288   *
 21289   * The Material Design icon style <code>.material-icons</code> and the icon font references are published in
 21290   * Material Design Icons:
 21291   *
 21292   * <ul>
 21293   * <li>http://www.google.com/design/icons/</li>
 21294   * <li>https://www.google.com/design/icons/#ic_accessibility</li>
 21295   * </ul>
 21296   *
 21297   * <h2 id="material_design_icons">Material Design Icons</h2>
 21298   * Using the Material Design Icon-Selector, developers can easily and quickly search for a Material Design font-icon and
 21299   * determine its textual name and character reference code. Click on any icon to see the slide-up information
 21300   * panel with details regarding a SVG download or information on the font-icon usage.
 21301   *
 21302   * <a href="https://www.google.com/design/icons/#ic_accessibility" target="_blank" style="border-bottom:none;">
 21303   * <img src="https://cloud.githubusercontent.com/assets/210413/7902490/fe8dd14c-0780-11e5-98fb-c821cc6475e6.png"
 21304   *      aria-label="Material Design Icon-Selector" style="max-width:75%;padding-left:10%">
 21305   * </a>
 21306   *
 21307   * <span class="image_caption">
 21308   *  Click on the image above to link to the
 21309   *  <a href="https://www.google.com/design/icons/#ic_accessibility" target="_blank">Material Design Icon-Selector</a>.
 21310   * </span>
 21311   *
 21312   * @param {string} md-font-icon String name of CSS icon associated with the font-face will be used
 21313   * to render the icon. Requires the fonts and the named CSS styles to be preloaded.
 21314   * @param {string} md-font-set CSS style name associated with the font library; which will be assigned as
 21315   * the class for the font-icon ligature. This value may also be an alias that is used to lookup the classname;
 21316   * internally use `$mdIconProvider.fontSet(<alias>)` to determine the style name.
 21317   * @param {string} md-svg-src String URL (or expression) used to load, cache, and display an
 21318   *     external SVG.
 21319   * @param {string} md-svg-icon md-svg-icon String name used for lookup of the icon from the internal cache;
 21320   *     interpolated strings or expressions may also be used. Specific set names can be used with
 21321   *     the syntax `<set name>:<icon name>`.<br/><br/>
 21322   * To use icon sets, developers are required to pre-register the sets using the `$mdIconProvider` service.
 21323   * @param {string=} aria-label Labels icon for accessibility. If an empty string is provided, icon
 21324   * will be hidden from accessibility layer with `aria-hidden="true"`. If there's no aria-label on the icon
 21325   * nor a label on the parent element, a warning will be logged to the console.
 21326   * @param {string=} alt Labels icon for accessibility. If an empty string is provided, icon
 21327   * will be hidden from accessibility layer with `aria-hidden="true"`. If there's no alt on the icon
 21328   * nor a label on the parent element, a warning will be logged to the console.
 21329   *
 21330   * @usage
 21331   * When using SVGs:
 21332   * <hljs lang="html">
 21333   *
 21334   *  <!-- Icon ID; may contain optional icon set prefix; icons must registered using $mdIconProvider -->
 21335   *  <md-icon md-svg-icon="social:android"    aria-label="android " ></md-icon>
 21336   *
 21337   *  <!-- Icon urls; may be preloaded in templateCache -->
 21338   *  <md-icon md-svg-src="/android.svg"       aria-label="android " ></md-icon>
 21339   *  <md-icon md-svg-src="{{ getAndroid() }}" aria-label="android " ></md-icon>
 21340   *
 21341   * </hljs>
 21342   *
 21343   * Use the <code>$mdIconProvider</code> to configure your application with
 21344   * svg iconsets.
 21345   *
 21346   * <hljs lang="js">
 21347   *  angular.module('appSvgIconSets', ['ngMaterial'])
 21348   *    .controller('DemoCtrl', function($scope) {})
 21349   *    .config(function($mdIconProvider) {
 21350   *      $mdIconProvider
 21351   *         .iconSet('social', 'img/icons/sets/social-icons.svg', 24)
 21352   *         .defaultIconSet('img/icons/sets/core-icons.svg', 24);
 21353   *     });
 21354   * </hljs>
 21355   *
 21356   *
 21357   * When using Font Icons with classnames:
 21358   * <hljs lang="html">
 21359   *
 21360   *  <md-icon md-font-icon="android" aria-label="android" ></md-icon>
 21361   *  <md-icon class="icon_home"      aria-label="Home"    ></md-icon>
 21362   *
 21363   * </hljs>
 21364   *
 21365   * When using Material Font Icons with ligatures:
 21366   * <hljs lang="html">
 21367   *  <!--
 21368   *  For Material Design Icons
 21369   *  The class '.material-icons' is auto-added if a style has NOT been specified
 21370   *  since `material-icons` is the default fontset. So your markup:
 21371   *  -->
 21372   *  <md-icon> face </md-icon>
 21373   *  <!-- becomes this at runtime: -->
 21374   *  <md-icon md-font-set="material-icons"> face </md-icon>
 21375   *  <!-- If the fontset does not support ligature names, then we need to use the ligature unicode.-->
 21376   *  <md-icon> &#xE87C; </md-icon>
 21377   *  <!-- The class '.material-icons' must be manually added if other styles are also specified-->
 21378   *  <md-icon class="material-icons md-light md-48"> face </md-icon>
 21379   * </hljs>
 21380   *
 21381   * When using other Font-Icon libraries:
 21382   *
 21383   * <hljs lang="js">
 21384   *  // Specify a font-icon style alias
 21385   *  angular.config(function($mdIconProvider) {
 21386   *    $mdIconProvider.fontSet('md', 'material-icons');
 21387   *  });
 21388   * </hljs>
 21389   *
 21390   * <hljs lang="html">
 21391   *  <md-icon md-font-set="md">favorite</md-icon>
 21392   * </hljs>
 21393   *
 21394   */
 21395  function mdIconDirective($mdIcon, $mdTheming, $mdAria ) {
 21396  
 21397    return {
 21398      restrict: 'E',
 21399      link : postLink
 21400    };
 21401  
 21402  
 21403    /**
 21404     * Directive postLink
 21405     * Supports embedded SVGs, font-icons, & external SVGs
 21406     */
 21407    function postLink(scope, element, attr) {
 21408      $mdTheming(element);
 21409  
 21410      prepareForFontIcon();
 21411  
 21412      // If using a font-icon, then the textual name of the icon itself
 21413      // provides the aria-label.
 21414  
 21415      var label = attr.alt || attr.mdFontIcon || attr.mdSvgIcon || element.text();
 21416      var attrName = attr.$normalize(attr.$attr.mdSvgIcon || attr.$attr.mdSvgSrc || '');
 21417  
 21418      if ( !attr['aria-label'] ) {
 21419  
 21420        if (label !== '' && !parentsHaveText() ) {
 21421  
 21422          $mdAria.expect(element, 'aria-label', label);
 21423          $mdAria.expect(element, 'role', 'img');
 21424  
 21425        } else if ( !element.text() ) {
 21426          // If not a font-icon with ligature, then
 21427          // hide from the accessibility layer.
 21428  
 21429          $mdAria.expect(element, 'aria-hidden', 'true');
 21430        }
 21431      }
 21432  
 21433      if (attrName) {
 21434        // Use either pre-configured SVG or URL source, respectively.
 21435        attr.$observe(attrName, function(attrVal) {
 21436  
 21437          element.empty();
 21438          if (attrVal) {
 21439            $mdIcon(attrVal)
 21440              .then(function(svg) {
 21441                element.empty();
 21442                element.append(svg);
 21443              });
 21444          }
 21445  
 21446        });
 21447      }
 21448  
 21449      function parentsHaveText() {
 21450        var parent = element.parent();
 21451        if (parent.attr('aria-label') || parent.text()) {
 21452          return true;
 21453        }
 21454        else if(parent.parent().attr('aria-label') || parent.parent().text()) {
 21455          return true;
 21456        }
 21457        return false;
 21458      }
 21459  
 21460      function prepareForFontIcon() {
 21461        if (!attr.mdSvgIcon && !attr.mdSvgSrc) {
 21462          if (attr.mdFontIcon) {
 21463            element.addClass('md-font ' + attr.mdFontIcon);
 21464          }
 21465          element.addClass($mdIcon.fontSet(attr.mdFontSet));
 21466        }
 21467      }
 21468    }
 21469  }
 21470  
 21471  })();
 21472  (function(){
 21473  "use strict";
 21474  
 21475    angular
 21476      .module('material.components.icon' )
 21477      .provider('$mdIcon', MdIconProvider);
 21478  
 21479    /**
 21480      * @ngdoc service
 21481      * @name $mdIconProvider
 21482      * @module material.components.icon
 21483      *
 21484      * @description
 21485      * `$mdIconProvider` is used only to register icon IDs with URLs. These configuration features allow
 21486      * icons and icon sets to be pre-registered and associated with source URLs **before** the `<md-icon />`
 21487      * directives are compiled.
 21488      *
 21489      * If using font-icons, the developer is responsible for loading the fonts.
 21490      *
 21491      * If using SVGs, loading of the actual svg files are deferred to on-demand requests and are loaded
 21492      * internally by the `$mdIcon` service using the `$http` service. When an SVG is requested by name/ID,
 21493      * the `$mdIcon` service searches its registry for the associated source URL;
 21494      * that URL is used to on-demand load and parse the SVG dynamically.
 21495      *
 21496      * **Notice:** Most font-icons libraries do not support ligatures (for example `fontawesome`).<br/>
 21497      *  In such cases you are not able to use the icon's ligature name - Like so:
 21498      *
 21499      *  <hljs lang="html">
 21500      *    <md-icon md-font-set="fa">fa-bell</md-icon>
 21501      *  </hljs>
 21502      *
 21503      * You should instead use the given unicode, instead of the ligature name.
 21504      *
 21505      * <p ng-hide="true"> ##// Notice we can't use a hljs element here, because the characters will be escaped.</p>
 21506      *  ```html
 21507      *    <md-icon md-font-set="fa">&#xf0f3</md-icon>
 21508      *  ```
 21509      *
 21510      * All unicode ligatures are prefixed with the `&#x` string.
 21511      *
 21512      * @usage
 21513      * <hljs lang="js">
 21514      *   app.config(function($mdIconProvider) {
 21515      *
 21516      *     // Configure URLs for icons specified by [set:]id.
 21517      *
 21518      *     $mdIconProvider
 21519      *          .defaultFontSet( 'fa' )                   // This sets our default fontset className.
 21520      *          .defaultIconSet('my/app/icons.svg')       // Register a default set of SVG icons
 21521      *          .iconSet('social', 'my/app/social.svg')   // Register a named icon set of SVGs
 21522      *          .icon('android', 'my/app/android.svg')    // Register a specific icon (by name)
 21523      *          .icon('work:chair', 'my/app/chair.svg');  // Register icon in a specific set
 21524      *   });
 21525      * </hljs>
 21526      *
 21527      * SVG icons and icon sets can be easily pre-loaded and cached using either (a) a build process or (b) a runtime
 21528      * **startup** process (shown below):
 21529      *
 21530      * <hljs lang="js">
 21531      *   app.config(function($mdIconProvider) {
 21532      *
 21533      *     // Register a default set of SVG icon definitions
 21534      *     $mdIconProvider.defaultIconSet('my/app/icons.svg')
 21535      *
 21536      *   })
 21537      *   .run(function($http, $templateCache){
 21538      *
 21539      *     // Pre-fetch icons sources by URL and cache in the $templateCache...
 21540      *     // subsequent $http calls will look there first.
 21541      *
 21542      *     var urls = [ 'imy/app/icons.svg', 'img/icons/android.svg'];
 21543      *
 21544      *     angular.forEach(urls, function(url) {
 21545      *       $http.get(url, {cache: $templateCache});
 21546      *     });
 21547      *
 21548      *   });
 21549      *
 21550      * </hljs>
 21551      *
 21552      * NOTE: the loaded SVG data is subsequently cached internally for future requests.
 21553      *
 21554      */
 21555  
 21556     /**
 21557      * @ngdoc method
 21558      * @name $mdIconProvider#icon
 21559      *
 21560      * @description
 21561      * Register a source URL for a specific icon name; the name may include optional 'icon set' name prefix.
 21562      * These icons  will later be retrieved from the cache using `$mdIcon( <icon name> )`
 21563      *
 21564      * @param {string} id Icon name/id used to register the icon
 21565      * @param {string} url specifies the external location for the data file. Used internally by `$http` to load the
 21566      * data or as part of the lookup in `$templateCache` if pre-loading was configured.
 21567      * @param {number=} viewBoxSize Sets the width and height the icon's viewBox.
 21568      * It is ignored for icons with an existing viewBox. Default size is 24.
 21569      *
 21570      * @returns {obj} an `$mdIconProvider` reference; used to support method call chains for the API
 21571      *
 21572      * @usage
 21573      * <hljs lang="js">
 21574      *   app.config(function($mdIconProvider) {
 21575      *
 21576      *     // Configure URLs for icons specified by [set:]id.
 21577      *
 21578      *     $mdIconProvider
 21579      *          .icon('android', 'my/app/android.svg')    // Register a specific icon (by name)
 21580      *          .icon('work:chair', 'my/app/chair.svg');  // Register icon in a specific set
 21581      *   });
 21582      * </hljs>
 21583      *
 21584      */
 21585     /**
 21586      * @ngdoc method
 21587      * @name $mdIconProvider#iconSet
 21588      *
 21589      * @description
 21590      * Register a source URL for a 'named' set of icons; group of SVG definitions where each definition
 21591      * has an icon id. Individual icons can be subsequently retrieved from this cached set using
 21592      * `$mdIcon(<icon set name>:<icon name>)`
 21593      *
 21594      * @param {string} id Icon name/id used to register the iconset
 21595      * @param {string} url specifies the external location for the data file. Used internally by `$http` to load the
 21596      * data or as part of the lookup in `$templateCache` if pre-loading was configured.
 21597      * @param {number=} viewBoxSize Sets the width and height of the viewBox of all icons in the set.
 21598      * It is ignored for icons with an existing viewBox. All icons in the icon set should be the same size.
 21599      * Default value is 24.
 21600      *
 21601      * @returns {obj} an `$mdIconProvider` reference; used to support method call chains for the API
 21602      *
 21603      *
 21604      * @usage
 21605      * <hljs lang="js">
 21606      *   app.config(function($mdIconProvider) {
 21607      *
 21608      *     // Configure URLs for icons specified by [set:]id.
 21609      *
 21610      *     $mdIconProvider
 21611      *          .iconSet('social', 'my/app/social.svg')   // Register a named icon set
 21612      *   });
 21613      * </hljs>
 21614      *
 21615      */
 21616     /**
 21617      * @ngdoc method
 21618      * @name $mdIconProvider#defaultIconSet
 21619      *
 21620      * @description
 21621      * Register a source URL for the default 'named' set of icons. Unless explicitly registered,
 21622      * subsequent lookups of icons will failover to search this 'default' icon set.
 21623      * Icon can be retrieved from this cached, default set using `$mdIcon(<name>)`
 21624      *
 21625      * @param {string} url specifies the external location for the data file. Used internally by `$http` to load the
 21626      * data or as part of the lookup in `$templateCache` if pre-loading was configured.
 21627      * @param {number=} viewBoxSize Sets the width and height of the viewBox of all icons in the set.
 21628      * It is ignored for icons with an existing viewBox. All icons in the icon set should be the same size.
 21629      * Default value is 24.
 21630      *
 21631      * @returns {obj} an `$mdIconProvider` reference; used to support method call chains for the API
 21632      *
 21633      * @usage
 21634      * <hljs lang="js">
 21635      *   app.config(function($mdIconProvider) {
 21636      *
 21637      *     // Configure URLs for icons specified by [set:]id.
 21638      *
 21639      *     $mdIconProvider
 21640      *          .defaultIconSet( 'my/app/social.svg' )   // Register a default icon set
 21641      *   });
 21642      * </hljs>
 21643      *
 21644      */
 21645    /**
 21646     * @ngdoc method
 21647     * @name $mdIconProvider#defaultFontSet
 21648     *
 21649     * @description
 21650     * When using Font-Icons, Angular Material assumes the the Material Design icons will be used and automatically
 21651     * configures the default font-set == 'material-icons'. Note that the font-set references the font-icon library
 21652     * class style that should be applied to the `<md-icon>`.
 21653     *
 21654     * Configuring the default means that the attributes
 21655     * `md-font-set="material-icons"` or `class="material-icons"` do not need to be explicitly declared on the
 21656     * `<md-icon>` markup. For example:
 21657     *
 21658     *  `<md-icon> face </md-icon>`
 21659     *  will render as
 21660     *  `<span class="material-icons"> face </span>`, and
 21661     *
 21662     *  `<md-icon md-font-set="fa"> face </md-icon>`
 21663     *  will render as
 21664     *  `<span class="fa"> face </span>`
 21665     *
 21666     * @param {string} name of the font-library style that should be applied to the md-icon DOM element
 21667     *
 21668     * @usage
 21669     * <hljs lang="js">
 21670     *   app.config(function($mdIconProvider) {
 21671     *     $mdIconProvider.defaultFontSet( 'fa' );
 21672     *   });
 21673     * </hljs>
 21674     *
 21675     */
 21676  
 21677    /**
 21678     * @ngdoc method
 21679     * @name $mdIconProvider#fontSet
 21680     *
 21681     * @description
 21682     * When using a font set for `<md-icon>` you must specify the correct font classname in the `md-font-set`
 21683     * attribute. If the fonset className is really long, your markup may become cluttered... an easy
 21684     * solution is to define an `alias` for your fontset:
 21685     *
 21686     * @param {string} alias of the specified fontset.
 21687     * @param {string} className of the fontset.
 21688     *
 21689     * @usage
 21690     * <hljs lang="js">
 21691     *   app.config(function($mdIconProvider) {
 21692     *     // In this case, we set an alias for the `material-icons` fontset.
 21693     *     $mdIconProvider.fontSet('md', 'material-icons');
 21694     *   });
 21695     * </hljs>
 21696     *
 21697     */
 21698  
 21699     /**
 21700      * @ngdoc method
 21701      * @name $mdIconProvider#defaultViewBoxSize
 21702      *
 21703      * @description
 21704      * While `<md-icon />` markup can also be style with sizing CSS, this method configures
 21705      * the default width **and** height used for all icons; unless overridden by specific CSS.
 21706      * The default sizing is (24px, 24px).
 21707      * @param {number=} viewBoxSize Sets the width and height of the viewBox for an icon or an icon set.
 21708      * All icons in a set should be the same size. The default value is 24.
 21709      *
 21710      * @returns {obj} an `$mdIconProvider` reference; used to support method call chains for the API
 21711      *
 21712      * @usage
 21713      * <hljs lang="js">
 21714      *   app.config(function($mdIconProvider) {
 21715      *
 21716      *     // Configure URLs for icons specified by [set:]id.
 21717      *
 21718      *     $mdIconProvider
 21719      *          .defaultViewBoxSize(36)   // Register a default icon size (width == height)
 21720      *   });
 21721      * </hljs>
 21722      *
 21723      */
 21724  
 21725   var config = {
 21726     defaultViewBoxSize: 24,
 21727     defaultFontSet: 'material-icons',
 21728     fontSets : [ ]
 21729   };
 21730  
 21731   function MdIconProvider() { }
 21732  
 21733   MdIconProvider.prototype = {
 21734     icon : function (id, url, viewBoxSize) {
 21735       if ( id.indexOf(':') == -1 ) id = '$default:' + id;
 21736  
 21737       config[id] = new ConfigurationItem(url, viewBoxSize );
 21738       return this;
 21739     },
 21740  
 21741     iconSet : function (id, url, viewBoxSize) {
 21742       config[id] = new ConfigurationItem(url, viewBoxSize );
 21743       return this;
 21744     },
 21745  
 21746     defaultIconSet : function (url, viewBoxSize) {
 21747       var setName = '$default';
 21748  
 21749       if ( !config[setName] ) {
 21750         config[setName] = new ConfigurationItem(url, viewBoxSize );
 21751       }
 21752  
 21753       config[setName].viewBoxSize = viewBoxSize || config.defaultViewBoxSize;
 21754  
 21755       return this;
 21756     },
 21757  
 21758     defaultViewBoxSize : function (viewBoxSize) {
 21759       config.defaultViewBoxSize = viewBoxSize;
 21760       return this;
 21761     },
 21762  
 21763     /**
 21764      * Register an alias name associated with a font-icon library style ;
 21765      */
 21766     fontSet : function fontSet(alias, className) {
 21767      config.fontSets.push({
 21768        alias : alias,
 21769        fontSet : className || alias
 21770      });
 21771      return this;
 21772     },
 21773  
 21774     /**
 21775      * Specify a default style name associated with a font-icon library
 21776      * fallback to Material Icons.
 21777      *
 21778      */
 21779     defaultFontSet : function defaultFontSet(className) {
 21780      config.defaultFontSet = !className ? '' : className;
 21781      return this;
 21782     },
 21783  
 21784     defaultIconSize : function defaultIconSize(iconSize) {
 21785       config.defaultIconSize = iconSize;
 21786       return this;
 21787     },
 21788  
 21789     preloadIcons: function ($templateCache) {
 21790       var iconProvider = this;
 21791       var svgRegistry = [
 21792         {
 21793           id : 'md-tabs-arrow',
 21794           url: 'md-tabs-arrow.svg',
 21795           svg: '<svg version="1.1" x="0px" y="0px" viewBox="0 0 24 24"><g><polygon points="15.4,7.4 14,6 8,12 14,18 15.4,16.6 10.8,12 "/></g></svg>'
 21796         },
 21797         {
 21798           id : 'md-close',
 21799           url: 'md-close.svg',
 21800           svg: '<svg version="1.1" x="0px" y="0px" viewBox="0 0 24 24"><g><path d="M19 6.41l-1.41-1.41-5.59 5.59-5.59-5.59-1.41 1.41 5.59 5.59-5.59 5.59 1.41 1.41 5.59-5.59 5.59 5.59 1.41-1.41-5.59-5.59z"/></g></svg>'
 21801         },
 21802         {
 21803           id:  'md-cancel',
 21804           url: 'md-cancel.svg',
 21805           svg: '<svg version="1.1" x="0px" y="0px" viewBox="0 0 24 24"><g><path d="M12 2c-5.53 0-10 4.47-10 10s4.47 10 10 10 10-4.47 10-10-4.47-10-10-10zm5 13.59l-1.41 1.41-3.59-3.59-3.59 3.59-1.41-1.41 3.59-3.59-3.59-3.59 1.41-1.41 3.59 3.59 3.59-3.59 1.41 1.41-3.59 3.59 3.59 3.59z"/></g></svg>'
 21806         },
 21807         {
 21808           id:  'md-menu',
 21809           url: 'md-menu.svg',
 21810           svg: '<svg version="1.1" x="0px" y="0px" viewBox="0 0 24 24"><path d="M3,6H21V8H3V6M3,11H21V13H3V11M3,16H21V18H3V16Z" /></svg>'
 21811         },
 21812         {
 21813           id:  'md-toggle-arrow',
 21814           url: 'md-toggle-arrow-svg',
 21815           svg: '<svg version="1.1" x="0px" y="0px" viewBox="0 0 48 48"><path d="M24 16l-12 12 2.83 2.83 9.17-9.17 9.17 9.17 2.83-2.83z"/><path d="M0 0h48v48h-48z" fill="none"/></svg>'
 21816         },
 21817         {
 21818           id:  'md-calendar',
 21819           url: 'md-calendar.svg',
 21820           svg: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11zM7 10h5v5H7z"/></svg>'
 21821         }
 21822       ];
 21823  
 21824       svgRegistry.forEach(function(asset){
 21825         iconProvider.icon(asset.id,  asset.url);
 21826         $templateCache.put(asset.url, asset.svg);
 21827       });
 21828  
 21829     },
 21830  
 21831     $get : ['$http', '$q', '$log', '$templateCache', '$mdUtil', function($http, $q, $log, $templateCache, $mdUtil) {
 21832       this.preloadIcons($templateCache);
 21833       return MdIconService(config, $http, $q, $log, $templateCache, $mdUtil);
 21834     }]
 21835   };
 21836  
 21837     /**
 21838      *  Configuration item stored in the Icon registry; used for lookups
 21839      *  to load if not already cached in the `loaded` cache
 21840      */
 21841     function ConfigurationItem(url, viewBoxSize) {
 21842       this.url = url;
 21843       this.viewBoxSize = viewBoxSize || config.defaultViewBoxSize;
 21844     }
 21845  
 21846   /**
 21847    * @ngdoc service
 21848    * @name $mdIcon
 21849    * @module material.components.icon
 21850    *
 21851    * @description
 21852    * The `$mdIcon` service is a function used to lookup SVG icons.
 21853    *
 21854    * @param {string} id Query value for a unique Id or URL. If the argument is a URL, then the service will retrieve the icon element
 21855    * from its internal cache or load the icon and cache it first. If the value is not a URL-type string, then an ID lookup is
 21856    * performed. The Id may be a unique icon ID or may include an iconSet ID prefix.
 21857    *
 21858    * For the **id** query to work properly, this means that all id-to-URL mappings must have been previously configured
 21859    * using the `$mdIconProvider`.
 21860    *
 21861    * @returns {obj} Clone of the initial SVG DOM element; which was created from the SVG markup in the SVG data file.
 21862    *
 21863    * @usage
 21864    * <hljs lang="js">
 21865    * function SomeDirective($mdIcon) {
 21866    *
 21867    *   // See if the icon has already been loaded, if not
 21868    *   // then lookup the icon from the registry cache, load and cache
 21869    *   // it for future requests.
 21870    *   // NOTE: ID queries require configuration with $mdIconProvider
 21871    *
 21872    *   $mdIcon('android').then(function(iconEl)    { element.append(iconEl); });
 21873    *   $mdIcon('work:chair').then(function(iconEl) { element.append(iconEl); });
 21874    *
 21875    *   // Load and cache the external SVG using a URL
 21876    *
 21877    *   $mdIcon('img/icons/android.svg').then(function(iconEl) {
 21878    *     element.append(iconEl);
 21879    *   });
 21880    * };
 21881    * </hljs>
 21882    *
 21883    * NOTE: The `<md-icon />  ` directive internally uses the `$mdIcon` service to query, loaded, and instantiate
 21884    * SVG DOM elements.
 21885    */
 21886  
 21887    /* @ngInject */
 21888   function MdIconService(config, $http, $q, $log, $templateCache, $mdUtil) {
 21889     var iconCache = {};
 21890     var urlRegex = /[-\w@:%\+.~#?&//=]{2,}\.[a-z]{2,4}\b(\/[-\w@:%\+.~#?&//=]*)?/i;
 21891     var dataUrlRegex = /^data:image\/svg\+xml[\s*;\w\-\=]*?(base64)?,(.*)$/i;
 21892  
 21893     Icon.prototype = { clone : cloneSVG, prepare: prepareAndStyle };
 21894     getIcon.fontSet = findRegisteredFontSet;
 21895  
 21896     // Publish service...
 21897     return getIcon;
 21898  
 21899     /**
 21900      * Actual $mdIcon service is essentially a lookup function
 21901      */
 21902     function getIcon(id) {
 21903       id = id || '';
 21904  
 21905       // If already loaded and cached, use a clone of the cached icon.
 21906       // Otherwise either load by URL, or lookup in the registry and then load by URL, and cache.
 21907  
 21908       if ( iconCache[id] ) return $q.when( transformClone(iconCache[id]) );
 21909       if ( urlRegex.test(id) || dataUrlRegex.test(id) ) return loadByURL(id).then( cacheIcon(id) );
 21910       if ( id.indexOf(':') == -1 ) id = '$default:' + id;
 21911  
 21912       var load = config[id] ? loadByID : loadFromIconSet;
 21913       return load(id)
 21914           .then( cacheIcon(id) );
 21915     }
 21916  
 21917     /**
 21918      * Lookup registered fontSet style using its alias...
 21919      * If not found,
 21920      */
 21921     function findRegisteredFontSet(alias) {
 21922        var useDefault = angular.isUndefined(alias) || !(alias && alias.length);
 21923        if ( useDefault ) return config.defaultFontSet;
 21924  
 21925        var result = alias;
 21926        angular.forEach(config.fontSets, function(it){
 21927          if ( it.alias == alias ) result = it.fontSet || result;
 21928        });
 21929  
 21930        return result;
 21931     }
 21932  
 21933     function transformClone(cacheElement) {
 21934       var clone = cacheElement.clone();
 21935       var cacheSuffix = '_cache' + $mdUtil.nextUid();
 21936  
 21937       // We need to modify for each cached icon the id attributes.
 21938       // This is needed because SVG id's are treated as normal DOM ids
 21939       // and should not have a duplicated id.
 21940       if (clone.id) clone.id += cacheSuffix;
 21941       angular.forEach(clone.querySelectorAll('[id]'), function (item) {
 21942         item.id += cacheSuffix;
 21943       });
 21944  
 21945       return clone;
 21946     }
 21947  
 21948     /**
 21949      * Prepare and cache the loaded icon for the specified `id`
 21950      */
 21951     function cacheIcon( id ) {
 21952  
 21953       return function updateCache( icon ) {
 21954         iconCache[id] = isIcon(icon) ? icon : new Icon(icon, config[id]);
 21955  
 21956         return iconCache[id].clone();
 21957       };
 21958     }
 21959  
 21960     /**
 21961      * Lookup the configuration in the registry, if !registered throw an error
 21962      * otherwise load the icon [on-demand] using the registered URL.
 21963      *
 21964      */
 21965     function loadByID(id) {
 21966      var iconConfig = config[id];
 21967       return loadByURL(iconConfig.url).then(function(icon) {
 21968         return new Icon(icon, iconConfig);
 21969       });
 21970     }
 21971  
 21972     /**
 21973      *    Loads the file as XML and uses querySelector( <id> ) to find
 21974      *    the desired node...
 21975      */
 21976     function loadFromIconSet(id) {
 21977       var setName = id.substring(0, id.lastIndexOf(':')) || '$default';
 21978       var iconSetConfig = config[setName];
 21979  
 21980       return !iconSetConfig ? announceIdNotFound(id) : loadByURL(iconSetConfig.url).then(extractFromSet);
 21981  
 21982       function extractFromSet(set) {
 21983         var iconName = id.slice(id.lastIndexOf(':') + 1);
 21984         var icon = set.querySelector('#' + iconName);
 21985         return !icon ? announceIdNotFound(id) : new Icon(icon, iconSetConfig);
 21986       }
 21987  
 21988       function announceIdNotFound(id) {
 21989         var msg = 'icon ' + id + ' not found';
 21990        $log.warn(msg);
 21991  
 21992         return $q.reject(msg || id);
 21993       }
 21994     }
 21995  
 21996     /**
 21997      * Load the icon by URL (may use the $templateCache).
 21998      * Extract the data for later conversion to Icon
 21999      */
 22000     function loadByURL(url) {
 22001       /* Load the icon from embedded data URL. */
 22002       function loadByDataUrl(url) {
 22003         var results = dataUrlRegex.exec(url);
 22004         var isBase64 = /base64/i.test(url);
 22005         var data = isBase64 ? window.atob(results[2]) : results[2];
 22006         return $q.when(angular.element(data)[0]);
 22007       }
 22008  
 22009       /* Load the icon by URL using HTTP. */
 22010       function loadByHttpUrl(url) {
 22011         return $http
 22012           .get(url, { cache: $templateCache })
 22013           .then(function(response) {
 22014             return angular.element('<div>').append(response.data).find('svg')[0];
 22015           }).catch(announceNotFound);
 22016       }
 22017  
 22018       return dataUrlRegex.test(url)
 22019         ? loadByDataUrl(url)
 22020         : loadByHttpUrl(url);
 22021     }
 22022  
 22023     /**
 22024      * Catch HTTP or generic errors not related to incorrect icon IDs.
 22025      */
 22026     function announceNotFound(err) {
 22027       var msg = angular.isString(err) ? err : (err.message || err.data || err.statusText);
 22028       $log.warn(msg);
 22029  
 22030       return $q.reject(msg);
 22031     }
 22032  
 22033     /**
 22034      * Check target signature to see if it is an Icon instance.
 22035      */
 22036     function isIcon(target) {
 22037       return angular.isDefined(target.element) && angular.isDefined(target.config);
 22038     }
 22039  
 22040     /**
 22041      *  Define the Icon class
 22042      */
 22043     function Icon(el, config) {
 22044       if (el && el.tagName != 'svg') {
 22045         el = angular.element('<svg xmlns="http://www.w3.org/2000/svg">').append(el)[0];
 22046       }
 22047  
 22048       // Inject the namespace if not available...
 22049       if ( !el.getAttribute('xmlns') ) {
 22050         el.setAttribute('xmlns', "http://www.w3.org/2000/svg");
 22051       }
 22052  
 22053       this.element = el;
 22054       this.config = config;
 22055       this.prepare();
 22056     }
 22057  
 22058     /**
 22059      *  Prepare the DOM element that will be cached in the
 22060      *  loaded iconCache store.
 22061      */
 22062     function prepareAndStyle() {
 22063       var viewBoxSize = this.config ? this.config.viewBoxSize : config.defaultViewBoxSize;
 22064           angular.forEach({
 22065             'fit'   : '',
 22066             'height': '100%',
 22067             'width' : '100%',
 22068             'preserveAspectRatio': 'xMidYMid meet',
 22069             'viewBox' : this.element.getAttribute('viewBox') || ('0 0 ' + viewBoxSize + ' ' + viewBoxSize),
 22070             'focusable': false // Disable IE11s default behavior to make SVGs focusable
 22071           }, function(val, attr) {
 22072             this.element.setAttribute(attr, val);
 22073           }, this);
 22074     }
 22075  
 22076     /**
 22077      * Clone the Icon DOM element.
 22078      */
 22079     function cloneSVG(){
 22080       // If the element or any of its children have a style attribute, then a CSP policy without
 22081       // 'unsafe-inline' in the style-src directive, will result in a violation.
 22082       return this.element.cloneNode(true);
 22083     }
 22084  
 22085   }
 22086   MdIconService.$inject = ["config", "$http", "$q", "$log", "$templateCache", "$mdUtil"];
 22087  
 22088  })();
 22089  (function(){
 22090  "use strict";
 22091  
 22092  
 22093  
 22094  angular
 22095      .module('material.components.menu')
 22096      .controller('mdMenuCtrl', MenuController);
 22097  
 22098  /**
 22099   * @ngInject
 22100   */
 22101  function MenuController($mdMenu, $attrs, $element, $scope, $mdUtil, $timeout, $rootScope, $q) {
 22102  
 22103    var menuContainer;
 22104    var self = this;
 22105    var triggerElement;
 22106  
 22107    this.nestLevel = parseInt($attrs.mdNestLevel, 10) || 0;
 22108  
 22109    /**
 22110     * Called by our linking fn to provide access to the menu-content
 22111     * element removed during link
 22112     */
 22113    this.init = function init(setMenuContainer, opts) {
 22114      opts = opts || {};
 22115      menuContainer = setMenuContainer;
 22116      // Default element for ARIA attributes has the ngClick or ngMouseenter expression
 22117      triggerElement = $element[0].querySelector('[ng-click],[ng-mouseenter]');
 22118      triggerElement.setAttribute('aria-expanded', 'false');
 22119  
 22120      this.isInMenuBar = opts.isInMenuBar;
 22121      this.nestedMenus = $mdUtil.nodesToArray(menuContainer[0].querySelectorAll('.md-nested-menu'));
 22122  
 22123      menuContainer.on('$mdInterimElementRemove', function() {
 22124        self.isOpen = false;
 22125      });
 22126  
 22127      var menuContainerId = 'menu_container_' + $mdUtil.nextUid();
 22128      menuContainer.attr('id', menuContainerId);
 22129      angular.element(triggerElement).attr({
 22130        'aria-owns': menuContainerId,
 22131        'aria-haspopup': 'true'
 22132      });
 22133  
 22134      $scope.$on('$destroy', this.disableHoverListener);
 22135      menuContainer.on('$destroy', function() {
 22136        $mdMenu.destroy();
 22137      });
 22138    };
 22139  
 22140    var openMenuTimeout, menuItems, deregisterScopeListeners = [];
 22141    this.enableHoverListener = function() {
 22142      deregisterScopeListeners.push($rootScope.$on('$mdMenuOpen', function(event, el) {
 22143        if (menuContainer[0].contains(el[0])) {
 22144          self.currentlyOpenMenu = el.controller('mdMenu');
 22145          self.isAlreadyOpening = false;
 22146          self.currentlyOpenMenu.registerContainerProxy(self.triggerContainerProxy.bind(self));
 22147        }
 22148      }));
 22149      deregisterScopeListeners.push($rootScope.$on('$mdMenuClose', function(event, el) {
 22150        if (menuContainer[0].contains(el[0])) {
 22151          self.currentlyOpenMenu = undefined;
 22152        }
 22153      }));
 22154      menuItems = angular.element($mdUtil.nodesToArray(menuContainer[0].children[0].children));
 22155      menuItems.on('mouseenter', self.handleMenuItemHover);
 22156      menuItems.on('mouseleave', self.handleMenuItemMouseLeave);
 22157    };
 22158  
 22159    this.disableHoverListener = function() {
 22160      while (deregisterScopeListeners.length) {
 22161        deregisterScopeListeners.shift()();
 22162      }
 22163      menuItems && menuItems.off('mouseenter', self.handleMenuItemHover);
 22164      menuItems && menuItems.off('mouseleave', self.handleMenuMouseLeave);
 22165    };
 22166  
 22167    this.handleMenuItemHover = function(event) {
 22168      if (self.isAlreadyOpening) return;
 22169      var nestedMenu = (
 22170        event.target.querySelector('md-menu')
 22171          || $mdUtil.getClosest(event.target, 'MD-MENU')
 22172      );
 22173      openMenuTimeout = $timeout(function() {
 22174        if (nestedMenu) {
 22175          nestedMenu = angular.element(nestedMenu).controller('mdMenu');
 22176        }
 22177  
 22178        if (self.currentlyOpenMenu && self.currentlyOpenMenu != nestedMenu) {
 22179          var closeTo = self.nestLevel + 1;
 22180          self.currentlyOpenMenu.close(true, { closeTo: closeTo });
 22181          self.isAlreadyOpening = true;
 22182          nestedMenu.open();
 22183        } else if (nestedMenu && !nestedMenu.isOpen && nestedMenu.open) {
 22184          self.isAlreadyOpening = true;
 22185          nestedMenu.open();
 22186        }
 22187      }, nestedMenu ? 100 : 250);
 22188      var focusableTarget = event.currentTarget.querySelector('.md-button:not([disabled])');
 22189      focusableTarget && focusableTarget.focus();
 22190    };
 22191  
 22192    this.handleMenuItemMouseLeave = function() {
 22193      if (openMenuTimeout) {
 22194        $timeout.cancel(openMenuTimeout);
 22195        openMenuTimeout = undefined;
 22196      }
 22197    };
 22198  
 22199  
 22200    /**
 22201     * Uses the $mdMenu interim element service to open the menu contents
 22202     */
 22203    this.open = function openMenu(ev) {
 22204      ev && ev.stopPropagation();
 22205      ev && ev.preventDefault();
 22206      if (self.isOpen) return;
 22207      self.enableHoverListener();
 22208      self.isOpen = true;
 22209      triggerElement = triggerElement || (ev ? ev.target : $element[0]);
 22210      triggerElement.setAttribute('aria-expanded', 'true');
 22211      $scope.$emit('$mdMenuOpen', $element);
 22212      $mdMenu.show({
 22213        scope: $scope,
 22214        mdMenuCtrl: self,
 22215        nestLevel: self.nestLevel,
 22216        element: menuContainer,
 22217        target: triggerElement,
 22218        preserveElement: true,
 22219        parent: 'body'
 22220      }).finally(function() {
 22221        triggerElement.setAttribute('aria-expanded', 'false');
 22222        self.disableHoverListener();
 22223      });
 22224    };
 22225  
 22226    // Expose a open function to the child scope for html to use
 22227    $scope.$mdOpenMenu = this.open;
 22228  
 22229    $scope.$watch(function() { return self.isOpen; }, function(isOpen) {
 22230      if (isOpen) {
 22231        menuContainer.attr('aria-hidden', 'false');
 22232        $element[0].classList.add('md-open');
 22233        angular.forEach(self.nestedMenus, function(el) {
 22234          el.classList.remove('md-open');
 22235        });
 22236      } else {
 22237        menuContainer.attr('aria-hidden', 'true');
 22238        $element[0].classList.remove('md-open');
 22239      }
 22240      $scope.$mdMenuIsOpen = self.isOpen;
 22241    });
 22242  
 22243    this.focusMenuContainer = function focusMenuContainer() {
 22244      var focusTarget = menuContainer[0].querySelector('[md-menu-focus-target]');
 22245      if (!focusTarget) focusTarget = menuContainer[0].querySelector('.md-button');
 22246      focusTarget.focus();
 22247    };
 22248  
 22249    this.registerContainerProxy = function registerContainerProxy(handler) {
 22250      this.containerProxy = handler;
 22251    };
 22252  
 22253    this.triggerContainerProxy = function triggerContainerProxy(ev) {
 22254      this.containerProxy && this.containerProxy(ev);
 22255    };
 22256  
 22257    this.destroy = function() {
 22258      return self.isOpen ? $mdMenu.destroy() : $q.when(false);
 22259    };
 22260  
 22261    // Use the $mdMenu interim element service to close the menu contents
 22262    this.close = function closeMenu(skipFocus, closeOpts) {
 22263      if ( !self.isOpen ) return;
 22264      self.isOpen = false;
 22265  
 22266      var eventDetails = angular.extend({}, closeOpts, { skipFocus: skipFocus });
 22267      $scope.$emit('$mdMenuClose', $element, eventDetails);
 22268      $mdMenu.hide(null, closeOpts);
 22269  
 22270      if (!skipFocus) {
 22271        var el = self.restoreFocusTo || $element.find('button')[0];
 22272        if (el instanceof angular.element) el = el[0];
 22273        if (el) el.focus();
 22274      }
 22275    };
 22276  
 22277    /**
 22278     * Build a nice object out of our string attribute which specifies the
 22279     * target mode for left and top positioning
 22280     */
 22281    this.positionMode = function positionMode() {
 22282      var attachment = ($attrs.mdPositionMode || 'target').split(' ');
 22283  
 22284      // If attachment is a single item, duplicate it for our second value.
 22285      // ie. 'target' -> 'target target'
 22286      if (attachment.length == 1) {
 22287        attachment.push(attachment[0]);
 22288      }
 22289  
 22290      return {
 22291        left: attachment[0],
 22292        top: attachment[1]
 22293      };
 22294    };
 22295  
 22296    /**
 22297     * Build a nice object out of our string attribute which specifies
 22298     * the offset of top and left in pixels.
 22299     */
 22300    this.offsets = function offsets() {
 22301      var position = ($attrs.mdOffset || '0 0').split(' ').map(parseFloat);
 22302      if (position.length == 2) {
 22303        return {
 22304          left: position[0],
 22305          top: position[1]
 22306        };
 22307      } else if (position.length == 1) {
 22308        return {
 22309          top: position[0],
 22310          left: position[0]
 22311        };
 22312      } else {
 22313        throw Error('Invalid offsets specified. Please follow format <x, y> or <n>');
 22314      }
 22315    };
 22316  }
 22317  MenuController.$inject = ["$mdMenu", "$attrs", "$element", "$scope", "$mdUtil", "$timeout", "$rootScope", "$q"];
 22318  
 22319  })();
 22320  (function(){
 22321  "use strict";
 22322  
 22323  /**
 22324   * @ngdoc directive
 22325   * @name mdMenu
 22326   * @module material.components.menu
 22327   * @restrict E
 22328   * @description
 22329   *
 22330   * Menus are elements that open when clicked. They are useful for displaying
 22331   * additional options within the context of an action.
 22332   *
 22333   * Every `md-menu` must specify exactly two child elements. The first element is what is
 22334   * left in the DOM and is used to open the menu. This element is called the trigger element.
 22335   * The trigger element's scope has access to `$mdOpenMenu($event)`
 22336   * which it may call to open the menu. By passing $event as argument, the
 22337   * corresponding event is stopped from propagating up the DOM-tree.
 22338   *
 22339   * The second element is the `md-menu-content` element which represents the
 22340   * contents of the menu when it is open. Typically this will contain `md-menu-item`s,
 22341   * but you can do custom content as well.
 22342   *
 22343   * <hljs lang="html">
 22344   * <md-menu>
 22345   *  <!-- Trigger element is a md-button with an icon -->
 22346   *  <md-button ng-click="$mdOpenMenu($event)" class="md-icon-button" aria-label="Open sample menu">
 22347   *    <md-icon md-svg-icon="call:phone"></md-icon>
 22348   *  </md-button>
 22349   *  <md-menu-content>
 22350   *    <md-menu-item><md-button ng-click="doSomething()">Do Something</md-button></md-menu-item>
 22351   *  </md-menu-content>
 22352   * </md-menu>
 22353   * </hljs>
 22354  
 22355   * ## Sizing Menus
 22356   *
 22357   * The width of the menu when it is open may be specified by specifying a `width`
 22358   * attribute on the `md-menu-content` element.
 22359   * See the [Material Design Spec](http://www.google.com/design/spec/components/menus.html#menus-specs)
 22360   * for more information.
 22361   *
 22362   *
 22363   * ## Aligning Menus
 22364   *
 22365   * When a menu opens, it is important that the content aligns with the trigger element.
 22366   * Failure to align menus can result in jarring experiences for users as content
 22367   * suddenly shifts. To help with this, `md-menu` provides several APIs to help
 22368   * with alignment.
 22369   *
 22370   * ### Target Mode
 22371   *
 22372   * By default, `md-menu` will attempt to align the `md-menu-content` by aligning
 22373   * designated child elements in both the trigger and the menu content.
 22374   *
 22375   * To specify the alignment element in the `trigger` you can use the `md-menu-origin`
 22376   * attribute on a child element. If no `md-menu-origin` is specified, the `md-menu`
 22377   * will be used as the origin element.
 22378   *
 22379   * Similarly, the `md-menu-content` may specify a `md-menu-align-target` for a
 22380   * `md-menu-item` to specify the node that it should try and align with.
 22381   *
 22382   * In this example code, we specify an icon to be our origin element, and an
 22383   * icon in our menu content to be our alignment target. This ensures that both
 22384   * icons are aligned when the menu opens.
 22385   *
 22386   * <hljs lang="html">
 22387   * <md-menu>
 22388   *  <md-button ng-click="$mdOpenMenu($event)" class="md-icon-button" aria-label="Open some menu">
 22389   *    <md-icon md-menu-origin md-svg-icon="call:phone"></md-icon>
 22390   *  </md-button>
 22391   *  <md-menu-content>
 22392   *    <md-menu-item>
 22393   *      <md-button ng-click="doSomething()" aria-label="Do something">
 22394   *        <md-icon md-menu-align-target md-svg-icon="call:phone"></md-icon>
 22395   *        Do Something
 22396   *      </md-button>
 22397   *    </md-menu-item>
 22398   *  </md-menu-content>
 22399   * </md-menu>
 22400   * </hljs>
 22401   *
 22402   * Sometimes we want to specify alignment on the right side of an element, for example
 22403   * if we have a menu on the right side a toolbar, we want to right align our menu content.
 22404   *
 22405   * We can specify the origin by using the `md-position-mode` attribute on both
 22406   * the `x` and `y` axis. Right now only the `x-axis` has more than one option.
 22407   * You may specify the default mode of `target target` or
 22408   * `target-right target` to specify a right-oriented alignment target. See the
 22409   * position section of the demos for more examples.
 22410   *
 22411   * ### Menu Offsets
 22412   *
 22413   * It is sometimes unavoidable to need to have a deeper level of control for
 22414   * the positioning of a menu to ensure perfect alignment. `md-menu` provides
 22415   * the `md-offset` attribute to allow pixel level specificty of adjusting the
 22416   * exact positioning.
 22417   *
 22418   * This offset is provided in the format of `x y` or `n` where `n` will be used
 22419   * in both the `x` and `y` axis.
 22420   *
 22421   * For example, to move a menu by `2px` from the top, we can use:
 22422   * <hljs lang="html">
 22423   * <md-menu md-offset="2 0">
 22424   *   <!-- menu-content -->
 22425   * </md-menu>
 22426   * </hljs>
 22427  
 22428   * ### Preventing close
 22429   *
 22430   * Sometimes you would like to be able to click on a menu item without having the menu
 22431   * close. To do this, ngMaterial exposes the `md-prevent-menu-close` attribute which
 22432   * can be added to a button inside a menu to stop the menu from automatically closing.
 22433   * You can then close the menu programatically by injecting `$mdMenu` and calling 
 22434   * `$mdMenu.hide()`.
 22435   *
 22436   * <hljs lang="html">
 22437   * <md-menu-item>
 22438   *   <md-button ng-click="doSomething()" aria-label="Do something" md-prevent-menu-close="md-prevent-menu-close">
 22439   *     <md-icon md-menu-align-target md-svg-icon="call:phone"></md-icon>
 22440   *     Do Something
 22441   *   </md-button>
 22442   * </md-menu-item>
 22443   * </hljs>
 22444   *
 22445   * @usage
 22446   * <hljs lang="html">
 22447   * <md-menu>
 22448   *  <md-button ng-click="$mdOpenMenu($event)" class="md-icon-button">
 22449   *    <md-icon md-svg-icon="call:phone"></md-icon>
 22450   *  </md-button>
 22451   *  <md-menu-content>
 22452   *    <md-menu-item><md-button ng-click="doSomething()">Do Something</md-button></md-menu-item>
 22453   *  </md-menu-content>
 22454   * </md-menu>
 22455   * </hljs>
 22456   *
 22457   * @param {string} md-position-mode The position mode in the form of
 22458   *           `x`, `y`. Default value is `target`,`target`. Right now the `x` axis
 22459   *           also supports `target-right`.
 22460   * @param {string} md-offset An offset to apply to the dropdown after positioning
 22461   *           `x`, `y`. Default value is `0`,`0`.
 22462   *
 22463   */
 22464  
 22465  angular
 22466      .module('material.components.menu')
 22467      .directive('mdMenu', MenuDirective);
 22468  
 22469  /**
 22470   * @ngInject
 22471   */
 22472  function MenuDirective($mdUtil) {
 22473    var INVALID_PREFIX = 'Invalid HTML for md-menu: ';
 22474    return {
 22475      restrict: 'E',
 22476      require: ['mdMenu', '?^mdMenuBar'],
 22477      controller: 'mdMenuCtrl', // empty function to be built by link
 22478      scope: true,
 22479      compile: compile
 22480    };
 22481  
 22482    function compile(templateElement) {
 22483      templateElement.addClass('md-menu');
 22484      var triggerElement = templateElement.children()[0];
 22485      if (!triggerElement.hasAttribute('ng-click')) {
 22486        triggerElement = triggerElement.querySelector('[ng-click],[ng-mouseenter]') || triggerElement;
 22487      }
 22488      if (triggerElement && (
 22489        triggerElement.nodeName == 'MD-BUTTON' ||
 22490        triggerElement.nodeName == 'BUTTON'
 22491      ) && !triggerElement.hasAttribute('type')) {
 22492        triggerElement.setAttribute('type', 'button');
 22493      }
 22494  
 22495      if (templateElement.children().length != 2) {
 22496        throw Error(INVALID_PREFIX + 'Expected two children elements.');
 22497      }
 22498  
 22499      // Default element for ARIA attributes has the ngClick or ngMouseenter expression
 22500      triggerElement && triggerElement.setAttribute('aria-haspopup', 'true');
 22501  
 22502      var nestedMenus = templateElement[0].querySelectorAll('md-menu');
 22503      var nestingDepth = parseInt(templateElement[0].getAttribute('md-nest-level'), 10) || 0;
 22504      if (nestedMenus) {
 22505        angular.forEach($mdUtil.nodesToArray(nestedMenus), function(menuEl) {
 22506          if (!menuEl.hasAttribute('md-position-mode')) {
 22507            menuEl.setAttribute('md-position-mode', 'cascade');
 22508          }
 22509          menuEl.classList.add('md-nested-menu');
 22510          menuEl.setAttribute('md-nest-level', nestingDepth + 1);
 22511        });
 22512      }
 22513      return link;
 22514    }
 22515  
 22516    function link(scope, element, attrs, ctrls) {
 22517      var mdMenuCtrl = ctrls[0];
 22518      var isInMenuBar = ctrls[1] != undefined;
 22519      // Move everything into a md-menu-container and pass it to the controller
 22520      var menuContainer = angular.element(
 22521        '<div class="md-open-menu-container md-whiteframe-z2"></div>'
 22522      );
 22523      var menuContents = element.children()[1];
 22524      if (!menuContents.hasAttribute('role')) {
 22525        menuContents.setAttribute('role', 'menu');
 22526      }
 22527      menuContainer.append(menuContents);
 22528  
 22529      element.on('$destroy', function() {
 22530        menuContainer.remove();
 22531      });
 22532  
 22533      element.append(menuContainer);
 22534      menuContainer[0].style.display = 'none';
 22535      mdMenuCtrl.init(menuContainer, { isInMenuBar: isInMenuBar });
 22536    }
 22537  }
 22538  MenuDirective.$inject = ["$mdUtil"];
 22539  
 22540  })();
 22541  (function(){
 22542  "use strict";
 22543  
 22544  angular
 22545    .module('material.components.menu')
 22546    .provider('$mdMenu', MenuProvider);
 22547  
 22548  /*
 22549   * Interim element provider for the menu.
 22550   * Handles behavior for a menu while it is open, including:
 22551   *    - handling animating the menu opening/closing
 22552   *    - handling key/mouse events on the menu element
 22553   *    - handling enabling/disabling scroll while the menu is open
 22554   *    - handling redrawing during resizes and orientation changes
 22555   *
 22556   */
 22557  
 22558  function MenuProvider($$interimElementProvider) {
 22559    var MENU_EDGE_MARGIN = 8;
 22560  
 22561    menuDefaultOptions.$inject = ["$mdUtil", "$mdTheming", "$mdConstant", "$document", "$window", "$q", "$$rAF", "$animateCss", "$animate"];
 22562    return $$interimElementProvider('$mdMenu')
 22563      .setDefaults({
 22564        methods: ['target'],
 22565        options: menuDefaultOptions
 22566      });
 22567  
 22568    /* @ngInject */
 22569    function menuDefaultOptions($mdUtil, $mdTheming, $mdConstant, $document, $window, $q, $$rAF, $animateCss, $animate) {
 22570      var animator = $mdUtil.dom.animator;
 22571  
 22572      return {
 22573        parent: 'body',
 22574        onShow: onShow,
 22575        onRemove: onRemove,
 22576        hasBackdrop: true,
 22577        disableParentScroll: true,
 22578        skipCompile: true,
 22579        preserveScope: true,
 22580        skipHide: true,
 22581        themable: true
 22582      };
 22583  
 22584      /**
 22585       * Show modal backdrop element...
 22586       * @returns {function(): void} A function that removes this backdrop
 22587       */
 22588      function showBackdrop(scope, element, options) {
 22589        if (options.nestLevel) return angular.noop;
 22590  
 22591        // If we are not within a dialog...
 22592        if (options.disableParentScroll && !$mdUtil.getClosest(options.target, 'MD-DIALOG')) {
 22593          // !! DO this before creating the backdrop; since disableScrollAround()
 22594          //    configures the scroll offset; which is used by mdBackDrop postLink()
 22595          options.restoreScroll = $mdUtil.disableScrollAround(options.element, options.parent);
 22596        } else {
 22597          options.disableParentScroll = false;
 22598        }
 22599  
 22600        if (options.hasBackdrop) {
 22601          options.backdrop = $mdUtil.createBackdrop(scope, "md-menu-backdrop md-click-catcher");
 22602  
 22603          $animate.enter(options.backdrop, $document[0].body);
 22604        }
 22605  
 22606        /**
 22607         * Hide and destroys the backdrop created by showBackdrop()
 22608         */
 22609        return function hideBackdrop() {
 22610          if (options.backdrop) options.backdrop.remove();
 22611          if (options.disableParentScroll) options.restoreScroll();
 22612        };
 22613      }
 22614  
 22615      /**
 22616       * Removing the menu element from the DOM and remove all associated event listeners
 22617       * and backdrop
 22618       */
 22619      function onRemove(scope, element, opts) {
 22620        opts.cleanupInteraction();
 22621        opts.cleanupResizing();
 22622        opts.hideBackdrop();
 22623  
 22624        // For navigation $destroy events, do a quick, non-animated removal,
 22625        // but for normal closes (from clicks, etc) animate the removal
 22626  
 22627        return (opts.$destroy === true) ? detachAndClean() : animateRemoval().then( detachAndClean );
 22628  
 22629        /**
 22630         * For normal closes, animate the removal.
 22631         * For forced closes (like $destroy events), skip the animations
 22632         */
 22633        function animateRemoval() {
 22634          return $animateCss(element, {addClass: 'md-leave'}).start();
 22635        }
 22636  
 22637        /**
 22638         * Detach the element
 22639         */
 22640        function detachAndClean() {
 22641          element.removeClass('md-active');
 22642          detachElement(element, opts);
 22643          opts.alreadyOpen = false;
 22644        }
 22645  
 22646      }
 22647  
 22648      /**
 22649       * Inserts and configures the staged Menu element into the DOM, positioning it,
 22650       * and wiring up various interaction events
 22651       */
 22652      function onShow(scope, element, opts) {
 22653        sanitizeAndConfigure(opts);
 22654  
 22655        // Wire up theming on our menu element
 22656        $mdTheming.inherit(opts.menuContentEl, opts.target);
 22657  
 22658        // Register various listeners to move menu on resize/orientation change
 22659        opts.cleanupResizing = startRepositioningOnResize();
 22660        opts.hideBackdrop = showBackdrop(scope, element, opts);
 22661  
 22662        // Return the promise for when our menu is done animating in
 22663        return showMenu()
 22664          .then(function(response) {
 22665            opts.alreadyOpen = true;
 22666            opts.cleanupInteraction = activateInteraction();
 22667            return response;
 22668          });
 22669  
 22670        /**
 22671         * Place the menu into the DOM and call positioning related functions
 22672         */
 22673        function showMenu() {
 22674          opts.parent.append(element);
 22675          element[0].style.display = '';
 22676  
 22677          return $q(function(resolve) {
 22678            var position = calculateMenuPosition(element, opts);
 22679  
 22680            element.removeClass('md-leave');
 22681  
 22682            // Animate the menu scaling, and opacity [from its position origin (default == top-left)]
 22683            // to normal scale.
 22684            $animateCss(element, {
 22685              addClass: 'md-active',
 22686              from: animator.toCss(position),
 22687              to: animator.toCss({transform: ''})
 22688            })
 22689            .start()
 22690            .then(resolve);
 22691  
 22692          });
 22693        }
 22694  
 22695        /**
 22696         * Check for valid opts and set some sane defaults
 22697         */
 22698        function sanitizeAndConfigure() {
 22699          if (!opts.target) {
 22700            throw Error(
 22701              '$mdMenu.show() expected a target to animate from in options.target'
 22702            );
 22703          }
 22704          angular.extend(opts, {
 22705            alreadyOpen: false,
 22706            isRemoved: false,
 22707            target: angular.element(opts.target), //make sure it's not a naked dom node
 22708            parent: angular.element(opts.parent),
 22709            menuContentEl: angular.element(element[0].querySelector('md-menu-content'))
 22710          });
 22711        }
 22712  
 22713        /**
 22714         * Configure various resize listeners for screen changes
 22715         */
 22716        function startRepositioningOnResize() {
 22717  
 22718          var repositionMenu = (function(target, options) {
 22719            return $$rAF.throttle(function() {
 22720              if (opts.isRemoved) return;
 22721              var position = calculateMenuPosition(target, options);
 22722  
 22723              target.css(animator.toCss(position));
 22724            });
 22725          })(element, opts);
 22726  
 22727          $window.addEventListener('resize', repositionMenu);
 22728          $window.addEventListener('orientationchange', repositionMenu);
 22729  
 22730          return function stopRepositioningOnResize() {
 22731  
 22732            // Disable resizing handlers
 22733            $window.removeEventListener('resize', repositionMenu);
 22734            $window.removeEventListener('orientationchange', repositionMenu);
 22735  
 22736          }
 22737        }
 22738  
 22739        /**
 22740         * Activate interaction on the menu. Wire up keyboard listerns for
 22741         * clicks, keypresses, backdrop closing, etc.
 22742         */
 22743        function activateInteraction() {
 22744          element.addClass('md-clickable');
 22745  
 22746          // close on backdrop click
 22747          if (opts.backdrop) opts.backdrop.on('click', onBackdropClick);
 22748  
 22749          // Wire up keyboard listeners.
 22750          // - Close on escape,
 22751          // - focus next item on down arrow,
 22752          // - focus prev item on up
 22753          opts.menuContentEl.on('keydown', onMenuKeyDown);
 22754          opts.menuContentEl[0].addEventListener('click', captureClickListener, true);
 22755  
 22756          // kick off initial focus in the menu on the first element
 22757          var focusTarget = opts.menuContentEl[0].querySelector('[md-menu-focus-target]');
 22758          if ( !focusTarget ) {
 22759            var firstChild = opts.menuContentEl[0].firstElementChild;
 22760  
 22761            focusTarget = firstChild && (firstChild.querySelector('.md-button:not([disabled])') || firstChild.firstElementChild);
 22762          }
 22763  
 22764          focusTarget && focusTarget.focus();
 22765  
 22766          return function cleanupInteraction() {
 22767            element.removeClass('md-clickable');
 22768            if (opts.backdrop) opts.backdrop.off('click', onBackdropClick);
 22769            opts.menuContentEl.off('keydown', onMenuKeyDown);
 22770            opts.menuContentEl[0].removeEventListener('click', captureClickListener, true);
 22771          };
 22772  
 22773          // ************************************
 22774          // internal functions
 22775          // ************************************
 22776  
 22777          function onMenuKeyDown(ev) {
 22778            var handled;
 22779            switch (ev.keyCode) {
 22780              case $mdConstant.KEY_CODE.ESCAPE:
 22781                opts.mdMenuCtrl.close(false, { closeAll: true });
 22782                handled = true;
 22783                break;
 22784              case $mdConstant.KEY_CODE.UP_ARROW:
 22785                if (!focusMenuItem(ev, opts.menuContentEl, opts, -1) && !opts.nestLevel) {
 22786                  opts.mdMenuCtrl.triggerContainerProxy(ev);
 22787                }
 22788                handled = true;
 22789                break;
 22790              case $mdConstant.KEY_CODE.DOWN_ARROW:
 22791                if (!focusMenuItem(ev, opts.menuContentEl, opts, 1) && !opts.nestLevel) {
 22792                  opts.mdMenuCtrl.triggerContainerProxy(ev);
 22793                }
 22794                handled = true;
 22795                break;
 22796              case $mdConstant.KEY_CODE.LEFT_ARROW:
 22797                if (opts.nestLevel) {
 22798                  opts.mdMenuCtrl.close();
 22799                } else {
 22800                  opts.mdMenuCtrl.triggerContainerProxy(ev);
 22801                }
 22802                handled = true;
 22803                break;
 22804              case $mdConstant.KEY_CODE.RIGHT_ARROW:
 22805                var parentMenu = $mdUtil.getClosest(ev.target, 'MD-MENU');
 22806                if (parentMenu && parentMenu != opts.parent[0]) {
 22807                  ev.target.click();
 22808                } else {
 22809                  opts.mdMenuCtrl.triggerContainerProxy(ev);
 22810                }
 22811                handled = true;
 22812                break;
 22813            }
 22814            if (handled) {
 22815              ev.preventDefault();
 22816              ev.stopImmediatePropagation();
 22817            }
 22818          }
 22819  
 22820          function onBackdropClick(e) {
 22821            e.preventDefault();
 22822            e.stopPropagation();
 22823            scope.$apply(function() {
 22824              opts.mdMenuCtrl.close(true, { closeAll: true });
 22825            });
 22826          }
 22827  
 22828          // Close menu on menu item click, if said menu-item is not disabled
 22829          function captureClickListener(e) {
 22830            var target = e.target;
 22831            // Traverse up the event until we get to the menuContentEl to see if
 22832            // there is an ng-click and that the ng-click is not disabled
 22833            do {
 22834              if (target == opts.menuContentEl[0]) return;
 22835              if ((hasAnyAttribute(target, ['ng-click', 'ng-href', 'ui-sref']) ||
 22836                  target.nodeName == 'BUTTON' || target.nodeName == 'MD-BUTTON') && !hasAnyAttribute(target, ['md-prevent-menu-close'])) {
 22837                var closestMenu = $mdUtil.getClosest(target, 'MD-MENU');
 22838                if (!target.hasAttribute('disabled') && (!closestMenu || closestMenu == opts.parent[0])) {
 22839                  close();
 22840                }
 22841                break;
 22842              }
 22843            } while (target = target.parentNode)
 22844  
 22845            function close() {
 22846              scope.$apply(function() {
 22847                opts.mdMenuCtrl.close(true, { closeAll: true });
 22848              });
 22849            }
 22850  
 22851            function hasAnyAttribute(target, attrs) {
 22852              if (!target) return false;
 22853              for (var i = 0, attr; attr = attrs[i]; ++i) {
 22854                var altForms = [attr, 'data-' + attr, 'x-' + attr];
 22855                for (var j = 0, rawAttr; rawAttr = altForms[j]; ++j) {
 22856                  if (target.hasAttribute(rawAttr)) {
 22857                    return true;
 22858                  }
 22859                }
 22860              }
 22861              return false;
 22862            }
 22863          }
 22864  
 22865          opts.menuContentEl[0].addEventListener('click', captureClickListener, true);
 22866  
 22867          return function cleanupInteraction() {
 22868            element.removeClass('md-clickable');
 22869            opts.menuContentEl.off('keydown');
 22870            opts.menuContentEl[0].removeEventListener('click', captureClickListener, true);
 22871          };
 22872        }
 22873      }
 22874  
 22875      /**
 22876       * Takes a keypress event and focuses the next/previous menu
 22877       * item from the emitting element
 22878       * @param {event} e - The origin keypress event
 22879       * @param {angular.element} menuEl - The menu element
 22880       * @param {object} opts - The interim element options for the mdMenu
 22881       * @param {number} direction - The direction to move in (+1 = next, -1 = prev)
 22882       */
 22883      function focusMenuItem(e, menuEl, opts, direction) {
 22884        var currentItem = $mdUtil.getClosest(e.target, 'MD-MENU-ITEM');
 22885  
 22886        var items = $mdUtil.nodesToArray(menuEl[0].children);
 22887        var currentIndex = items.indexOf(currentItem);
 22888  
 22889        // Traverse through our elements in the specified direction (+/-1) and try to
 22890        // focus them until we find one that accepts focus
 22891        var didFocus;
 22892        for (var i = currentIndex + direction; i >= 0 && i < items.length; i = i + direction) {
 22893          var focusTarget = items[i].querySelector('.md-button');
 22894          didFocus = attemptFocus(focusTarget);
 22895          if (didFocus) {
 22896            break;
 22897          }
 22898        }
 22899        return didFocus;
 22900      }
 22901  
 22902      /**
 22903       * Attempts to focus an element. Checks whether that element is the currently
 22904       * focused element after attempting.
 22905       * @param {HTMLElement} el - the element to attempt focus on
 22906       * @returns {bool} - whether the element was successfully focused
 22907       */
 22908      function attemptFocus(el) {
 22909        if (el && el.getAttribute('tabindex') != -1) {
 22910          el.focus();
 22911          return ($document[0].activeElement == el);
 22912        }
 22913      }
 22914  
 22915      /**
 22916       * Use browser to remove this element without triggering a $destroy event
 22917       */
 22918      function detachElement(element, opts) {
 22919        if (!opts.preserveElement) {
 22920          if (toNode(element).parentNode === toNode(opts.parent)) {
 22921            toNode(opts.parent).removeChild(toNode(element));
 22922          }
 22923        } else {
 22924          toNode(element).style.display = 'none';
 22925        }
 22926      }
 22927  
 22928      /**
 22929       * Computes menu position and sets the style on the menu container
 22930       * @param {HTMLElement} el - the menu container element
 22931       * @param {object} opts - the interim element options object
 22932       */
 22933      function calculateMenuPosition(el, opts) {
 22934  
 22935        var containerNode = el[0],
 22936          openMenuNode = el[0].firstElementChild,
 22937          openMenuNodeRect = openMenuNode.getBoundingClientRect(),
 22938          boundryNode = $document[0].body,
 22939          boundryNodeRect = boundryNode.getBoundingClientRect();
 22940  
 22941        var menuStyle = $window.getComputedStyle(openMenuNode);
 22942  
 22943        var originNode = opts.target[0].querySelector('[md-menu-origin]') || opts.target[0],
 22944          originNodeRect = originNode.getBoundingClientRect();
 22945  
 22946        var bounds = {
 22947          left: boundryNodeRect.left + MENU_EDGE_MARGIN,
 22948          top: Math.max(boundryNodeRect.top, 0) + MENU_EDGE_MARGIN,
 22949          bottom: Math.max(boundryNodeRect.bottom, Math.max(boundryNodeRect.top, 0) + boundryNodeRect.height) - MENU_EDGE_MARGIN,
 22950          right: boundryNodeRect.right - MENU_EDGE_MARGIN
 22951        };
 22952  
 22953        var alignTarget, alignTargetRect = { top:0, left : 0, right:0, bottom:0 }, existingOffsets  = { top:0, left : 0, right:0, bottom:0  };
 22954        var positionMode = opts.mdMenuCtrl.positionMode();
 22955  
 22956        if (positionMode.top == 'target' || positionMode.left == 'target' || positionMode.left == 'target-right') {
 22957          alignTarget = firstVisibleChild();
 22958          if ( alignTarget ) {
 22959            // TODO: Allow centering on an arbitrary node, for now center on first menu-item's child
 22960            alignTarget = alignTarget.firstElementChild || alignTarget;
 22961            alignTarget = alignTarget.querySelector('[md-menu-align-target]') || alignTarget;
 22962            alignTargetRect = alignTarget.getBoundingClientRect();
 22963  
 22964            existingOffsets = {
 22965              top: parseFloat(containerNode.style.top || 0),
 22966              left: parseFloat(containerNode.style.left || 0)
 22967            };
 22968          }
 22969        }
 22970  
 22971        var position = {};
 22972        var transformOrigin = 'top ';
 22973  
 22974        switch (positionMode.top) {
 22975          case 'target':
 22976            position.top = existingOffsets.top + originNodeRect.top - alignTargetRect.top;
 22977            break;
 22978          case 'cascade':
 22979            position.top = originNodeRect.top - parseFloat(menuStyle.paddingTop) - originNode.style.top;
 22980            break;
 22981          case 'bottom':
 22982            position.top = originNodeRect.top + originNodeRect.height;
 22983            break;
 22984          default:
 22985            throw new Error('Invalid target mode "' + positionMode.top + '" specified for md-menu on Y axis.');
 22986        }
 22987  
 22988        switch (positionMode.left) {
 22989          case 'target':
 22990            position.left = existingOffsets.left + originNodeRect.left - alignTargetRect.left;
 22991            transformOrigin += 'left';
 22992            break;
 22993          case 'target-right':
 22994            position.left = originNodeRect.right - openMenuNodeRect.width + (openMenuNodeRect.right - alignTargetRect.right);
 22995            transformOrigin += 'right';
 22996            break;
 22997          case 'cascade':
 22998            var willFitRight = (originNodeRect.right + openMenuNodeRect.width) < bounds.right;
 22999            position.left = willFitRight ? originNodeRect.right - originNode.style.left : originNodeRect.left - originNode.style.left - openMenuNodeRect.width;
 23000            transformOrigin += willFitRight ? 'left' : 'right';
 23001            break;
 23002          case 'left':
 23003            position.left = originNodeRect.left;
 23004            transformOrigin += 'left';
 23005            break;
 23006          default:
 23007            throw new Error('Invalid target mode "' + positionMode.left + '" specified for md-menu on X axis.');
 23008        }
 23009  
 23010        var offsets = opts.mdMenuCtrl.offsets();
 23011        position.top += offsets.top;
 23012        position.left += offsets.left;
 23013  
 23014        clamp(position);
 23015  
 23016        var scaleX = Math.round(100 * Math.min(originNodeRect.width / containerNode.offsetWidth, 1.0)) / 100;
 23017        var scaleY = Math.round(100 * Math.min(originNodeRect.height / containerNode.offsetHeight, 1.0)) / 100;
 23018  
 23019        return {
 23020          top: Math.round(position.top),
 23021          left: Math.round(position.left),
 23022          // Animate a scale out if we aren't just repositioning
 23023          transform: !opts.alreadyOpen ? $mdUtil.supplant('scale({0},{1})', [scaleX, scaleY]) : undefined,
 23024          transformOrigin: transformOrigin
 23025        };
 23026  
 23027        /**
 23028         * Clamps the repositioning of the menu within the confines of
 23029         * bounding element (often the screen/body)
 23030         */
 23031        function clamp(pos) {
 23032          pos.top = Math.max(Math.min(pos.top, bounds.bottom - containerNode.offsetHeight), bounds.top);
 23033          pos.left = Math.max(Math.min(pos.left, bounds.right - containerNode.offsetWidth), bounds.left);
 23034        }
 23035  
 23036        /**
 23037         * Gets the first visible child in the openMenuNode
 23038         * Necessary incase menu nodes are being dynamically hidden
 23039         */
 23040        function firstVisibleChild() {
 23041          for (var i = 0; i < openMenuNode.children.length; ++i) {
 23042            if ($window.getComputedStyle(openMenuNode.children[i]).display != 'none') {
 23043              return openMenuNode.children[i];
 23044            }
 23045          }
 23046        }
 23047      }
 23048    }
 23049    function toNode(el) {
 23050      if (el instanceof angular.element) {
 23051        el = el[0];
 23052      }
 23053      return el;
 23054    }
 23055  }
 23056  MenuProvider.$inject = ["$$interimElementProvider"];
 23057  
 23058  })();
 23059  (function(){
 23060  "use strict";
 23061  
 23062  
 23063  angular
 23064    .module('material.components.menuBar')
 23065    .controller('MenuBarController', MenuBarController);
 23066  
 23067  var BOUND_MENU_METHODS = ['handleKeyDown', 'handleMenuHover', 'scheduleOpenHoveredMenu', 'cancelScheduledOpen'];
 23068  
 23069  /**
 23070   * @ngInject
 23071   */
 23072  function MenuBarController($scope, $rootScope, $element, $attrs, $mdConstant, $document, $mdUtil, $timeout) {
 23073    this.$element = $element;
 23074    this.$attrs = $attrs;
 23075    this.$mdConstant = $mdConstant;
 23076    this.$mdUtil = $mdUtil;
 23077    this.$document = $document;
 23078    this.$scope = $scope;
 23079    this.$rootScope = $rootScope;
 23080    this.$timeout = $timeout;
 23081  
 23082    var self = this;
 23083    angular.forEach(BOUND_MENU_METHODS, function(methodName) {
 23084      self[methodName] = angular.bind(self, self[methodName]);
 23085    });
 23086  }
 23087  MenuBarController.$inject = ["$scope", "$rootScope", "$element", "$attrs", "$mdConstant", "$document", "$mdUtil", "$timeout"];
 23088  
 23089  MenuBarController.prototype.init = function() {
 23090    var $element = this.$element;
 23091    var $mdUtil = this.$mdUtil;
 23092    var $scope = this.$scope;
 23093  
 23094    var self = this;
 23095    var deregisterFns = [];
 23096    $element.on('keydown', this.handleKeyDown);
 23097    this.parentToolbar = $mdUtil.getClosest($element, 'MD-TOOLBAR');
 23098  
 23099    deregisterFns.push(this.$rootScope.$on('$mdMenuOpen', function(event, el) {
 23100      if (self.getMenus().indexOf(el[0]) != -1) {
 23101        $element[0].classList.add('md-open');
 23102        el[0].classList.add('md-open');
 23103        self.currentlyOpenMenu = el.controller('mdMenu');
 23104        self.currentlyOpenMenu.registerContainerProxy(self.handleKeyDown);
 23105        self.enableOpenOnHover();
 23106      }
 23107    }));
 23108  
 23109    deregisterFns.push(this.$rootScope.$on('$mdMenuClose', function(event, el, opts) {
 23110      var rootMenus = self.getMenus();
 23111      if (rootMenus.indexOf(el[0]) != -1) {
 23112        $element[0].classList.remove('md-open');
 23113        el[0].classList.remove('md-open');
 23114      }
 23115  
 23116      if ($element[0].contains(el[0])) {
 23117        var parentMenu = el[0];
 23118        while (parentMenu && rootMenus.indexOf(parentMenu) == -1) {
 23119          parentMenu = $mdUtil.getClosest(parentMenu, 'MD-MENU', true);
 23120        }
 23121        if (parentMenu) {
 23122          if (!opts.skipFocus) parentMenu.querySelector('button:not([disabled])').focus();
 23123          self.currentlyOpenMenu = undefined;
 23124          self.disableOpenOnHover();
 23125          self.setKeyboardMode(true);
 23126        }
 23127      }
 23128    }));
 23129  
 23130    $scope.$on('$destroy', function() {
 23131      while (deregisterFns.length) {
 23132        deregisterFns.shift()();
 23133      }
 23134    });
 23135  
 23136  
 23137    this.setKeyboardMode(true);
 23138  };
 23139  
 23140  MenuBarController.prototype.setKeyboardMode = function(enabled) {
 23141    if (enabled) this.$element[0].classList.add('md-keyboard-mode');
 23142    else this.$element[0].classList.remove('md-keyboard-mode');
 23143  };
 23144  
 23145  MenuBarController.prototype.enableOpenOnHover = function() {
 23146    if (this.openOnHoverEnabled) return;
 23147    this.openOnHoverEnabled = true;
 23148  
 23149    var parentToolbar;
 23150    if (parentToolbar = this.parentToolbar) {
 23151      parentToolbar.dataset.mdRestoreStyle = parentToolbar.getAttribute('style');
 23152      parentToolbar.style.position = 'relative';
 23153      parentToolbar.style.zIndex = 100;
 23154    }
 23155    angular
 23156      .element(this.getMenus())
 23157      .on('mouseenter', this.handleMenuHover);
 23158  };
 23159  
 23160  MenuBarController.prototype.handleMenuHover = function(e) {
 23161    this.setKeyboardMode(false);
 23162    if (this.openOnHoverEnabled) {
 23163      this.scheduleOpenHoveredMenu(e);
 23164    }
 23165  };
 23166  
 23167  
 23168  MenuBarController.prototype.disableOpenOnHover = function() {
 23169    if (!this.openOnHoverEnabled) return;
 23170    this.openOnHoverEnabled = false;
 23171    var parentToolbar;
 23172    if (parentToolbar = this.parentToolbar) {
 23173      parentToolbar.style.cssText = parentToolbar.dataset.mdRestoreStyle || '';
 23174    }
 23175    angular
 23176      .element(this.getMenus())
 23177      .off('mouseenter', this.handleMenuHover);
 23178  };
 23179  
 23180  MenuBarController.prototype.scheduleOpenHoveredMenu = function(e) {
 23181    var menuEl = angular.element(e.currentTarget);
 23182    var menuCtrl = menuEl.controller('mdMenu');
 23183    this.setKeyboardMode(false);
 23184    this.scheduleOpenMenu(menuCtrl);
 23185  };
 23186  
 23187  MenuBarController.prototype.scheduleOpenMenu = function(menuCtrl) {
 23188    var self = this;
 23189    var $timeout = this.$timeout;
 23190    if (menuCtrl != self.currentlyOpenMenu) {
 23191      $timeout.cancel(self.pendingMenuOpen);
 23192      self.pendingMenuOpen = $timeout(function() {
 23193        self.pendingMenuOpen = undefined;
 23194        if (self.currentlyOpenMenu) {
 23195          self.currentlyOpenMenu.close(true, { closeAll: true });
 23196        }
 23197        menuCtrl.open();
 23198      }, 200, false);
 23199    }
 23200  };
 23201  
 23202  MenuBarController.prototype.handleKeyDown = function(e) {
 23203    var keyCodes = this.$mdConstant.KEY_CODE;
 23204    var currentMenu = this.currentlyOpenMenu;
 23205    var wasOpen = currentMenu && currentMenu.isOpen;
 23206    this.setKeyboardMode(true);
 23207    var handled, newMenu, newMenuCtrl;
 23208    switch (e.keyCode) {
 23209      case keyCodes.DOWN_ARROW:
 23210        if (currentMenu) {
 23211          currentMenu.focusMenuContainer();
 23212        } else {
 23213          this.openFocusedMenu();
 23214        }
 23215        handled = true;
 23216        break;
 23217      case keyCodes.UP_ARROW:
 23218        currentMenu && currentMenu.close();
 23219        handled = true;
 23220        break;
 23221      case keyCodes.LEFT_ARROW:
 23222        newMenu = this.focusMenu(-1);
 23223        if (wasOpen) {
 23224          newMenuCtrl = angular.element(newMenu).controller('mdMenu');
 23225          this.scheduleOpenMenu(newMenuCtrl);
 23226        }
 23227        handled = true;
 23228        break;
 23229      case keyCodes.RIGHT_ARROW:
 23230        newMenu = this.focusMenu(+1);
 23231        if (wasOpen) {
 23232          newMenuCtrl = angular.element(newMenu).controller('mdMenu');
 23233          this.scheduleOpenMenu(newMenuCtrl);
 23234        }
 23235        handled = true;
 23236        break;
 23237    }
 23238    if (handled) {
 23239      e && e.preventDefault && e.preventDefault();
 23240      e && e.stopImmediatePropagation && e.stopImmediatePropagation();
 23241    }
 23242  };
 23243  
 23244  MenuBarController.prototype.focusMenu = function(direction) {
 23245    var menus = this.getMenus();
 23246    var focusedIndex = this.getFocusedMenuIndex();
 23247  
 23248    if (focusedIndex == -1) { focusedIndex = this.getOpenMenuIndex(); }
 23249  
 23250    var changed = false;
 23251  
 23252    if (focusedIndex == -1) { focusedIndex = 0; changed = true; }
 23253    else if (
 23254      direction < 0 && focusedIndex > 0 ||
 23255      direction > 0 && focusedIndex < menus.length - direction
 23256    ) {
 23257      focusedIndex += direction;
 23258      changed = true;
 23259    }
 23260    if (changed) {
 23261      menus[focusedIndex].querySelector('button').focus();
 23262      return menus[focusedIndex];
 23263    }
 23264  };
 23265  
 23266  MenuBarController.prototype.openFocusedMenu = function() {
 23267    var menu = this.getFocusedMenu();
 23268    menu && angular.element(menu).controller('mdMenu').open();
 23269  };
 23270  
 23271  MenuBarController.prototype.getMenus = function() {
 23272    var $element = this.$element;
 23273    return this.$mdUtil.nodesToArray($element[0].children)
 23274      .filter(function(el) { return el.nodeName == 'MD-MENU'; });
 23275  };
 23276  
 23277  MenuBarController.prototype.getFocusedMenu = function() {
 23278    return this.getMenus()[this.getFocusedMenuIndex()];
 23279  };
 23280  
 23281  MenuBarController.prototype.getFocusedMenuIndex = function() {
 23282    var $mdUtil = this.$mdUtil;
 23283    var focusedEl = $mdUtil.getClosest(
 23284      this.$document[0].activeElement,
 23285      'MD-MENU'
 23286    );
 23287    if (!focusedEl) return -1;
 23288  
 23289    var focusedIndex = this.getMenus().indexOf(focusedEl);
 23290    return focusedIndex;
 23291  
 23292  };
 23293  
 23294  MenuBarController.prototype.getOpenMenuIndex = function() {
 23295    var menus = this.getMenus();
 23296    for (var i = 0; i < menus.length; ++i) {
 23297      if (menus[i].classList.contains('md-open')) return i;
 23298    }
 23299    return -1;
 23300  };
 23301  
 23302  
 23303  
 23304  
 23305  
 23306  
 23307  
 23308  
 23309  
 23310  })();
 23311  (function(){
 23312  "use strict";
 23313  
 23314  /**
 23315   * @ngdoc directive
 23316   * @name mdMenuBar
 23317   * @module material.components.menu-bar
 23318   * @restrict E
 23319   * @description
 23320   *
 23321   * Menu bars are containers that hold multiple menus. They change the behavior and appearance
 23322   * of the `md-menu` directive to behave similar to an operating system provided menu.
 23323   *
 23324   * @usage
 23325   * <hljs lang="html">
 23326   * <md-menu-bar>
 23327   *   <md-menu>
 23328   *     <button ng-click="$mdOpenMenu()">
 23329   *       File
 23330   *     </button>
 23331   *     <md-menu-content>
 23332   *       <md-menu-item>
 23333   *         <md-button ng-click="ctrl.sampleAction('share', $event)">
 23334   *           Share...
 23335   *         </md-button>
 23336   *       </md-menu-item>
 23337   *       <md-menu-divider></md-menu-divider>
 23338   *       <md-menu-item>
 23339   *       <md-menu-item>
 23340   *         <md-menu>
 23341   *           <md-button ng-click="$mdOpenMenu()">New</md-button>
 23342   *           <md-menu-content>
 23343   *             <md-menu-item><md-button ng-click="ctrl.sampleAction('New Document', $event)">Document</md-button></md-menu-item>
 23344   *             <md-menu-item><md-button ng-click="ctrl.sampleAction('New Spreadsheet', $event)">Spreadsheet</md-button></md-menu-item>
 23345   *             <md-menu-item><md-button ng-click="ctrl.sampleAction('New Presentation', $event)">Presentation</md-button></md-menu-item>
 23346   *             <md-menu-item><md-button ng-click="ctrl.sampleAction('New Form', $event)">Form</md-button></md-menu-item>
 23347   *             <md-menu-item><md-button ng-click="ctrl.sampleAction('New Drawing', $event)">Drawing</md-button></md-menu-item>
 23348   *           </md-menu-content>
 23349   *         </md-menu>
 23350   *       </md-menu-item>
 23351   *     </md-menu-content>
 23352   *   </md-menu>
 23353   * </md-menu-bar>
 23354   * </hljs>
 23355   *
 23356   * ## Menu Bar Controls
 23357   *
 23358   * You may place `md-menu-items` that function as controls within menu bars.
 23359   * There are two modes that are exposed via the `type` attribute of the `md-menu-item`.
 23360   * `type="checkbox"` will function as a boolean control for the `ng-model` attribute of the
 23361   * `md-menu-item`. `type="radio"` will function like a radio button, setting the `ngModel`
 23362   * to the `string` value of the `value` attribute. If you need non-string values, you can use
 23363   * `ng-value` to provide an expression (this is similar to how angular's native `input[type=radio]` works.
 23364   *
 23365   * <hljs lang="html">
 23366   * <md-menu-bar>
 23367   *  <md-menu>
 23368   *    <button ng-click="$mdOpenMenu()">
 23369   *      Sample Menu
 23370   *    </button>
 23371   *    <md-menu-content>
 23372   *      <md-menu-item type="checkbox" ng-model="settings.allowChanges">Allow changes</md-menu-item>
 23373   *      <md-menu-divider></md-menu-divider>
 23374   *      <md-menu-item type="radio" ng-model="settings.mode" ng-value="1">Mode 1</md-menu-item>
 23375   *      <md-menu-item type="radio" ng-model="settings.mode" ng-value="1">Mode 2</md-menu-item>
 23376   *      <md-menu-item type="radio" ng-model="settings.mode" ng-value="1">Mode 3</md-menu-item>
 23377   *    </md-menu-content>
 23378   *  </md-menu>
 23379   * </md-menu-bar>
 23380   * </hljs>
 23381   *
 23382   *
 23383   * ### Nesting Menus
 23384   *
 23385   * Menus may be nested within menu bars. This is commonly called cascading menus.
 23386   * To nest a menu place the nested menu inside the content of the `md-menu-item`.
 23387   * <hljs lang="html">
 23388   * <md-menu-item>
 23389   *   <md-menu>
 23390   *     <button ng-click="$mdOpenMenu()">New</md-button>
 23391   *     <md-menu-content>
 23392   *       <md-menu-item><md-button ng-click="ctrl.sampleAction('New Document', $event)">Document</md-button></md-menu-item>
 23393   *       <md-menu-item><md-button ng-click="ctrl.sampleAction('New Spreadsheet', $event)">Spreadsheet</md-button></md-menu-item>
 23394   *       <md-menu-item><md-button ng-click="ctrl.sampleAction('New Presentation', $event)">Presentation</md-button></md-menu-item>
 23395   *       <md-menu-item><md-button ng-click="ctrl.sampleAction('New Form', $event)">Form</md-button></md-menu-item>
 23396   *       <md-menu-item><md-button ng-click="ctrl.sampleAction('New Drawing', $event)">Drawing</md-button></md-menu-item>
 23397   *     </md-menu-content>
 23398   *   </md-menu>
 23399   * </md-menu-item>
 23400   * </hljs>
 23401   *
 23402   */
 23403  
 23404  angular
 23405    .module('material.components.menuBar')
 23406    .directive('mdMenuBar', MenuBarDirective);
 23407  
 23408  /* @ngInject */
 23409  function MenuBarDirective($mdUtil, $mdTheming) {
 23410    return {
 23411      restrict: 'E',
 23412      require: 'mdMenuBar',
 23413      controller: 'MenuBarController',
 23414  
 23415      compile: function compile(templateEl, templateAttrs) {
 23416        if (!templateAttrs.ariaRole) {
 23417          templateEl[0].setAttribute('role', 'menubar');
 23418        }
 23419        angular.forEach(templateEl[0].children, function(menuEl) {
 23420          if (menuEl.nodeName == 'MD-MENU') {
 23421            if (!menuEl.hasAttribute('md-position-mode')) {
 23422              menuEl.setAttribute('md-position-mode', 'left bottom');
 23423              menuEl.querySelector('button,a').setAttribute('role', 'menuitem');
 23424            }
 23425            var contentEls = $mdUtil.nodesToArray(menuEl.querySelectorAll('md-menu-content'));
 23426            angular.forEach(contentEls, function(contentEl) {
 23427              contentEl.classList.add('md-menu-bar-menu');
 23428              contentEl.classList.add('md-dense');
 23429              if (!contentEl.hasAttribute('width')) {
 23430                contentEl.setAttribute('width', 5);
 23431              }
 23432            });
 23433          }
 23434        });
 23435  
 23436        return function postLink(scope, el, attrs, ctrl) {
 23437          $mdTheming(scope, el);
 23438          ctrl.init();
 23439        };
 23440      }
 23441    };
 23442  
 23443  }
 23444  MenuBarDirective.$inject = ["$mdUtil", "$mdTheming"];
 23445  
 23446  })();
 23447  (function(){
 23448  "use strict";
 23449  
 23450  
 23451  angular
 23452    .module('material.components.menuBar')
 23453    .directive('mdMenuDivider', MenuDividerDirective);
 23454  
 23455  
 23456  function MenuDividerDirective() {
 23457    return {
 23458      restrict: 'E',
 23459      compile: function(templateEl, templateAttrs) {
 23460        if (!templateAttrs.role) {
 23461          templateEl[0].setAttribute('role', 'separator');
 23462        }
 23463      }
 23464    };
 23465  }
 23466  
 23467  })();
 23468  (function(){
 23469  "use strict";
 23470  
 23471  
 23472  angular
 23473    .module('material.components.menuBar')
 23474    .controller('MenuItemController', MenuItemController);
 23475  
 23476  
 23477  /**
 23478   * @ngInject
 23479   */
 23480  function MenuItemController($scope, $element, $attrs) {
 23481    this.$element = $element;
 23482    this.$attrs = $attrs;
 23483    this.$scope = $scope;
 23484  }
 23485  MenuItemController.$inject = ["$scope", "$element", "$attrs"];
 23486  
 23487  MenuItemController.prototype.init = function(ngModel) {
 23488    var $element = this.$element;
 23489    var $attrs = this.$attrs;
 23490  
 23491    this.ngModel = ngModel;
 23492    if ($attrs.type == 'checkbox' || $attrs.type == 'radio') {
 23493      this.mode  = $attrs.type;
 23494      this.iconEl = $element[0].children[0];
 23495      this.buttonEl = $element[0].children[1];
 23496      if (ngModel) {
 23497        // Clear ngAria set attributes
 23498        this.initClickListeners();
 23499      }
 23500    }
 23501  };
 23502  
 23503  // ngAria auto sets attributes on a menu-item with a ngModel.
 23504  // We don't want this because our content (buttons) get the focus
 23505  // and set their own aria attributes appropritately. Having both
 23506  // breaks NVDA / JAWS. This undeoes ngAria's attrs.
 23507  MenuItemController.prototype.clearNgAria = function() {
 23508    var el = this.$element[0];
 23509    var clearAttrs = ['role', 'tabindex', 'aria-invalid', 'aria-checked'];
 23510    angular.forEach(clearAttrs, function(attr) {
 23511      el.removeAttribute(attr);
 23512    });
 23513  };
 23514  
 23515  MenuItemController.prototype.initClickListeners = function() {
 23516    var self = this;
 23517    var ngModel = this.ngModel;
 23518    var $scope = this.$scope;
 23519    var $attrs = this.$attrs;
 23520    var $element = this.$element;
 23521    var mode = this.mode;
 23522  
 23523    this.handleClick = angular.bind(this, this.handleClick);
 23524  
 23525    var icon = this.iconEl;
 23526    var button = angular.element(this.buttonEl);
 23527    var handleClick = this.handleClick;
 23528  
 23529    $attrs.$observe('disabled', setDisabled);
 23530    setDisabled($attrs.disabled);
 23531  
 23532    ngModel.$render = function render() {
 23533      self.clearNgAria();
 23534      if (isSelected()) {
 23535        icon.style.display = '';
 23536        button.attr('aria-checked', 'true');
 23537      } else {
 23538        icon.style.display = 'none';
 23539        button.attr('aria-checked', 'false');
 23540      }
 23541    };
 23542  
 23543    $scope.$$postDigest(ngModel.$render);
 23544  
 23545    function isSelected() {
 23546      if (mode == 'radio') {
 23547        var val = $attrs.ngValue ? $scope.$eval($attrs.ngValue) : $attrs.value;
 23548        return ngModel.$modelValue == val;
 23549      } else {
 23550        return ngModel.$modelValue;
 23551      }
 23552    }
 23553  
 23554    function setDisabled(disabled) {
 23555      if (disabled) {
 23556        button.off('click', handleClick);
 23557      } else {
 23558        button.on('click', handleClick);
 23559      }
 23560    }
 23561  };
 23562  
 23563  MenuItemController.prototype.handleClick = function(e) {
 23564    var mode = this.mode;
 23565    var ngModel = this.ngModel;
 23566    var $attrs = this.$attrs;
 23567    var newVal;
 23568    if (mode == 'checkbox') {
 23569      newVal = !ngModel.$modelValue;
 23570    } else if (mode == 'radio') {
 23571      newVal = $attrs.ngValue ? this.$scope.$eval($attrs.ngValue) : $attrs.value;
 23572    }
 23573    ngModel.$setViewValue(newVal);
 23574    ngModel.$render();
 23575  };
 23576  
 23577  })();
 23578  (function(){
 23579  "use strict";
 23580  
 23581  
 23582  angular
 23583    .module('material.components.menuBar')
 23584    .directive('mdMenuItem', MenuItemDirective);
 23585  
 23586   /* @ngInject */
 23587  function MenuItemDirective() {
 23588    return {
 23589      require: ['mdMenuItem', '?ngModel'],
 23590      priority: 210, // ensure that our post link runs after ngAria
 23591      compile: function(templateEl, templateAttrs) {
 23592        if (templateAttrs.type == 'checkbox' || templateAttrs.type == 'radio') {
 23593          var text = templateEl[0].textContent;
 23594          var buttonEl = angular.element('<md-button type="button"></md-button>');
 23595              buttonEl.html(text);
 23596              buttonEl.attr('tabindex', '0');
 23597  
 23598          templateEl.html('');
 23599          templateEl.append(angular.element('<md-icon md-svg-icon="check"></md-icon>'));
 23600          templateEl.append(buttonEl);
 23601          templateEl[0].classList.add('md-indent');
 23602  
 23603          setDefault('role', (templateAttrs.type == 'checkbox') ? 'menuitemcheckbox' : 'menuitemradio', buttonEl);
 23604          angular.forEach(['ng-disabled'], moveAttrToButton);
 23605  
 23606        } else {
 23607          setDefault('role', 'menuitem', templateEl[0].querySelector('md-button,button,a'));
 23608        }
 23609  
 23610  
 23611        return function(scope, el, attrs, ctrls) {
 23612          var ctrl = ctrls[0];
 23613          var ngModel = ctrls[1];
 23614          ctrl.init(ngModel);
 23615        };
 23616  
 23617        function setDefault(attr, val, el) {
 23618          el = el || templateEl;
 23619          if (el instanceof angular.element) {
 23620            el = el[0];
 23621          }
 23622          if (!el.hasAttribute(attr)) {
 23623            el.setAttribute(attr, val);
 23624          }
 23625        }
 23626  
 23627        function moveAttrToButton(attr) {
 23628          if (templateEl[0].hasAttribute(attr)) {
 23629            var val = templateEl[0].getAttribute(attr);
 23630            buttonEl[0].setAttribute(attr, val);
 23631            templateEl[0].removeAttribute(attr);
 23632          }
 23633        }
 23634      },
 23635      controller: 'MenuItemController'
 23636    };
 23637  }
 23638  
 23639  })();
 23640  (function(){
 23641  "use strict";
 23642  
 23643  /**
 23644   * @ngdoc directive
 23645   * @name mdTab
 23646   * @module material.components.tabs
 23647   *
 23648   * @restrict E
 23649   *
 23650   * @description
 23651   * Use the `<md-tab>` a nested directive used within `<md-tabs>` to specify a tab with a **label** and optional *view content*.
 23652   *
 23653   * If the `label` attribute is not specified, then an optional `<md-tab-label>` tag can be used to specify more
 23654   * complex tab header markup. If neither the **label** nor the **md-tab-label** are specified, then the nested
 23655   * markup of the `<md-tab>` is used as the tab header markup.
 23656   *
 23657   * Please note that if you use `<md-tab-label>`, your content **MUST** be wrapped in the `<md-tab-body>` tag.  This
 23658   * is to define a clear separation between the tab content and the tab label.
 23659   *
 23660   * This container is used by the TabsController to show/hide the active tab's content view. This synchronization is
 23661   * automatically managed by the internal TabsController whenever the tab selection changes. Selection changes can
 23662   * be initiated via data binding changes, programmatic invocation, or user gestures.
 23663   *
 23664   * @param {string=} label Optional attribute to specify a simple string as the tab label
 23665   * @param {boolean=} ng-disabled If present and expression evaluates to truthy, disabled tab selection.
 23666   * @param {expression=} md-on-deselect Expression to be evaluated after the tab has been de-selected.
 23667   * @param {expression=} md-on-select Expression to be evaluated after the tab has been selected.
 23668   * @param {boolean=} md-active When true, sets the active tab.  Note: There can only be one active tab at a time.
 23669   *
 23670   *
 23671   * @usage
 23672   *
 23673   * <hljs lang="html">
 23674   * <md-tab label="" ng-disabled md-on-select="" md-on-deselect="" >
 23675   *   <h3>My Tab content</h3>
 23676   * </md-tab>
 23677   *
 23678   * <md-tab >
 23679   *   <md-tab-label>
 23680   *     <h3>My Tab content</h3>
 23681   *   </md-tab-label>
 23682   *   <md-tab-body>
 23683   *     <p>
 23684   *       Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium,
 23685   *       totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae
 23686   *       dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit,
 23687   *       sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.
 23688   *     </p>
 23689   *   </md-tab-body>
 23690   * </md-tab>
 23691   * </hljs>
 23692   *
 23693   */
 23694  angular
 23695      .module('material.components.tabs')
 23696      .directive('mdTab', MdTab);
 23697  
 23698  function MdTab () {
 23699    return {
 23700      require:  '^?mdTabs',
 23701      terminal: true,
 23702      compile:  function (element, attr) {
 23703        var label = firstChild(element, 'md-tab-label'),
 23704            body  = firstChild(element, 'md-tab-body');
 23705  
 23706        if (label.length == 0) {
 23707          label = angular.element('<md-tab-label></md-tab-label>');
 23708          if (attr.label) label.text(attr.label);
 23709          else label.append(element.contents());
 23710  
 23711          if (body.length == 0) {
 23712            var contents = element.contents().detach();
 23713            body         = angular.element('<md-tab-body></md-tab-body>');
 23714            body.append(contents);
 23715          }
 23716        }
 23717  
 23718        element.append(label);
 23719        if (body.html()) element.append(body);
 23720  
 23721        return postLink;
 23722      },
 23723      scope:    {
 23724        active:   '=?mdActive',
 23725        disabled: '=?ngDisabled',
 23726        select:   '&?mdOnSelect',
 23727        deselect: '&?mdOnDeselect'
 23728      }
 23729    };
 23730  
 23731    function postLink (scope, element, attr, ctrl) {
 23732      if (!ctrl) return;
 23733      var index = ctrl.getTabElementIndex(element),
 23734          body  = firstChild(element, 'md-tab-body').remove(),
 23735          label = firstChild(element, 'md-tab-label').remove(),
 23736          data  = ctrl.insertTab({
 23737            scope:    scope,
 23738            parent:   scope.$parent,
 23739            index:    index,
 23740            element:  element,
 23741            template: body.html(),
 23742            label:    label.html()
 23743          }, index);
 23744  
 23745      scope.select   = scope.select || angular.noop;
 23746      scope.deselect = scope.deselect || angular.noop;
 23747  
 23748      scope.$watch('active', function (active) { if (active) ctrl.select(data.getIndex(), true); });
 23749      scope.$watch('disabled', function () { ctrl.refreshIndex(); });
 23750      scope.$watch(
 23751          function () {
 23752            return ctrl.getTabElementIndex(element);
 23753          },
 23754          function (newIndex) {
 23755            data.index = newIndex;
 23756            ctrl.updateTabOrder();
 23757          }
 23758      );
 23759      scope.$on('$destroy', function () { ctrl.removeTab(data); });
 23760    }
 23761  
 23762    function firstChild (element, tagName) {
 23763      var children = element[0].children;
 23764      for (var i = 0, len = children.length; i < len; i++) {
 23765        var child = children[i];
 23766        if (child.tagName === tagName.toUpperCase()) return angular.element(child);
 23767      }
 23768      return angular.element();
 23769    }
 23770  }
 23771  
 23772  })();
 23773  (function(){
 23774  "use strict";
 23775  
 23776  angular
 23777      .module('material.components.tabs')
 23778      .directive('mdTabItem', MdTabItem);
 23779  
 23780  function MdTabItem () {
 23781    return {
 23782      require: '^?mdTabs',
 23783      link:    function link (scope, element, attr, ctrl) {
 23784        if (!ctrl) return;
 23785        ctrl.attachRipple(scope, element);
 23786      }
 23787    };
 23788  }
 23789  
 23790  })();
 23791  (function(){
 23792  "use strict";
 23793  
 23794  angular
 23795      .module('material.components.tabs')
 23796      .directive('mdTabLabel', MdTabLabel);
 23797  
 23798  function MdTabLabel () {
 23799    return { terminal: true };
 23800  }
 23801  
 23802  
 23803  })();
 23804  (function(){
 23805  "use strict";
 23806  
 23807  angular.module('material.components.tabs')
 23808      .directive('mdTabScroll', MdTabScroll);
 23809  
 23810  function MdTabScroll ($parse) {
 23811    return {
 23812      restrict: 'A',
 23813      compile: function ($element, attr) {
 23814        var fn = $parse(attr.mdTabScroll, null, true);
 23815        return function ngEventHandler (scope, element) {
 23816          element.on('mousewheel', function (event) {
 23817            scope.$apply(function () { fn(scope, { $event: event }); });
 23818          });
 23819        };
 23820      }
 23821    }
 23822  }
 23823  MdTabScroll.$inject = ["$parse"];
 23824  
 23825  })();
 23826  (function(){
 23827  "use strict";
 23828  
 23829  angular
 23830      .module('material.components.tabs')
 23831      .controller('MdTabsController', MdTabsController);
 23832  
 23833  /**
 23834   * @ngInject
 23835   */
 23836  function MdTabsController ($scope, $element, $window, $mdConstant, $mdTabInkRipple,
 23837                             $mdUtil, $animateCss, $attrs, $compile, $mdTheming) {
 23838    // define private properties
 23839    var ctrl      = this,
 23840        locked    = false,
 23841        elements  = getElements(),
 23842        queue     = [],
 23843        destroyed = false,
 23844        loaded    = false;
 23845  
 23846    // define one-way bindings
 23847    defineOneWayBinding('stretchTabs', handleStretchTabs);
 23848  
 23849    // define public properties with change handlers
 23850    defineProperty('focusIndex', handleFocusIndexChange, ctrl.selectedIndex || 0);
 23851    defineProperty('offsetLeft', handleOffsetChange, 0);
 23852    defineProperty('hasContent', handleHasContent, false);
 23853    defineProperty('maxTabWidth', handleMaxTabWidth, getMaxTabWidth());
 23854    defineProperty('shouldPaginate', handleShouldPaginate, false);
 23855  
 23856    // define boolean attributes
 23857    defineBooleanAttribute('noInkBar', handleInkBar);
 23858    defineBooleanAttribute('dynamicHeight', handleDynamicHeight);
 23859    defineBooleanAttribute('noPagination');
 23860    defineBooleanAttribute('swipeContent');
 23861    defineBooleanAttribute('noDisconnect');
 23862    defineBooleanAttribute('autoselect');
 23863    defineBooleanAttribute('noSelectClick');
 23864    defineBooleanAttribute('centerTabs', handleCenterTabs, false);
 23865    defineBooleanAttribute('enableDisconnect');
 23866  
 23867    // define public properties
 23868    ctrl.scope             = $scope;
 23869    ctrl.parent            = $scope.$parent;
 23870    ctrl.tabs              = [];
 23871    ctrl.lastSelectedIndex = null;
 23872    ctrl.hasFocus          = false;
 23873    ctrl.lastClick         = true;
 23874    ctrl.shouldCenterTabs  = shouldCenterTabs();
 23875  
 23876    // define public methods
 23877    ctrl.updatePagination   = $mdUtil.debounce(updatePagination, 100);
 23878    ctrl.redirectFocus      = redirectFocus;
 23879    ctrl.attachRipple       = attachRipple;
 23880    ctrl.insertTab          = insertTab;
 23881    ctrl.removeTab          = removeTab;
 23882    ctrl.select             = select;
 23883    ctrl.scroll             = scroll;
 23884    ctrl.nextPage           = nextPage;
 23885    ctrl.previousPage       = previousPage;
 23886    ctrl.keydown            = keydown;
 23887    ctrl.canPageForward     = canPageForward;
 23888    ctrl.canPageBack        = canPageBack;
 23889    ctrl.refreshIndex       = refreshIndex;
 23890    ctrl.incrementIndex     = incrementIndex;
 23891    ctrl.getTabElementIndex = getTabElementIndex;
 23892    ctrl.updateInkBarStyles = $mdUtil.debounce(updateInkBarStyles, 100);
 23893    ctrl.updateTabOrder     = $mdUtil.debounce(updateTabOrder, 100);
 23894  
 23895    init();
 23896  
 23897    /**
 23898     * Perform initialization for the controller, setup events and watcher(s)
 23899     */
 23900    function init () {
 23901      ctrl.selectedIndex = ctrl.selectedIndex || 0;
 23902      compileTemplate();
 23903      configureWatchers();
 23904      bindEvents();
 23905      $mdTheming($element);
 23906      $mdUtil.nextTick(function () {
 23907        updateHeightFromContent();
 23908        adjustOffset();
 23909        updateInkBarStyles();
 23910        ctrl.tabs[ ctrl.selectedIndex ] && ctrl.tabs[ ctrl.selectedIndex ].scope.select();
 23911        loaded = true;
 23912        updatePagination();
 23913      });
 23914    }
 23915  
 23916    /**
 23917     * Compiles the template provided by the user.  This is passed as an attribute from the tabs
 23918     * directive's template function.
 23919     */
 23920    function compileTemplate () {
 23921      var template = $attrs.$mdTabsTemplate,
 23922          element  = angular.element(elements.data);
 23923      element.html(template);
 23924      $compile(element.contents())(ctrl.parent);
 23925      delete $attrs.$mdTabsTemplate;
 23926    }
 23927  
 23928    /**
 23929     * Binds events used by the tabs component.
 23930     */
 23931    function bindEvents () {
 23932      angular.element($window).on('resize', handleWindowResize);
 23933      $scope.$on('$destroy', cleanup);
 23934    }
 23935  
 23936    /**
 23937     * Configure watcher(s) used by Tabs
 23938     */
 23939    function configureWatchers () {
 23940      $scope.$watch('$mdTabsCtrl.selectedIndex', handleSelectedIndexChange);
 23941    }
 23942  
 23943    /**
 23944     * Creates a one-way binding manually rather than relying on Angular's isolated scope
 23945     * @param key
 23946     * @param handler
 23947     */
 23948    function defineOneWayBinding (key, handler) {
 23949      var attr = $attrs.$normalize('md-' + key);
 23950      if (handler) defineProperty(key, handler);
 23951      $attrs.$observe(attr, function (newValue) { ctrl[ key ] = newValue; });
 23952    }
 23953  
 23954    /**
 23955     * Defines boolean attributes with default value set to true.  (ie. md-stretch-tabs with no value
 23956     * will be treated as being truthy)
 23957     * @param key
 23958     * @param handler
 23959     */
 23960    function defineBooleanAttribute (key, handler) {
 23961      var attr = $attrs.$normalize('md-' + key);
 23962      if (handler) defineProperty(key, handler);
 23963      if ($attrs.hasOwnProperty(attr)) updateValue($attrs[attr]);
 23964      $attrs.$observe(attr, updateValue);
 23965      function updateValue (newValue) {
 23966        ctrl[ key ] = newValue !== 'false';
 23967      }
 23968    }
 23969  
 23970    /**
 23971     * Remove any events defined by this controller
 23972     */
 23973    function cleanup () {
 23974      destroyed = true;
 23975      angular.element($window).off('resize', handleWindowResize);
 23976    }
 23977  
 23978    // Change handlers
 23979  
 23980    /**
 23981     * Toggles stretch tabs class and updates inkbar when tab stretching changes
 23982     * @param stretchTabs
 23983     */
 23984    function handleStretchTabs (stretchTabs) {
 23985      angular.element(elements.wrapper).toggleClass('md-stretch-tabs', shouldStretchTabs());
 23986      updateInkBarStyles();
 23987    }
 23988  
 23989    function handleCenterTabs (newValue) {
 23990      ctrl.shouldCenterTabs = shouldCenterTabs();
 23991    }
 23992  
 23993    function handleMaxTabWidth (newWidth, oldWidth) {
 23994      if (newWidth !== oldWidth) {
 23995        angular.forEach(elements.tabs, function(tab) {
 23996          tab.style.maxWidth = newWidth + 'px';
 23997        });
 23998        $mdUtil.nextTick(ctrl.updateInkBarStyles);
 23999      }
 24000    }
 24001  
 24002    function handleShouldPaginate (newValue, oldValue) {
 24003      if (newValue !== oldValue) {
 24004        ctrl.maxTabWidth      = getMaxTabWidth();
 24005        ctrl.shouldCenterTabs = shouldCenterTabs();
 24006        $mdUtil.nextTick(function () {
 24007          ctrl.maxTabWidth = getMaxTabWidth();
 24008          adjustOffset(ctrl.selectedIndex);
 24009        });
 24010      }
 24011    }
 24012  
 24013    /**
 24014     * Add/remove the `md-no-tab-content` class depending on `ctrl.hasContent`
 24015     * @param hasContent
 24016     */
 24017    function handleHasContent (hasContent) {
 24018      $element[ hasContent ? 'removeClass' : 'addClass' ]('md-no-tab-content');
 24019    }
 24020  
 24021    /**
 24022     * Apply ctrl.offsetLeft to the paging element when it changes
 24023     * @param left
 24024     */
 24025    function handleOffsetChange (left) {
 24026      var newValue = ctrl.shouldCenterTabs ? '' : '-' + left + 'px';
 24027      angular.element(elements.paging).css($mdConstant.CSS.TRANSFORM, 'translate3d(' + newValue + ', 0, 0)');
 24028      $scope.$broadcast('$mdTabsPaginationChanged');
 24029    }
 24030  
 24031    /**
 24032     * Update the UI whenever `ctrl.focusIndex` is updated
 24033     * @param newIndex
 24034     * @param oldIndex
 24035     */
 24036    function handleFocusIndexChange (newIndex, oldIndex) {
 24037      if (newIndex === oldIndex) return;
 24038      if (!elements.tabs[ newIndex ]) return;
 24039      adjustOffset();
 24040      redirectFocus();
 24041    }
 24042  
 24043    /**
 24044     * Update the UI whenever the selected index changes. Calls user-defined select/deselect methods.
 24045     * @param newValue
 24046     * @param oldValue
 24047     */
 24048    function handleSelectedIndexChange (newValue, oldValue) {
 24049      if (newValue === oldValue) return;
 24050      
 24051      ctrl.selectedIndex     = getNearestSafeIndex(newValue);
 24052      ctrl.lastSelectedIndex = oldValue;
 24053      ctrl.updateInkBarStyles();
 24054      updateHeightFromContent();
 24055      adjustOffset(newValue);
 24056      $scope.$broadcast('$mdTabsChanged');
 24057      ctrl.tabs[ oldValue ] && ctrl.tabs[ oldValue ].scope.deselect();
 24058      ctrl.tabs[ newValue ] && ctrl.tabs[ newValue ].scope.select();
 24059    }
 24060  
 24061    function getTabElementIndex(tabEl){
 24062      var tabs = $element[0].getElementsByTagName('md-tab');
 24063      return Array.prototype.indexOf.call(tabs, tabEl[0]);
 24064    }
 24065  
 24066    /**
 24067     * Queues up a call to `handleWindowResize` when a resize occurs while the tabs component is
 24068     * hidden.
 24069     */
 24070    function handleResizeWhenVisible () {
 24071      // if there is already a watcher waiting for resize, do nothing
 24072      if (handleResizeWhenVisible.watcher) return;
 24073      // otherwise, we will abuse the $watch function to check for visible
 24074      handleResizeWhenVisible.watcher = $scope.$watch(function () {
 24075        // since we are checking for DOM size, we use $mdUtil.nextTick() to wait for after the DOM updates
 24076        $mdUtil.nextTick(function () {
 24077          // if the watcher has already run (ie. multiple digests in one cycle), do nothing
 24078          if (!handleResizeWhenVisible.watcher) return;
 24079  
 24080          if ($element.prop('offsetParent')) {
 24081            handleResizeWhenVisible.watcher();
 24082            handleResizeWhenVisible.watcher = null;
 24083  
 24084            handleWindowResize();
 24085          }
 24086        }, false);
 24087      });
 24088    }
 24089  
 24090    // Event handlers / actions
 24091  
 24092    /**
 24093     * Handle user keyboard interactions
 24094     * @param event
 24095     */
 24096    function keydown (event) {
 24097      switch (event.keyCode) {
 24098        case $mdConstant.KEY_CODE.LEFT_ARROW:
 24099          event.preventDefault();
 24100          incrementIndex(-1, true);
 24101          break;
 24102        case $mdConstant.KEY_CODE.RIGHT_ARROW:
 24103          event.preventDefault();
 24104          incrementIndex(1, true);
 24105          break;
 24106        case $mdConstant.KEY_CODE.SPACE:
 24107        case $mdConstant.KEY_CODE.ENTER:
 24108          event.preventDefault();
 24109          if (!locked) ctrl.selectedIndex = ctrl.focusIndex;
 24110          break;
 24111      }
 24112      ctrl.lastClick = false;
 24113    }
 24114  
 24115    /**
 24116     * Update the selected index. Triggers a click event on the original `md-tab` element in order
 24117     * to fire user-added click events if canSkipClick or `md-no-select-click` are false.
 24118     * @param index
 24119     * @param canSkipClick Optionally allow not firing the click event if `md-no-select-click` is also true.
 24120     */
 24121    function select (index, canSkipClick) {
 24122      if (!locked) ctrl.focusIndex = ctrl.selectedIndex = index;
 24123      ctrl.lastClick = true;
 24124      // skip the click event if noSelectClick is enabled
 24125      if (canSkipClick && ctrl.noSelectClick) return;
 24126      // nextTick is required to prevent errors in user-defined click events
 24127      $mdUtil.nextTick(function () {
 24128        ctrl.tabs[ index ].element.triggerHandler('click');
 24129      }, false);
 24130    }
 24131  
 24132    /**
 24133     * When pagination is on, this makes sure the selected index is in view.
 24134     * @param event
 24135     */
 24136    function scroll (event) {
 24137      if (!ctrl.shouldPaginate) return;
 24138      event.preventDefault();
 24139      ctrl.offsetLeft = fixOffset(ctrl.offsetLeft - event.wheelDelta);
 24140    }
 24141  
 24142    /**
 24143     * Slides the tabs over approximately one page forward.
 24144     */
 24145    function nextPage () {
 24146      var viewportWidth = elements.canvas.clientWidth,
 24147          totalWidth    = viewportWidth + ctrl.offsetLeft,
 24148          i, tab;
 24149      for (i = 0; i < elements.tabs.length; i++) {
 24150        tab = elements.tabs[ i ];
 24151        if (tab.offsetLeft + tab.offsetWidth > totalWidth) break;
 24152      }
 24153      ctrl.offsetLeft = fixOffset(tab.offsetLeft);
 24154    }
 24155  
 24156    /**
 24157     * Slides the tabs over approximately one page backward.
 24158     */
 24159    function previousPage () {
 24160      var i, tab;
 24161      for (i = 0; i < elements.tabs.length; i++) {
 24162        tab = elements.tabs[ i ];
 24163        if (tab.offsetLeft + tab.offsetWidth >= ctrl.offsetLeft) break;
 24164      }
 24165      ctrl.offsetLeft = fixOffset(tab.offsetLeft + tab.offsetWidth - elements.canvas.clientWidth);
 24166    }
 24167  
 24168    /**
 24169     * Update size calculations when the window is resized.
 24170     */
 24171    function handleWindowResize () {
 24172      ctrl.lastSelectedIndex = ctrl.selectedIndex;
 24173      ctrl.offsetLeft        = fixOffset(ctrl.offsetLeft);
 24174      $mdUtil.nextTick(function () {
 24175        ctrl.updateInkBarStyles();
 24176        updatePagination();
 24177      });
 24178    }
 24179  
 24180    function handleInkBar (hide) {
 24181      angular.element(elements.inkBar).toggleClass('ng-hide', hide);
 24182    }
 24183  
 24184    /**
 24185     * Toggle dynamic height class when value changes
 24186     * @param value
 24187     */
 24188    function handleDynamicHeight (value) {
 24189      $element.toggleClass('md-dynamic-height', value);
 24190    }
 24191  
 24192    /**
 24193     * Remove a tab from the data and select the nearest valid tab.
 24194     * @param tabData
 24195     */
 24196    function removeTab (tabData) {
 24197      if (destroyed) return;
 24198      var selectedIndex = ctrl.selectedIndex,
 24199          tab           = ctrl.tabs.splice(tabData.getIndex(), 1)[ 0 ];
 24200      refreshIndex();
 24201      // when removing a tab, if the selected index did not change, we have to manually trigger the
 24202      //   tab select/deselect events
 24203      if (ctrl.selectedIndex === selectedIndex) {
 24204        tab.scope.deselect();
 24205        ctrl.tabs[ ctrl.selectedIndex ] && ctrl.tabs[ ctrl.selectedIndex ].scope.select();
 24206      }
 24207      $mdUtil.nextTick(function () {
 24208        updatePagination();
 24209        ctrl.offsetLeft = fixOffset(ctrl.offsetLeft);
 24210      });
 24211    }
 24212  
 24213    /**
 24214     * Create an entry in the tabs array for a new tab at the specified index.
 24215     * @param tabData
 24216     * @param index
 24217     * @returns {*}
 24218     */
 24219    function insertTab (tabData, index) {
 24220      var hasLoaded = loaded;
 24221      var proto     = {
 24222            getIndex:     function () { return ctrl.tabs.indexOf(tab); },
 24223            isActive:     function () { return this.getIndex() === ctrl.selectedIndex; },
 24224            isLeft:       function () { return this.getIndex() < ctrl.selectedIndex; },
 24225            isRight:      function () { return this.getIndex() > ctrl.selectedIndex; },
 24226            shouldRender: function () { return !ctrl.noDisconnect || this.isActive(); },
 24227            hasFocus:     function () {
 24228              return !ctrl.lastClick
 24229                  && ctrl.hasFocus && this.getIndex() === ctrl.focusIndex;
 24230            },
 24231            id:           $mdUtil.nextUid()
 24232          },
 24233          tab       = angular.extend(proto, tabData);
 24234      if (angular.isDefined(index)) {
 24235        ctrl.tabs.splice(index, 0, tab);
 24236      } else {
 24237        ctrl.tabs.push(tab);
 24238      }
 24239      processQueue();
 24240      updateHasContent();
 24241      $mdUtil.nextTick(function () {
 24242        updatePagination();
 24243        // if autoselect is enabled, select the newly added tab
 24244        if (hasLoaded && ctrl.autoselect) $mdUtil.nextTick(function () {
 24245          $mdUtil.nextTick(function () { select(ctrl.tabs.indexOf(tab)); });
 24246        });
 24247      });
 24248      return tab;
 24249    }
 24250  
 24251    // Getter methods
 24252  
 24253    /**
 24254     * Gathers references to all of the DOM elements used by this controller.
 24255     * @returns {{}}
 24256     */
 24257    function getElements () {
 24258      var elements = {};
 24259  
 24260      // gather tab bar elements
 24261      elements.wrapper = $element[ 0 ].getElementsByTagName('md-tabs-wrapper')[ 0 ];
 24262      elements.data    = $element[ 0 ].getElementsByTagName('md-tab-data')[ 0 ];
 24263      elements.canvas  = elements.wrapper.getElementsByTagName('md-tabs-canvas')[ 0 ];
 24264      elements.paging  = elements.canvas.getElementsByTagName('md-pagination-wrapper')[ 0 ];
 24265      elements.tabs    = elements.paging.getElementsByTagName('md-tab-item');
 24266      elements.dummies = elements.canvas.getElementsByTagName('md-dummy-tab');
 24267      elements.inkBar  = elements.paging.getElementsByTagName('md-ink-bar')[ 0 ];
 24268  
 24269      // gather tab content elements
 24270      elements.contentsWrapper = $element[ 0 ].getElementsByTagName('md-tabs-content-wrapper')[ 0 ];
 24271      elements.contents        = elements.contentsWrapper.getElementsByTagName('md-tab-content');
 24272  
 24273      return elements;
 24274    }
 24275  
 24276    /**
 24277     * Determines whether or not the left pagination arrow should be enabled.
 24278     * @returns {boolean}
 24279     */
 24280    function canPageBack () {
 24281      return ctrl.offsetLeft > 0;
 24282    }
 24283  
 24284    /**
 24285     * Determines whether or not the right pagination arrow should be enabled.
 24286     * @returns {*|boolean}
 24287     */
 24288    function canPageForward () {
 24289      var lastTab = elements.tabs[ elements.tabs.length - 1 ];
 24290      return lastTab && lastTab.offsetLeft + lastTab.offsetWidth > elements.canvas.clientWidth +
 24291          ctrl.offsetLeft;
 24292    }
 24293  
 24294    /**
 24295     * Determines if the UI should stretch the tabs to fill the available space.
 24296     * @returns {*}
 24297     */
 24298    function shouldStretchTabs () {
 24299      switch (ctrl.stretchTabs) {
 24300        case 'always':
 24301          return true;
 24302        case 'never':
 24303          return false;
 24304        default:
 24305          return !ctrl.shouldPaginate
 24306              && $window.matchMedia('(max-width: 600px)').matches;
 24307      }
 24308    }
 24309  
 24310    /**
 24311     * Determines if the tabs should appear centered.
 24312     * @returns {string|boolean}
 24313     */
 24314    function shouldCenterTabs () {
 24315      return ctrl.centerTabs && !ctrl.shouldPaginate;
 24316    }
 24317  
 24318    /**
 24319     * Determines if pagination is necessary to display the tabs within the available space.
 24320     * @returns {boolean}
 24321     */
 24322    function shouldPaginate () {
 24323      if (ctrl.noPagination || !loaded) return false;
 24324      var canvasWidth = $element.prop('clientWidth');
 24325      angular.forEach(getElements().dummies, function (tab) { canvasWidth -= tab.offsetWidth; });
 24326      return canvasWidth < 0;
 24327    }
 24328  
 24329    /**
 24330     * Finds the nearest tab index that is available.  This is primarily used for when the active
 24331     * tab is removed.
 24332     * @param newIndex
 24333     * @returns {*}
 24334     */
 24335    function getNearestSafeIndex (newIndex) {
 24336      if (newIndex === -1) return -1;
 24337      var maxOffset = Math.max(ctrl.tabs.length - newIndex, newIndex),
 24338          i, tab;
 24339      for (i = 0; i <= maxOffset; i++) {
 24340        tab = ctrl.tabs[ newIndex + i ];
 24341        if (tab && (tab.scope.disabled !== true)) return tab.getIndex();
 24342        tab = ctrl.tabs[ newIndex - i ];
 24343        if (tab && (tab.scope.disabled !== true)) return tab.getIndex();
 24344      }
 24345      return newIndex;
 24346    }
 24347  
 24348    // Utility methods
 24349  
 24350    /**
 24351     * Defines a property using a getter and setter in order to trigger a change handler without
 24352     * using `$watch` to observe changes.
 24353     * @param key
 24354     * @param handler
 24355     * @param value
 24356     */
 24357    function defineProperty (key, handler, value) {
 24358      Object.defineProperty(ctrl, key, {
 24359        get: function () { return value; },
 24360        set: function (newValue) {
 24361          var oldValue = value;
 24362          value        = newValue;
 24363          handler && handler(newValue, oldValue);
 24364        }
 24365      });
 24366    }
 24367  
 24368    /**
 24369     * Updates whether or not pagination should be displayed.
 24370     */
 24371    function updatePagination () {
 24372      if (!shouldStretchTabs()) updatePagingWidth();
 24373      ctrl.maxTabWidth = getMaxTabWidth();
 24374      ctrl.shouldPaginate = shouldPaginate();
 24375    }
 24376  
 24377    function updatePagingWidth() {
 24378      var width = 1;
 24379      angular.forEach(getElements().dummies, function (element) {
 24380        //-- Uses the larger value between `getBoundingClientRect().width` and `offsetWidth`.  This
 24381        //   prevents `offsetWidth` value from being rounded down and causing wrapping issues, but
 24382        //   also handles scenarios where `getBoundingClientRect()` is inaccurate (ie. tabs inside
 24383        //   of a dialog)
 24384        width += Math.max(element.offsetWidth, element.getBoundingClientRect().width);
 24385      });
 24386      angular.element(elements.paging).css('width', Math.ceil(width) + 'px');
 24387    }
 24388  
 24389    function getMaxTabWidth () {
 24390      return $element.prop('clientWidth');
 24391    }
 24392  
 24393    /**
 24394     * Re-orders the tabs and updates the selected and focus indexes to their new positions.
 24395     * This is triggered by `tabDirective.js` when the user's tabs have been re-ordered.
 24396     */
 24397    function updateTabOrder () {
 24398      var selectedItem   = ctrl.tabs[ ctrl.selectedIndex ],
 24399          focusItem      = ctrl.tabs[ ctrl.focusIndex ];
 24400      ctrl.tabs          = ctrl.tabs.sort(function (a, b) {
 24401        return a.index - b.index;
 24402      });
 24403      ctrl.selectedIndex = ctrl.tabs.indexOf(selectedItem);
 24404      ctrl.focusIndex    = ctrl.tabs.indexOf(focusItem);
 24405    }
 24406  
 24407    /**
 24408     * This moves the selected or focus index left or right.  This is used by the keydown handler.
 24409     * @param inc
 24410     */
 24411    function incrementIndex (inc, focus) {
 24412      var newIndex,
 24413          key   = focus ? 'focusIndex' : 'selectedIndex',
 24414          index = ctrl[ key ];
 24415      for (newIndex = index + inc;
 24416           ctrl.tabs[ newIndex ] && ctrl.tabs[ newIndex ].scope.disabled;
 24417           newIndex += inc) {}
 24418      if (ctrl.tabs[ newIndex ]) {
 24419        ctrl[ key ] = newIndex;
 24420      }
 24421    }
 24422  
 24423    /**
 24424     * This is used to forward focus to dummy elements.  This method is necessary to avoid animation
 24425     * issues when attempting to focus an item that is out of view.
 24426     */
 24427    function redirectFocus () {
 24428      getElements().dummies[ ctrl.focusIndex ].focus();
 24429    }
 24430  
 24431    /**
 24432     * Forces the pagination to move the focused tab into view.
 24433     */
 24434    function adjustOffset (index) {
 24435      if (index == null) index = ctrl.focusIndex;
 24436      if (!elements.tabs[ index ]) return;
 24437      if (ctrl.shouldCenterTabs) return;
 24438      var tab         = elements.tabs[ index ],
 24439          left        = tab.offsetLeft,
 24440          right       = tab.offsetWidth + left;
 24441      ctrl.offsetLeft = Math.max(ctrl.offsetLeft, fixOffset(right - elements.canvas.clientWidth + 32 * 2));
 24442      ctrl.offsetLeft = Math.min(ctrl.offsetLeft, fixOffset(left));
 24443    }
 24444  
 24445    /**
 24446     * Iterates through all queued functions and clears the queue.  This is used for functions that
 24447     * are called before the UI is ready, such as size calculations.
 24448     */
 24449    function processQueue () {
 24450      queue.forEach(function (func) { $mdUtil.nextTick(func); });
 24451      queue = [];
 24452    }
 24453  
 24454    /**
 24455     * Determines if the tab content area is needed.
 24456     */
 24457    function updateHasContent () {
 24458      var hasContent  = false;
 24459      angular.forEach(ctrl.tabs, function (tab) {
 24460        if (tab.template) hasContent = true;
 24461      });
 24462      ctrl.hasContent = hasContent;
 24463    }
 24464  
 24465    /**
 24466     * Moves the indexes to their nearest valid values.
 24467     */
 24468    function refreshIndex () {
 24469      ctrl.selectedIndex = getNearestSafeIndex(ctrl.selectedIndex);
 24470      ctrl.focusIndex    = getNearestSafeIndex(ctrl.focusIndex);
 24471    }
 24472  
 24473    /**
 24474     * Calculates the content height of the current tab.
 24475     * @returns {*}
 24476     */
 24477    function updateHeightFromContent () {
 24478      if (!ctrl.dynamicHeight) return $element.css('height', '');
 24479      if (!ctrl.tabs.length) return queue.push(updateHeightFromContent);
 24480  
 24481      var tabContent    = elements.contents[ ctrl.selectedIndex ],
 24482          contentHeight = tabContent ? tabContent.offsetHeight : 0,
 24483          tabsHeight    = elements.wrapper.offsetHeight,
 24484          newHeight     = contentHeight + tabsHeight,
 24485          currentHeight = $element.prop('clientHeight');
 24486  
 24487      if (currentHeight === newHeight) return;
 24488  
 24489      // Adjusts calculations for when the buttons are bottom-aligned since this relies on absolute
 24490      // positioning.  This should probably be cleaned up if a cleaner solution is possible.
 24491      if ($element.attr('md-align-tabs') === 'bottom') {
 24492        currentHeight -= tabsHeight;
 24493        newHeight -= tabsHeight;
 24494        // Need to include bottom border in these calculations
 24495        if ($element.attr('md-border-bottom') !== undefined) ++currentHeight;
 24496      }
 24497  
 24498      // Lock during animation so the user can't change tabs
 24499      locked = true;
 24500  
 24501      var fromHeight = { height: currentHeight + 'px' },
 24502          toHeight = { height: newHeight + 'px' };
 24503  
 24504      // Set the height to the current, specific pixel height to fix a bug on iOS where the height
 24505      // first animates to 0, then back to the proper height causing a visual glitch
 24506      $element.css(fromHeight);
 24507  
 24508      // Animate the height from the old to the new
 24509      $animateCss($element, {
 24510        from: fromHeight,
 24511        to: toHeight,
 24512        easing: 'cubic-bezier(0.35, 0, 0.25, 1)',
 24513        duration: 0.5
 24514      }).start().done(function () {
 24515        // Then (to fix the same iOS issue as above), disable transitions and remove the specific
 24516        // pixel height so the height can size with browser width/content changes, etc.
 24517        $element.css({
 24518          transition: 'none',
 24519          height: ''
 24520        });
 24521  
 24522        // In the next tick, re-allow transitions (if we do it all at once, $element.css is "smart"
 24523        // enough to batch it for us instead of doing it immediately, which undoes the original
 24524        // transition: none)
 24525        $mdUtil.nextTick(function() {
 24526          $element.css('transition', '');
 24527        });
 24528  
 24529        // And unlock so tab changes can occur
 24530        locked = false;
 24531      });
 24532    }
 24533  
 24534    /**
 24535     * Repositions the ink bar to the selected tab.
 24536     * @returns {*}
 24537     */
 24538    function updateInkBarStyles () {
 24539      if (!elements.tabs[ ctrl.selectedIndex ]) {
 24540        angular.element(elements.inkBar).css({ left: 'auto', right: 'auto' });
 24541        return;
 24542      }
 24543      if (!ctrl.tabs.length) return queue.push(ctrl.updateInkBarStyles);
 24544      // if the element is not visible, we will not be able to calculate sizes until it is
 24545      // we should treat that as a resize event rather than just updating the ink bar
 24546      if (!$element.prop('offsetParent')) return handleResizeWhenVisible();
 24547      var index      = ctrl.selectedIndex,
 24548          totalWidth = elements.paging.offsetWidth,
 24549          tab        = elements.tabs[ index ],
 24550          left       = tab.offsetLeft,
 24551          right      = totalWidth - left - tab.offsetWidth,
 24552          tabWidth;
 24553      if (ctrl.shouldCenterTabs) {
 24554        tabWidth = Array.prototype.slice.call(elements.tabs).reduce(function (value, element) {
 24555          return value + element.offsetWidth;
 24556        }, 0);
 24557        if (totalWidth > tabWidth) $mdUtil.nextTick(updateInkBarStyles, false);
 24558      }
 24559      updateInkBarClassName();
 24560      angular.element(elements.inkBar).css({ left: left + 'px', right: right + 'px' });
 24561    }
 24562  
 24563    /**
 24564     * Adds left/right classes so that the ink bar will animate properly.
 24565     */
 24566    function updateInkBarClassName () {
 24567      var newIndex = ctrl.selectedIndex,
 24568          oldIndex = ctrl.lastSelectedIndex,
 24569          ink      = angular.element(elements.inkBar);
 24570      if (!angular.isNumber(oldIndex)) return;
 24571      ink
 24572          .toggleClass('md-left', newIndex < oldIndex)
 24573          .toggleClass('md-right', newIndex > oldIndex);
 24574    }
 24575  
 24576    /**
 24577     * Takes an offset value and makes sure that it is within the min/max allowed values.
 24578     * @param value
 24579     * @returns {*}
 24580     */
 24581    function fixOffset (value) {
 24582      if (!elements.tabs.length || !ctrl.shouldPaginate) return 0;
 24583      var lastTab    = elements.tabs[ elements.tabs.length - 1 ],
 24584          totalWidth = lastTab.offsetLeft + lastTab.offsetWidth;
 24585      value          = Math.max(0, value);
 24586      value          = Math.min(totalWidth - elements.canvas.clientWidth, value);
 24587      return value;
 24588    }
 24589  
 24590    /**
 24591     * Attaches a ripple to the tab item element.
 24592     * @param scope
 24593     * @param element
 24594     */
 24595    function attachRipple (scope, element) {
 24596      var options = { colorElement: angular.element(elements.inkBar) };
 24597      $mdTabInkRipple.attach(scope, element, options);
 24598    }
 24599  }
 24600  MdTabsController.$inject = ["$scope", "$element", "$window", "$mdConstant", "$mdTabInkRipple", "$mdUtil", "$animateCss", "$attrs", "$compile", "$mdTheming"];
 24601  
 24602  })();
 24603  (function(){
 24604  "use strict";
 24605  
 24606  /**
 24607   * @ngdoc directive
 24608   * @name mdTabs
 24609   * @module material.components.tabs
 24610   *
 24611   * @restrict E
 24612   *
 24613   * @description
 24614   * The `<md-tabs>` directive serves as the container for 1..n `<md-tab>` child directives to produces a Tabs components.
 24615   * In turn, the nested `<md-tab>` directive is used to specify a tab label for the **header button** and a [optional] tab view
 24616   * content that will be associated with each tab button.
 24617   *
 24618   * Below is the markup for its simplest usage:
 24619   *
 24620   *  <hljs lang="html">
 24621   *  <md-tabs>
 24622   *    <md-tab label="Tab #1"></md-tab>
 24623   *    <md-tab label="Tab #2"></md-tab>
 24624   *    <md-tab label="Tab #3"></md-tab>
 24625   *  </md-tabs>
 24626   *  </hljs>
 24627   *
 24628   * Tabs supports three (3) usage scenarios:
 24629   *
 24630   *  1. Tabs (buttons only)
 24631   *  2. Tabs with internal view content
 24632   *  3. Tabs with external view content
 24633   *
 24634   * **Tab-only** support is useful when tab buttons are used for custom navigation regardless of any other components, content, or views.
 24635   * **Tabs with internal views** are the traditional usages where each tab has associated view content and the view switching is managed internally by the Tabs component.
 24636   * **Tabs with external view content** is often useful when content associated with each tab is independently managed and data-binding notifications announce tab selection changes.
 24637   *
 24638   * Additional features also include:
 24639   *
 24640   * *  Content can include any markup.
 24641   * *  If a tab is disabled while active/selected, then the next tab will be auto-selected.
 24642   *
 24643   * ### Explanation of tab stretching
 24644   *
 24645   * Initially, tabs will have an inherent size.  This size will either be defined by how much space is needed to accommodate their text or set by the user through CSS.  Calculations will be based on this size.
 24646   *
 24647   * On mobile devices, tabs will be expanded to fill the available horizontal space.  When this happens, all tabs will become the same size.
 24648   *
 24649   * On desktops, by default, stretching will never occur.
 24650   *
 24651   * This default behavior can be overridden through the `md-stretch-tabs` attribute.  Here is a table showing when stretching will occur:
 24652   *
 24653   * `md-stretch-tabs` | mobile    | desktop
 24654   * ------------------|-----------|--------
 24655   * `auto`            | stretched | ---
 24656   * `always`          | stretched | stretched
 24657   * `never`           | ---       | ---
 24658   *
 24659   * @param {integer=} md-selected Index of the active/selected tab
 24660   * @param {boolean=} md-no-ink If present, disables ink ripple effects.
 24661   * @param {boolean=} md-no-ink-bar If present, disables the selection ink bar.
 24662   * @param {string=}  md-align-tabs Attribute to indicate position of tab buttons: `bottom` or `top`; default is `top`
 24663   * @param {string=} md-stretch-tabs Attribute to indicate whether or not to stretch tabs: `auto`, `always`, or `never`; default is `auto`
 24664   * @param {boolean=} md-dynamic-height When enabled, the tab wrapper will resize based on the contents of the selected tab
 24665   * @param {boolean=} md-border-bottom If present, shows a solid `1px` border between the tabs and their content
 24666   * @param {boolean=} md-center-tabs When enabled, tabs will be centered provided there is no need for pagination
 24667   * @param {boolean=} md-no-pagination When enabled, pagination will remain off
 24668   * @param {boolean=} md-swipe-content When enabled, swipe gestures will be enabled for the content area to jump between tabs
 24669   * @param {boolean=} md-enable-disconnect When enabled, scopes will be disconnected for tabs that are not being displayed.  This provides a performance boost, but may also cause unexpected issues and is not recommended for most users.
 24670   * @param {boolean=} md-autoselect When present, any tabs added after the initial load will be automatically selected
 24671   * @param {boolean=} md-no-select-click When enabled, click events will not be fired when selecting tabs
 24672   *
 24673   * @usage
 24674   * <hljs lang="html">
 24675   * <md-tabs md-selected="selectedIndex" >
 24676   *   <img ng-src="img/angular.png" class="centered">
 24677   *   <md-tab
 24678   *       ng-repeat="tab in tabs | orderBy:predicate:reversed"
 24679   *       md-on-select="onTabSelected(tab)"
 24680   *       md-on-deselect="announceDeselected(tab)"
 24681   *       ng-disabled="tab.disabled">
 24682   *     <md-tab-label>
 24683   *       {{tab.title}}
 24684   *       <img src="img/removeTab.png" ng-click="removeTab(tab)" class="delete">
 24685   *     </md-tab-label>
 24686   *     <md-tab-body>
 24687   *       {{tab.content}}
 24688   *     </md-tab-body>
 24689   *   </md-tab>
 24690   * </md-tabs>
 24691   * </hljs>
 24692   *
 24693   */
 24694  angular
 24695      .module('material.components.tabs')
 24696      .directive('mdTabs', MdTabs);
 24697  
 24698  function MdTabs () {
 24699    return {
 24700      scope:            {
 24701        selectedIndex: '=?mdSelected'
 24702      },
 24703      template:         function (element, attr) {
 24704        attr[ "$mdTabsTemplate" ] = element.html();
 24705        return '' +
 24706          '<md-tabs-wrapper> ' +
 24707            '<md-tab-data></md-tab-data> ' +
 24708            '<md-prev-button ' +
 24709                'tabindex="-1" ' +
 24710                'role="button" ' +
 24711                'aria-label="Previous Page" ' +
 24712                'aria-disabled="{{!$mdTabsCtrl.canPageBack()}}" ' +
 24713                'ng-class="{ \'md-disabled\': !$mdTabsCtrl.canPageBack() }" ' +
 24714                'ng-if="$mdTabsCtrl.shouldPaginate" ' +
 24715                'ng-click="$mdTabsCtrl.previousPage()"> ' +
 24716              '<md-icon md-svg-icon="md-tabs-arrow"></md-icon> ' +
 24717            '</md-prev-button> ' +
 24718            '<md-next-button ' +
 24719                'tabindex="-1" ' +
 24720                'role="button" ' +
 24721                'aria-label="Next Page" ' +
 24722                'aria-disabled="{{!$mdTabsCtrl.canPageForward()}}" ' +
 24723                'ng-class="{ \'md-disabled\': !$mdTabsCtrl.canPageForward() }" ' +
 24724                'ng-if="$mdTabsCtrl.shouldPaginate" ' +
 24725                'ng-click="$mdTabsCtrl.nextPage()"> ' +
 24726              '<md-icon md-svg-icon="md-tabs-arrow"></md-icon> ' +
 24727            '</md-next-button> ' +
 24728            '<md-tabs-canvas ' +
 24729                'tabindex="{{ $mdTabsCtrl.hasFocus ? -1 : 0 }}" ' +
 24730                'aria-activedescendant="tab-item-{{$mdTabsCtrl.tabs[$mdTabsCtrl.focusIndex].id}}" ' +
 24731                'ng-focus="$mdTabsCtrl.redirectFocus()" ' +
 24732                'ng-class="{ ' +
 24733                    '\'md-paginated\': $mdTabsCtrl.shouldPaginate, ' +
 24734                    '\'md-center-tabs\': $mdTabsCtrl.shouldCenterTabs ' +
 24735                '}" ' +
 24736                'ng-keydown="$mdTabsCtrl.keydown($event)" ' +
 24737                'role="tablist"> ' +
 24738              '<md-pagination-wrapper ' +
 24739                  'ng-class="{ \'md-center-tabs\': $mdTabsCtrl.shouldCenterTabs }" ' +
 24740                  'md-tab-scroll="$mdTabsCtrl.scroll($event)"> ' +
 24741                '<md-tab-item ' +
 24742                    'tabindex="-1" ' +
 24743                    'class="md-tab" ' +
 24744                    'ng-repeat="tab in $mdTabsCtrl.tabs" ' +
 24745                    'role="tab" ' +
 24746                    'aria-controls="tab-content-{{::tab.id}}" ' +
 24747                    'aria-selected="{{tab.isActive()}}" ' +
 24748                    'aria-disabled="{{tab.scope.disabled || \'false\'}}" ' +
 24749                    'ng-click="$mdTabsCtrl.select(tab.getIndex())" ' +
 24750                    'ng-class="{ ' +
 24751                        '\'md-active\':    tab.isActive(), ' +
 24752                        '\'md-focused\':   tab.hasFocus(), ' +
 24753                        '\'md-disabled\':  tab.scope.disabled ' +
 24754                    '}" ' +
 24755                    'ng-disabled="tab.scope.disabled" ' +
 24756                    'md-swipe-left="$mdTabsCtrl.nextPage()" ' +
 24757                    'md-swipe-right="$mdTabsCtrl.previousPage()" ' +
 24758                    'md-tabs-template="::tab.label" ' +
 24759                    'md-scope="::tab.parent"></md-tab-item> ' +
 24760                '<md-ink-bar></md-ink-bar> ' +
 24761              '</md-pagination-wrapper> ' +
 24762              '<div class="md-visually-hidden md-dummy-wrapper"> ' +
 24763                '<md-dummy-tab ' +
 24764                    'class="md-tab" ' +
 24765                    'tabindex="-1" ' +
 24766                    'id="tab-item-{{::tab.id}}" ' +
 24767                    'role="tab" ' +
 24768                    'aria-controls="tab-content-{{::tab.id}}" ' +
 24769                    'aria-selected="{{tab.isActive()}}" ' +
 24770                    'aria-disabled="{{tab.scope.disabled || \'false\'}}" ' +
 24771                    'ng-focus="$mdTabsCtrl.hasFocus = true" ' +
 24772                    'ng-blur="$mdTabsCtrl.hasFocus = false" ' +
 24773                    'ng-repeat="tab in $mdTabsCtrl.tabs" ' +
 24774                    'md-tabs-template="::tab.label" ' +
 24775                    'md-scope="::tab.parent"></md-dummy-tab> ' +
 24776              '</div> ' +
 24777            '</md-tabs-canvas> ' +
 24778          '</md-tabs-wrapper> ' +
 24779          '<md-tabs-content-wrapper ng-show="$mdTabsCtrl.hasContent && $mdTabsCtrl.selectedIndex >= 0"> ' +
 24780            '<md-tab-content ' +
 24781                'id="tab-content-{{::tab.id}}" ' +
 24782                'role="tabpanel" ' +
 24783                'aria-labelledby="tab-item-{{::tab.id}}" ' +
 24784                'md-swipe-left="$mdTabsCtrl.swipeContent && $mdTabsCtrl.incrementIndex(1)" ' +
 24785                'md-swipe-right="$mdTabsCtrl.swipeContent && $mdTabsCtrl.incrementIndex(-1)" ' +
 24786                'ng-if="$mdTabsCtrl.hasContent" ' +
 24787                'ng-repeat="(index, tab) in $mdTabsCtrl.tabs" ' +
 24788                'ng-class="{ ' +
 24789                  '\'md-no-transition\': $mdTabsCtrl.lastSelectedIndex == null, ' +
 24790                  '\'md-active\':        tab.isActive(), ' +
 24791                  '\'md-left\':          tab.isLeft(), ' +
 24792                  '\'md-right\':         tab.isRight(), ' +
 24793                  '\'md-no-scroll\':     $mdTabsCtrl.dynamicHeight ' +
 24794                '}"> ' +
 24795              '<div ' +
 24796                  'md-tabs-template="::tab.template" ' +
 24797                  'md-connected-if="tab.isActive()" ' +
 24798                  'md-scope="::tab.parent" ' +
 24799                  'ng-if="$mdTabsCtrl.enableDisconnect || tab.shouldRender()"></div> ' +
 24800            '</md-tab-content> ' +
 24801          '</md-tabs-content-wrapper>';
 24802      },
 24803      controller:       'MdTabsController',
 24804      controllerAs:     '$mdTabsCtrl',
 24805      bindToController: true
 24806    };
 24807  }
 24808  
 24809  })();
 24810  (function(){
 24811  "use strict";
 24812  
 24813  angular
 24814      .module('material.components.tabs')
 24815      .directive('mdTabsTemplate', MdTabsTemplate);
 24816  
 24817  function MdTabsTemplate ($compile, $mdUtil) {
 24818    return {
 24819      restrict: 'A',
 24820      link:     link,
 24821      scope:    {
 24822        template:     '=mdTabsTemplate',
 24823        connected:    '=?mdConnectedIf',
 24824        compileScope: '=mdScope'
 24825      },
 24826      require:  '^?mdTabs'
 24827    };
 24828    function link (scope, element, attr, ctrl) {
 24829      if (!ctrl) return;
 24830      var compileScope = ctrl.enableDisconnect ? scope.compileScope.$new() : scope.compileScope;
 24831      element.html(scope.template);
 24832      $compile(element.contents())(compileScope);
 24833      new MutationObserver(function() {
 24834          ctrl.updatePagination();
 24835          ctrl.updateInkBarStyles();
 24836      }).observe(element[0], { childList: true, subtree: true } );
 24837      return $mdUtil.nextTick(handleScope);
 24838  
 24839      function handleScope () {
 24840        scope.$watch('connected', function (value) { value === false ? disconnect() : reconnect(); });
 24841        scope.$on('$destroy', reconnect);
 24842      }
 24843  
 24844      function disconnect () {
 24845        if (ctrl.enableDisconnect) $mdUtil.disconnectScope(compileScope);
 24846      }
 24847  
 24848      function reconnect () {
 24849        if (ctrl.enableDisconnect) $mdUtil.reconnectScope(compileScope);
 24850      }
 24851    }
 24852  }
 24853  MdTabsTemplate.$inject = ["$compile", "$mdUtil"];
 24854  
 24855  })();
 24856  (function(){ 
 24857  angular.module("material.core").constant("$MD_THEME_CSS", "md-autocomplete.md-THEME_NAME-theme {  background: '{{background-50}}'; }  md-autocomplete.md-THEME_NAME-theme[disabled] {    background: '{{background-100}}'; }  md-autocomplete.md-THEME_NAME-theme button md-icon path {    fill: '{{background-600}}'; }  md-autocomplete.md-THEME_NAME-theme button:after {    background: '{{background-600-0.3}}'; }.md-autocomplete-suggestions-container.md-THEME_NAME-theme {  background: '{{background-50}}'; }  .md-autocomplete-suggestions-container.md-THEME_NAME-theme li {    color: '{{background-900}}'; }    .md-autocomplete-suggestions-container.md-THEME_NAME-theme li .highlight {      color: '{{background-600}}'; }    .md-autocomplete-suggestions-container.md-THEME_NAME-theme li:hover, .md-autocomplete-suggestions-container.md-THEME_NAME-theme li.selected {      background: '{{background-200}}'; }md-backdrop {  background-color: '{{background-900-0.0}}'; }  md-backdrop.md-opaque.md-THEME_NAME-theme {    background-color: '{{background-900-1.0}}'; }a.md-button.md-THEME_NAME-theme:not([disabled]):hover,.md-button.md-THEME_NAME-theme:not([disabled]):hover {  background-color: '{{background-500-0.2}}'; }a.md-button.md-THEME_NAME-theme:not([disabled]).md-focused,.md-button.md-THEME_NAME-theme:not([disabled]).md-focused {  background-color: '{{background-500-0.2}}'; }a.md-button.md-THEME_NAME-theme:not([disabled]).md-icon-button:hover,.md-button.md-THEME_NAME-theme:not([disabled]).md-icon-button:hover {  background-color: transparent; }a.md-button.md-THEME_NAME-theme.md-fab,.md-button.md-THEME_NAME-theme.md-fab {  background-color: '{{accent-color}}';  color: '{{accent-contrast}}'; }  a.md-button.md-THEME_NAME-theme.md-fab md-icon,  .md-button.md-THEME_NAME-theme.md-fab md-icon {    color: '{{accent-contrast}}'; }  a.md-button.md-THEME_NAME-theme.md-fab:not([disabled]):hover,  .md-button.md-THEME_NAME-theme.md-fab:not([disabled]):hover {    background-color: '{{accent-color}}'; }  a.md-button.md-THEME_NAME-theme.md-fab:not([disabled]).md-focused,  .md-button.md-THEME_NAME-theme.md-fab:not([disabled]).md-focused {    background-color: '{{accent-A700}}'; }a.md-button.md-THEME_NAME-theme.md-primary,.md-button.md-THEME_NAME-theme.md-primary {  color: '{{primary-color}}'; }  a.md-button.md-THEME_NAME-theme.md-primary.md-raised, a.md-button.md-THEME_NAME-theme.md-primary.md-fab,  .md-button.md-THEME_NAME-theme.md-primary.md-raised,  .md-button.md-THEME_NAME-theme.md-primary.md-fab {    color: '{{primary-contrast}}';    background-color: '{{primary-color}}'; }    a.md-button.md-THEME_NAME-theme.md-primary.md-raised:not([disabled]) md-icon, a.md-button.md-THEME_NAME-theme.md-primary.md-fab:not([disabled]) md-icon,    .md-button.md-THEME_NAME-theme.md-primary.md-raised:not([disabled]) md-icon,    .md-button.md-THEME_NAME-theme.md-primary.md-fab:not([disabled]) md-icon {      color: '{{primary-contrast}}'; }    a.md-button.md-THEME_NAME-theme.md-primary.md-raised:not([disabled]):hover, a.md-button.md-THEME_NAME-theme.md-primary.md-fab:not([disabled]):hover,    .md-button.md-THEME_NAME-theme.md-primary.md-raised:not([disabled]):hover,    .md-button.md-THEME_NAME-theme.md-primary.md-fab:not([disabled]):hover {      background-color: '{{primary-color}}'; }    a.md-button.md-THEME_NAME-theme.md-primary.md-raised:not([disabled]).md-focused, a.md-button.md-THEME_NAME-theme.md-primary.md-fab:not([disabled]).md-focused,    .md-button.md-THEME_NAME-theme.md-primary.md-raised:not([disabled]).md-focused,    .md-button.md-THEME_NAME-theme.md-primary.md-fab:not([disabled]).md-focused {      background-color: '{{primary-600}}'; }  a.md-button.md-THEME_NAME-theme.md-primary:not([disabled]) md-icon,  .md-button.md-THEME_NAME-theme.md-primary:not([disabled]) md-icon {    color: '{{primary-color}}'; }a.md-button.md-THEME_NAME-theme.md-fab,.md-button.md-THEME_NAME-theme.md-fab {  background-color: '{{accent-color}}';  color: '{{accent-contrast}}'; }  a.md-button.md-THEME_NAME-theme.md-fab:not([disabled]) .md-icon,  .md-button.md-THEME_NAME-theme.md-fab:not([disabled]) .md-icon {    color: '{{accent-contrast}}'; }  a.md-button.md-THEME_NAME-theme.md-fab:not([disabled]):hover,  .md-button.md-THEME_NAME-theme.md-fab:not([disabled]):hover {    background-color: '{{accent-color}}'; }  a.md-button.md-THEME_NAME-theme.md-fab:not([disabled]).md-focused,  .md-button.md-THEME_NAME-theme.md-fab:not([disabled]).md-focused {    background-color: '{{accent-A700}}'; }a.md-button.md-THEME_NAME-theme.md-raised,.md-button.md-THEME_NAME-theme.md-raised {  color: '{{background-900}}';  background-color: '{{background-50}}'; }  a.md-button.md-THEME_NAME-theme.md-raised:not([disabled]) md-icon,  .md-button.md-THEME_NAME-theme.md-raised:not([disabled]) md-icon {    color: '{{background-900}}'; }  a.md-button.md-THEME_NAME-theme.md-raised:not([disabled]):hover,  .md-button.md-THEME_NAME-theme.md-raised:not([disabled]):hover {    background-color: '{{background-50}}'; }  a.md-button.md-THEME_NAME-theme.md-raised:not([disabled]).md-focused,  .md-button.md-THEME_NAME-theme.md-raised:not([disabled]).md-focused {    background-color: '{{background-200}}'; }a.md-button.md-THEME_NAME-theme.md-warn,.md-button.md-THEME_NAME-theme.md-warn {  color: '{{warn-color}}'; }  a.md-button.md-THEME_NAME-theme.md-warn.md-raised, a.md-button.md-THEME_NAME-theme.md-warn.md-fab,  .md-button.md-THEME_NAME-theme.md-warn.md-raised,  .md-button.md-THEME_NAME-theme.md-warn.md-fab {    color: '{{warn-contrast}}';    background-color: '{{warn-color}}'; }    a.md-button.md-THEME_NAME-theme.md-warn.md-raised:not([disabled]) md-icon, a.md-button.md-THEME_NAME-theme.md-warn.md-fab:not([disabled]) md-icon,    .md-button.md-THEME_NAME-theme.md-warn.md-raised:not([disabled]) md-icon,    .md-button.md-THEME_NAME-theme.md-warn.md-fab:not([disabled]) md-icon {      color: '{{warn-contrast}}'; }    a.md-button.md-THEME_NAME-theme.md-warn.md-raised:not([disabled]):hover, a.md-button.md-THEME_NAME-theme.md-warn.md-fab:not([disabled]):hover,    .md-button.md-THEME_NAME-theme.md-warn.md-raised:not([disabled]):hover,    .md-button.md-THEME_NAME-theme.md-warn.md-fab:not([disabled]):hover {      background-color: '{{warn-color}}'; }    a.md-button.md-THEME_NAME-theme.md-warn.md-raised:not([disabled]).md-focused, a.md-button.md-THEME_NAME-theme.md-warn.md-fab:not([disabled]).md-focused,    .md-button.md-THEME_NAME-theme.md-warn.md-raised:not([disabled]).md-focused,    .md-button.md-THEME_NAME-theme.md-warn.md-fab:not([disabled]).md-focused {      background-color: '{{warn-700}}'; }  a.md-button.md-THEME_NAME-theme.md-warn:not([disabled]) md-icon,  .md-button.md-THEME_NAME-theme.md-warn:not([disabled]) md-icon {    color: '{{warn-color}}'; }a.md-button.md-THEME_NAME-theme.md-accent,.md-button.md-THEME_NAME-theme.md-accent {  color: '{{accent-color}}'; }  a.md-button.md-THEME_NAME-theme.md-accent.md-raised, a.md-button.md-THEME_NAME-theme.md-accent.md-fab,  .md-button.md-THEME_NAME-theme.md-accent.md-raised,  .md-button.md-THEME_NAME-theme.md-accent.md-fab {    color: '{{accent-contrast}}';    background-color: '{{accent-color}}'; }    a.md-button.md-THEME_NAME-theme.md-accent.md-raised:not([disabled]) md-icon, a.md-button.md-THEME_NAME-theme.md-accent.md-fab:not([disabled]) md-icon,    .md-button.md-THEME_NAME-theme.md-accent.md-raised:not([disabled]) md-icon,    .md-button.md-THEME_NAME-theme.md-accent.md-fab:not([disabled]) md-icon {      color: '{{accent-contrast}}'; }    a.md-button.md-THEME_NAME-theme.md-accent.md-raised:not([disabled]):hover, a.md-button.md-THEME_NAME-theme.md-accent.md-fab:not([disabled]):hover,    .md-button.md-THEME_NAME-theme.md-accent.md-raised:not([disabled]):hover,    .md-button.md-THEME_NAME-theme.md-accent.md-fab:not([disabled]):hover {      background-color: '{{accent-color}}'; }    a.md-button.md-THEME_NAME-theme.md-accent.md-raised:not([disabled]).md-focused, a.md-button.md-THEME_NAME-theme.md-accent.md-fab:not([disabled]).md-focused,    .md-button.md-THEME_NAME-theme.md-accent.md-raised:not([disabled]).md-focused,    .md-button.md-THEME_NAME-theme.md-accent.md-fab:not([disabled]).md-focused {      background-color: '{{accent-700}}'; }  a.md-button.md-THEME_NAME-theme.md-accent:not([disabled]) md-icon,  .md-button.md-THEME_NAME-theme.md-accent:not([disabled]) md-icon {    color: '{{accent-color}}'; }a.md-button.md-THEME_NAME-theme[disabled], a.md-button.md-THEME_NAME-theme.md-raised[disabled], a.md-button.md-THEME_NAME-theme.md-fab[disabled], a.md-button.md-THEME_NAME-theme.md-accent[disabled], a.md-button.md-THEME_NAME-theme.md-warn[disabled],.md-button.md-THEME_NAME-theme[disabled],.md-button.md-THEME_NAME-theme.md-raised[disabled],.md-button.md-THEME_NAME-theme.md-fab[disabled],.md-button.md-THEME_NAME-theme.md-accent[disabled],.md-button.md-THEME_NAME-theme.md-warn[disabled] {  color: '{{foreground-3}}' !important;  cursor: default; }  a.md-button.md-THEME_NAME-theme[disabled] md-icon, a.md-button.md-THEME_NAME-theme.md-raised[disabled] md-icon, a.md-button.md-THEME_NAME-theme.md-fab[disabled] md-icon, a.md-button.md-THEME_NAME-theme.md-accent[disabled] md-icon, a.md-button.md-THEME_NAME-theme.md-warn[disabled] md-icon,  .md-button.md-THEME_NAME-theme[disabled] md-icon,  .md-button.md-THEME_NAME-theme.md-raised[disabled] md-icon,  .md-button.md-THEME_NAME-theme.md-fab[disabled] md-icon,  .md-button.md-THEME_NAME-theme.md-accent[disabled] md-icon,  .md-button.md-THEME_NAME-theme.md-warn[disabled] md-icon {    color: '{{foreground-3}}'; }a.md-button.md-THEME_NAME-theme.md-raised[disabled], a.md-button.md-THEME_NAME-theme.md-fab[disabled],.md-button.md-THEME_NAME-theme.md-raised[disabled],.md-button.md-THEME_NAME-theme.md-fab[disabled] {  background-color: '{{foreground-4}}'; }a.md-button.md-THEME_NAME-theme[disabled],.md-button.md-THEME_NAME-theme[disabled] {  background-color: transparent; }md-bottom-sheet.md-THEME_NAME-theme {  background-color: '{{background-50}}';  border-top-color: '{{background-300}}'; }  md-bottom-sheet.md-THEME_NAME-theme.md-list md-list-item {    color: '{{foreground-1}}'; }  md-bottom-sheet.md-THEME_NAME-theme .md-subheader {    background-color: '{{background-50}}'; }  md-bottom-sheet.md-THEME_NAME-theme .md-subheader {    color: '{{foreground-1}}'; }md-card.md-THEME_NAME-theme {  background-color: '{{background-color}}';  border-radius: 2px; }  md-card.md-THEME_NAME-theme .md-card-image {    border-radius: 2px 2px 0 0; }  md-card.md-THEME_NAME-theme md-card-header md-card-avatar md-icon {    color: '{{background-color}}';    background-color: '{{foreground-3}}'; }  md-card.md-THEME_NAME-theme md-card-header md-card-header-text .md-subhead {    color: '{{foreground-2}}'; }  md-card.md-THEME_NAME-theme md-card-title md-card-title-text:not(:only-child) .md-subhead {    color: '{{foreground-2}}'; }md-checkbox.md-THEME_NAME-theme .md-ripple {  color: '{{accent-600}}'; }md-checkbox.md-THEME_NAME-theme.md-checked .md-ripple {  color: '{{background-600}}'; }md-checkbox.md-THEME_NAME-theme.md-checked.md-focused .md-container:before {  background-color: '{{accent-color-0.26}}'; }md-checkbox.md-THEME_NAME-theme .md-ink-ripple {  color: '{{foreground-2}}'; }md-checkbox.md-THEME_NAME-theme.md-checked .md-ink-ripple {  color: '{{accent-color-0.87}}'; }md-checkbox.md-THEME_NAME-theme .md-icon {  border-color: '{{foreground-2}}'; }md-checkbox.md-THEME_NAME-theme.md-checked .md-icon {  background-color: '{{accent-color-0.87}}'; }md-checkbox.md-THEME_NAME-theme.md-checked .md-icon:after {  border-color: '{{accent-contrast-0.87}}'; }md-checkbox.md-THEME_NAME-theme:not([disabled]).md-primary .md-ripple {  color: '{{primary-600}}'; }md-checkbox.md-THEME_NAME-theme:not([disabled]).md-primary.md-checked .md-ripple {  color: '{{background-600}}'; }md-checkbox.md-THEME_NAME-theme:not([disabled]).md-primary .md-ink-ripple {  color: '{{foreground-2}}'; }md-checkbox.md-THEME_NAME-theme:not([disabled]).md-primary.md-checked .md-ink-ripple {  color: '{{primary-color-0.87}}'; }md-checkbox.md-THEME_NAME-theme:not([disabled]).md-primary .md-icon {  border-color: '{{foreground-2}}'; }md-checkbox.md-THEME_NAME-theme:not([disabled]).md-primary.md-checked .md-icon {  background-color: '{{primary-color-0.87}}'; }md-checkbox.md-THEME_NAME-theme:not([disabled]).md-primary.md-checked.md-focused .md-container:before {  background-color: '{{primary-color-0.26}}'; }md-checkbox.md-THEME_NAME-theme:not([disabled]).md-primary.md-checked .md-icon:after {  border-color: '{{primary-contrast-0.87}}'; }md-checkbox.md-THEME_NAME-theme:not([disabled]).md-warn .md-ripple {  color: '{{warn-600}}'; }md-checkbox.md-THEME_NAME-theme:not([disabled]).md-warn .md-ink-ripple {  color: '{{foreground-2}}'; }md-checkbox.md-THEME_NAME-theme:not([disabled]).md-warn.md-checked .md-ink-ripple {  color: '{{warn-color-0.87}}'; }md-checkbox.md-THEME_NAME-theme:not([disabled]).md-warn .md-icon {  border-color: '{{foreground-2}}'; }md-checkbox.md-THEME_NAME-theme:not([disabled]).md-warn.md-checked .md-icon {  background-color: '{{warn-color-0.87}}'; }md-checkbox.md-THEME_NAME-theme:not([disabled]).md-warn.md-checked.md-focused:not([disabled]) .md-container:before {  background-color: '{{warn-color-0.26}}'; }md-checkbox.md-THEME_NAME-theme:not([disabled]).md-warn.md-checked .md-icon:after {  border-color: '{{background-200}}'; }md-checkbox.md-THEME_NAME-theme[disabled] .md-icon {  border-color: '{{foreground-3}}'; }md-checkbox.md-THEME_NAME-theme[disabled].md-checked .md-icon {  background-color: '{{foreground-3}}'; }md-checkbox.md-THEME_NAME-theme[disabled].md-checked .md-icon:after {  border-color: '{{background-200}}'; }md-checkbox.md-THEME_NAME-theme[disabled] .md-label {  color: '{{foreground-3}}'; }md-content.md-THEME_NAME-theme {  color: '{{foreground-1}}';  background-color: '{{background-color}}'; }md-chips.md-THEME_NAME-theme .md-chips {  box-shadow: 0 1px '{{background-300}}'; }  md-chips.md-THEME_NAME-theme .md-chips.md-focused {    box-shadow: 0 2px '{{primary-color}}'; }md-chips.md-THEME_NAME-theme .md-chip {  background: '{{background-300}}';  color: '{{background-800}}'; }  md-chips.md-THEME_NAME-theme .md-chip.md-focused {    background: '{{primary-color}}';    color: '{{primary-contrast}}'; }    md-chips.md-THEME_NAME-theme .md-chip.md-focused md-icon {      color: '{{primary-contrast}}'; }md-chips.md-THEME_NAME-theme md-chip-remove .md-button md-icon path {  fill: '{{background-500}}'; }.md-contact-suggestion span.md-contact-email {  color: '{{background-400}}'; }/** Theme styles for mdCalendar. */.md-calendar.md-THEME_NAME-theme {  color: '{{foreground-1}}'; }  .md-calendar.md-THEME_NAME-theme tr:last-child td {    border-bottom-color: '{{background-200}}'; }.md-THEME_NAME-theme .md-calendar-day-header {  background: '{{background-hue-1}}';  color: '{{foreground-1}}'; }.md-THEME_NAME-theme .md-calendar-date.md-calendar-date-today .md-calendar-date-selection-indicator {  border: 1px solid '{{primary-500}}'; }.md-THEME_NAME-theme .md-calendar-date.md-calendar-date-today.md-calendar-date-disabled {  color: '{{primary-500-0.6}}'; }.md-THEME_NAME-theme .md-calendar-date.md-focus .md-calendar-date-selection-indicator {  background: '{{background-hue-1}}'; }.md-THEME_NAME-theme .md-calendar-date-selection-indicator:hover {  background: '{{background-hue-1}}'; }.md-THEME_NAME-theme .md-calendar-date.md-calendar-selected-date .md-calendar-date-selection-indicator,.md-THEME_NAME-theme .md-calendar-date.md-focus.md-calendar-selected-date .md-calendar-date-selection-indicator {  background: '{{primary-500}}';  color: '{{primary-500-contrast}}';  border-color: transparent; }.md-THEME_NAME-theme .md-calendar-date-disabled,.md-THEME_NAME-theme .md-calendar-month-label-disabled {  color: '{{foreground-3}}'; }/** Theme styles for mdDatepicker. */md-datepicker.md-THEME_NAME-theme {  background: '{{background-color}}'; }.md-THEME_NAME-theme .md-datepicker-input {  color: '{{background-contrast}}';  background: '{{background-color}}'; }  .md-THEME_NAME-theme .md-datepicker-input::-webkit-input-placeholder, .md-THEME_NAME-theme .md-datepicker-input::-moz-placeholder, .md-THEME_NAME-theme .md-datepicker-input:-moz-placeholder, .md-THEME_NAME-theme .md-datepicker-input:-ms-input-placeholder {    color: \"{{foreground-3}}\"; }.md-THEME_NAME-theme .md-datepicker-input-container {  border-bottom-color: '{{background-300}}'; }  .md-THEME_NAME-theme .md-datepicker-input-container.md-datepicker-focused {    border-bottom-color: '{{primary-500}}'; }  .md-THEME_NAME-theme .md-datepicker-input-container.md-datepicker-invalid {    border-bottom-color: '{{warn-A700}}'; }.md-THEME_NAME-theme .md-datepicker-calendar-pane {  border-color: '{{background-300}}'; }.md-THEME_NAME-theme .md-datepicker-triangle-button .md-datepicker-expand-triangle {  border-top-color: '{{foreground-3}}'; }.md-THEME_NAME-theme .md-datepicker-triangle-button:hover .md-datepicker-expand-triangle {  border-top-color: '{{foreground-2}}'; }.md-THEME_NAME-theme .md-datepicker-open .md-datepicker-calendar-icon {  fill: '{{primary-500}}'; }.md-THEME_NAME-theme .md-datepicker-calendar,.md-THEME_NAME-theme .md-datepicker-input-mask-opaque {  background: '{{background-color}}'; }md-dialog.md-THEME_NAME-theme {  border-radius: 4px;  background-color: '{{background-color}}'; }  md-dialog.md-THEME_NAME-theme.md-content-overflow .md-actions, md-dialog.md-THEME_NAME-theme.md-content-overflow md-dialog-actions {    border-top-color: '{{foreground-4}}'; }md-divider.md-THEME_NAME-theme {  border-top-color: '{{foreground-4}}'; }.layout-row > md-divider.md-THEME_NAME-theme {  border-right-color: '{{foreground-4}}'; }md-icon.md-THEME_NAME-theme {  color: '{{foreground-2}}'; }  md-icon.md-THEME_NAME-theme.md-primary {    color: '{{primary-color}}'; }  md-icon.md-THEME_NAME-theme.md-accent {    color: '{{accent-color}}'; }  md-icon.md-THEME_NAME-theme.md-warn {    color: '{{warn-color}}'; }md-input-container.md-THEME_NAME-theme .md-input {  color: '{{foreground-1}}';  border-color: '{{foreground-4}}';  text-shadow: '{{foreground-shadow}}'; }  md-input-container.md-THEME_NAME-theme .md-input::-webkit-input-placeholder, md-input-container.md-THEME_NAME-theme .md-input::-moz-placeholder, md-input-container.md-THEME_NAME-theme .md-input:-moz-placeholder, md-input-container.md-THEME_NAME-theme .md-input:-ms-input-placeholder {    color: \"{{foreground-3}}\"; }md-input-container.md-THEME_NAME-theme > md-icon {  color: '{{foreground-1}}'; }md-input-container.md-THEME_NAME-theme label,md-input-container.md-THEME_NAME-theme .md-placeholder {  text-shadow: '{{foreground-shadow}}';  color: '{{foreground-3}}'; }md-input-container.md-THEME_NAME-theme ng-messages, md-input-container.md-THEME_NAME-theme [ng-messages],md-input-container.md-THEME_NAME-theme ng-message, md-input-container.md-THEME_NAME-theme data-ng-message, md-input-container.md-THEME_NAME-theme x-ng-message,md-input-container.md-THEME_NAME-theme [ng-message], md-input-container.md-THEME_NAME-theme [data-ng-message], md-input-container.md-THEME_NAME-theme [x-ng-message],md-input-container.md-THEME_NAME-theme [ng-message-exp], md-input-container.md-THEME_NAME-theme [data-ng-message-exp], md-input-container.md-THEME_NAME-theme [x-ng-message-exp] {  color: '{{warn-A700}}'; }  md-input-container.md-THEME_NAME-theme ng-messages .md-char-counter, md-input-container.md-THEME_NAME-theme [ng-messages] .md-char-counter,  md-input-container.md-THEME_NAME-theme ng-message .md-char-counter, md-input-container.md-THEME_NAME-theme data-ng-message .md-char-counter, md-input-container.md-THEME_NAME-theme x-ng-message .md-char-counter,  md-input-container.md-THEME_NAME-theme [ng-message] .md-char-counter, md-input-container.md-THEME_NAME-theme [data-ng-message] .md-char-counter, md-input-container.md-THEME_NAME-theme [x-ng-message] .md-char-counter,  md-input-container.md-THEME_NAME-theme [ng-message-exp] .md-char-counter, md-input-container.md-THEME_NAME-theme [data-ng-message-exp] .md-char-counter, md-input-container.md-THEME_NAME-theme [x-ng-message-exp] .md-char-counter {    color: '{{foreground-1}}'; }md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-has-value label {  color: '{{foreground-2}}'; }md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-focused .md-input {  border-color: '{{primary-500}}'; }md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-focused label {  color: '{{primary-500}}'; }md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-focused md-icon {  color: '{{primary-500}}'; }md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-focused.md-accent .md-input {  border-color: '{{accent-500}}'; }md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-focused.md-accent label {  color: '{{accent-500}}'; }md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-focused.md-warn .md-input {  border-color: '{{warn-A700}}'; }md-input-container.md-THEME_NAME-theme:not(.md-input-invalid).md-input-focused.md-warn label {  color: '{{warn-A700}}'; }md-input-container.md-THEME_NAME-theme.md-input-invalid .md-input {  border-color: '{{warn-A700}}'; }md-input-container.md-THEME_NAME-theme.md-input-invalid.md-input-focused label {  color: '{{warn-A700}}'; }md-input-container.md-THEME_NAME-theme.md-input-invalid ng-message, md-input-container.md-THEME_NAME-theme.md-input-invalid data-ng-message, md-input-container.md-THEME_NAME-theme.md-input-invalid x-ng-message,md-input-container.md-THEME_NAME-theme.md-input-invalid [ng-message], md-input-container.md-THEME_NAME-theme.md-input-invalid [data-ng-message], md-input-container.md-THEME_NAME-theme.md-input-invalid [x-ng-message],md-input-container.md-THEME_NAME-theme.md-input-invalid [ng-message-exp], md-input-container.md-THEME_NAME-theme.md-input-invalid [data-ng-message-exp], md-input-container.md-THEME_NAME-theme.md-input-invalid [x-ng-message-exp],md-input-container.md-THEME_NAME-theme.md-input-invalid .md-char-counter {  color: '{{warn-A700}}'; }md-input-container.md-THEME_NAME-theme .md-input[disabled],md-input-container.md-THEME_NAME-theme .md-input [disabled] {  border-bottom-color: transparent;  color: '{{foreground-3}}';  background-image: linear-gradient(to right, \"{{foreground-3}}\" 0%, \"{{foreground-3}}\" 33%, transparent 0%);  background-image: -ms-linear-gradient(left, transparent 0%, \"{{foreground-3}}\" 100%); }md-menu-content.md-THEME_NAME-theme {  background-color: '{{background-color}}'; }  md-menu-content.md-THEME_NAME-theme md-menu-divider {    background-color: '{{foreground-4}}'; }md-list.md-THEME_NAME-theme md-list-item.md-2-line .md-list-item-text h3, md-list.md-THEME_NAME-theme md-list-item.md-2-line .md-list-item-text h4,md-list.md-THEME_NAME-theme md-list-item.md-3-line .md-list-item-text h3,md-list.md-THEME_NAME-theme md-list-item.md-3-line .md-list-item-text h4 {  color: '{{foreground-1}}'; }md-list.md-THEME_NAME-theme md-list-item.md-2-line .md-list-item-text p,md-list.md-THEME_NAME-theme md-list-item.md-3-line .md-list-item-text p {  color: '{{foreground-2}}'; }md-list.md-THEME_NAME-theme .md-proxy-focus.md-focused div.md-no-style {  background-color: '{{background-100}}'; }md-list.md-THEME_NAME-theme md-list-item > .md-avatar-icon {  background-color: '{{foreground-3}}';  color: '{{background-color}}'; }md-list.md-THEME_NAME-theme md-list-item > md-icon {  color: '{{foreground-2}}'; }  md-list.md-THEME_NAME-theme md-list-item > md-icon.md-highlight {    color: '{{primary-color}}'; }    md-list.md-THEME_NAME-theme md-list-item > md-icon.md-highlight.md-accent {      color: '{{accent-color}}'; }md-menu-bar.md-THEME_NAME-theme > button.md-button {  color: '{{foreground-2}}';  border-radius: 2px; }md-menu-bar.md-THEME_NAME-theme md-menu.md-open > button, md-menu-bar.md-THEME_NAME-theme md-menu > button:focus {  outline: none;  background: '{{background-200}}'; }md-menu-bar.md-THEME_NAME-theme.md-open:not(.md-keyboard-mode) md-menu:hover > button {  background-color: '{{ background-500-0.2}}'; }md-menu-bar.md-THEME_NAME-theme:not(.md-keyboard-mode):not(.md-open) md-menu button:hover,md-menu-bar.md-THEME_NAME-theme:not(.md-keyboard-mode):not(.md-open) md-menu button:focus {  background: transparent; }md-menu-content.md-THEME_NAME-theme .md-menu > .md-button:after {  color: '{{foreground-2}}'; }md-menu-content.md-THEME_NAME-theme .md-menu.md-open > .md-button {  background-color: '{{ background-500-0.2}}'; }md-toolbar.md-THEME_NAME-theme.md-menu-toolbar {  background-color: '{{background-color}}';  color: '{{foreground-1}}'; }  md-toolbar.md-THEME_NAME-theme.md-menu-toolbar md-toolbar-filler {    background-color: '{{primary-color}}';    color: '{{primary-contrast}}'; }    md-toolbar.md-THEME_NAME-theme.md-menu-toolbar md-toolbar-filler md-icon {      color: '{{primary-contrast}}'; }md-progress-circular.md-THEME_NAME-theme {  background-color: transparent; }  md-progress-circular.md-THEME_NAME-theme .md-inner .md-gap {    border-top-color: '{{primary-color}}';    border-bottom-color: '{{primary-color}}'; }  md-progress-circular.md-THEME_NAME-theme .md-inner .md-left .md-half-circle, md-progress-circular.md-THEME_NAME-theme .md-inner .md-right .md-half-circle {    border-top-color: '{{primary-color}}'; }  md-progress-circular.md-THEME_NAME-theme .md-inner .md-right .md-half-circle {    border-right-color: '{{primary-color}}'; }  md-progress-circular.md-THEME_NAME-theme .md-inner .md-left .md-half-circle {    border-left-color: '{{primary-color}}'; }  md-progress-circular.md-THEME_NAME-theme.md-warn .md-inner .md-gap {    border-top-color: '{{warn-color}}';    border-bottom-color: '{{warn-color}}'; }  md-progress-circular.md-THEME_NAME-theme.md-warn .md-inner .md-left .md-half-circle, md-progress-circular.md-THEME_NAME-theme.md-warn .md-inner .md-right .md-half-circle {    border-top-color: '{{warn-color}}'; }  md-progress-circular.md-THEME_NAME-theme.md-warn .md-inner .md-right .md-half-circle {    border-right-color: '{{warn-color}}'; }  md-progress-circular.md-THEME_NAME-theme.md-warn .md-inner .md-left .md-half-circle {    border-left-color: '{{warn-color}}'; }  md-progress-circular.md-THEME_NAME-theme.md-accent .md-inner .md-gap {    border-top-color: '{{accent-color}}';    border-bottom-color: '{{accent-color}}'; }  md-progress-circular.md-THEME_NAME-theme.md-accent .md-inner .md-left .md-half-circle, md-progress-circular.md-THEME_NAME-theme.md-accent .md-inner .md-right .md-half-circle {    border-top-color: '{{accent-color}}'; }  md-progress-circular.md-THEME_NAME-theme.md-accent .md-inner .md-right .md-half-circle {    border-right-color: '{{accent-color}}'; }  md-progress-circular.md-THEME_NAME-theme.md-accent .md-inner .md-left .md-half-circle {    border-left-color: '{{accent-color}}'; }md-progress-linear.md-THEME_NAME-theme .md-container {  background-color: '{{primary-100}}'; }md-progress-linear.md-THEME_NAME-theme .md-bar {  background-color: '{{primary-color}}'; }md-progress-linear.md-THEME_NAME-theme.md-warn .md-container {  background-color: '{{warn-100}}'; }md-progress-linear.md-THEME_NAME-theme.md-warn .md-bar {  background-color: '{{warn-color}}'; }md-progress-linear.md-THEME_NAME-theme.md-accent .md-container {  background-color: '{{accent-100}}'; }md-progress-linear.md-THEME_NAME-theme.md-accent .md-bar {  background-color: '{{accent-color}}'; }md-progress-linear.md-THEME_NAME-theme[md-mode=buffer].md-warn .md-bar1 {  background-color: '{{warn-100}}'; }md-progress-linear.md-THEME_NAME-theme[md-mode=buffer].md-warn .md-dashed:before {  background: radial-gradient(\"{{warn-100}}\" 0%, \"{{warn-100}}\" 16%, transparent 42%); }md-progress-linear.md-THEME_NAME-theme[md-mode=buffer].md-accent .md-bar1 {  background-color: '{{accent-100}}'; }md-progress-linear.md-THEME_NAME-theme[md-mode=buffer].md-accent .md-dashed:before {  background: radial-gradient(\"{{accent-100}}\" 0%, \"{{accent-100}}\" 16%, transparent 42%); }md-radio-button.md-THEME_NAME-theme .md-off {  border-color: '{{foreground-2}}'; }md-radio-button.md-THEME_NAME-theme .md-on {  background-color: '{{accent-color-0.87}}'; }md-radio-button.md-THEME_NAME-theme.md-checked .md-off {  border-color: '{{accent-color-0.87}}'; }md-radio-button.md-THEME_NAME-theme.md-checked .md-ink-ripple {  color: '{{accent-color-0.87}}'; }md-radio-button.md-THEME_NAME-theme .md-container .md-ripple {  color: '{{accent-600}}'; }md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-primary .md-on, md-radio-group.md-THEME_NAME-theme:not([disabled]).md-primary .md-on,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-primary .md-on,md-radio-button.md-THEME_NAME-theme:not([disabled]).md-primary .md-on {  background-color: '{{primary-color-0.87}}'; }md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-primary .md-checked .md-off, md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-primary.md-checked .md-off, md-radio-group.md-THEME_NAME-theme:not([disabled]).md-primary .md-checked .md-off, md-radio-group.md-THEME_NAME-theme:not([disabled]).md-primary.md-checked .md-off,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-primary .md-checked .md-off,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-primary.md-checked .md-off,md-radio-button.md-THEME_NAME-theme:not([disabled]).md-primary .md-checked .md-off,md-radio-button.md-THEME_NAME-theme:not([disabled]).md-primary.md-checked .md-off {  border-color: '{{primary-color-0.87}}'; }md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-primary .md-checked .md-ink-ripple, md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-primary.md-checked .md-ink-ripple, md-radio-group.md-THEME_NAME-theme:not([disabled]).md-primary .md-checked .md-ink-ripple, md-radio-group.md-THEME_NAME-theme:not([disabled]).md-primary.md-checked .md-ink-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-primary .md-checked .md-ink-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-primary.md-checked .md-ink-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]).md-primary .md-checked .md-ink-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]).md-primary.md-checked .md-ink-ripple {  color: '{{primary-color-0.87}}'; }md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-primary .md-container .md-ripple, md-radio-group.md-THEME_NAME-theme:not([disabled]).md-primary .md-container .md-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-primary .md-container .md-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]).md-primary .md-container .md-ripple {  color: '{{primary-600}}'; }md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-warn .md-on, md-radio-group.md-THEME_NAME-theme:not([disabled]).md-warn .md-on,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-warn .md-on,md-radio-button.md-THEME_NAME-theme:not([disabled]).md-warn .md-on {  background-color: '{{warn-color-0.87}}'; }md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-warn .md-checked .md-off, md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-warn.md-checked .md-off, md-radio-group.md-THEME_NAME-theme:not([disabled]).md-warn .md-checked .md-off, md-radio-group.md-THEME_NAME-theme:not([disabled]).md-warn.md-checked .md-off,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-warn .md-checked .md-off,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-warn.md-checked .md-off,md-radio-button.md-THEME_NAME-theme:not([disabled]).md-warn .md-checked .md-off,md-radio-button.md-THEME_NAME-theme:not([disabled]).md-warn.md-checked .md-off {  border-color: '{{warn-color-0.87}}'; }md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-warn .md-checked .md-ink-ripple, md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-warn.md-checked .md-ink-ripple, md-radio-group.md-THEME_NAME-theme:not([disabled]).md-warn .md-checked .md-ink-ripple, md-radio-group.md-THEME_NAME-theme:not([disabled]).md-warn.md-checked .md-ink-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-warn .md-checked .md-ink-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-warn.md-checked .md-ink-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]).md-warn .md-checked .md-ink-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]).md-warn.md-checked .md-ink-ripple {  color: '{{warn-color-0.87}}'; }md-radio-group.md-THEME_NAME-theme:not([disabled]) .md-warn .md-container .md-ripple, md-radio-group.md-THEME_NAME-theme:not([disabled]).md-warn .md-container .md-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]) .md-warn .md-container .md-ripple,md-radio-button.md-THEME_NAME-theme:not([disabled]).md-warn .md-container .md-ripple {  color: '{{warn-600}}'; }md-radio-group.md-THEME_NAME-theme[disabled],md-radio-button.md-THEME_NAME-theme[disabled] {  color: '{{foreground-3}}'; }  md-radio-group.md-THEME_NAME-theme[disabled] .md-container .md-off,  md-radio-button.md-THEME_NAME-theme[disabled] .md-container .md-off {    border-color: '{{foreground-3}}'; }  md-radio-group.md-THEME_NAME-theme[disabled] .md-container .md-on,  md-radio-button.md-THEME_NAME-theme[disabled] .md-container .md-on {    border-color: '{{foreground-3}}'; }md-radio-group.md-THEME_NAME-theme .md-checked .md-ink-ripple {  color: '{{accent-color-0.26}}'; }md-radio-group.md-THEME_NAME-theme.md-primary .md-checked:not([disabled]) .md-ink-ripple, md-radio-group.md-THEME_NAME-theme .md-checked:not([disabled]).md-primary .md-ink-ripple {  color: '{{primary-color-0.26}}'; }md-radio-group.md-THEME_NAME-theme .md-checked.md-primary .md-ink-ripple {  color: '{{warn-color-0.26}}'; }md-radio-group.md-THEME_NAME-theme.md-focused:not(:empty) .md-checked .md-container:before {  background-color: '{{accent-color-0.26}}'; }md-radio-group.md-THEME_NAME-theme.md-focused:not(:empty).md-primary .md-checked .md-container:before,md-radio-group.md-THEME_NAME-theme.md-focused:not(:empty) .md-checked.md-primary .md-container:before {  background-color: '{{primary-color-0.26}}'; }md-radio-group.md-THEME_NAME-theme.md-focused:not(:empty).md-warn .md-checked .md-container:before,md-radio-group.md-THEME_NAME-theme.md-focused:not(:empty) .md-checked.md-warn .md-container:before {  background-color: '{{warn-color-0.26}}'; }md-select.md-THEME_NAME-theme[disabled] .md-select-value {  border-bottom-color: transparent;  background-image: linear-gradient(to right, \"{{foreground-3}}\" 0%, \"{{foreground-3}}\" 33%, transparent 0%);  background-image: -ms-linear-gradient(left, transparent 0%, \"{{foreground-3}}\" 100%); }md-select.md-THEME_NAME-theme .md-select-value {  border-bottom-color: '{{foreground-4}}'; }  md-select.md-THEME_NAME-theme .md-select-value.md-select-placeholder {    color: '{{foreground-3}}'; }md-select.md-THEME_NAME-theme.ng-invalid.ng-dirty .md-select-value {  color: '{{warn-A700}}' !important;  border-bottom-color: '{{warn-A700}}' !important; }md-select.md-THEME_NAME-theme:not([disabled]):focus .md-select-value {  border-bottom-color: '{{primary-color}}';  color: '{{ foreground-1 }}'; }  md-select.md-THEME_NAME-theme:not([disabled]):focus .md-select-value.md-select-placeholder {    color: '{{ foreground-1 }}'; }md-select.md-THEME_NAME-theme:not([disabled]):focus.md-accent .md-select-value {  border-bottom-color: '{{accent-color}}'; }md-select.md-THEME_NAME-theme:not([disabled]):focus.md-warn .md-select-value {  border-bottom-color: '{{warn-color}}'; }md-select.md-THEME_NAME-theme[disabled] .md-select-value {  color: '{{foreground-3}}'; }  md-select.md-THEME_NAME-theme[disabled] .md-select-value.md-select-placeholder {    color: '{{foreground-3}}'; }md-select-menu.md-THEME_NAME-theme md-option[disabled] {  color: '{{foreground-3}}'; }md-select-menu.md-THEME_NAME-theme md-optgroup {  color: '{{foreground-2}}'; }  md-select-menu.md-THEME_NAME-theme md-optgroup md-option {    color: '{{foreground-1}}'; }md-select-menu.md-THEME_NAME-theme md-option[selected] {  color: '{{primary-500}}'; }  md-select-menu.md-THEME_NAME-theme md-option[selected]:focus {    color: '{{primary-600}}'; }  md-select-menu.md-THEME_NAME-theme md-option[selected].md-accent {    color: '{{accent-500}}'; }    md-select-menu.md-THEME_NAME-theme md-option[selected].md-accent:focus {      color: '{{accent-600}}'; }md-select-menu.md-THEME_NAME-theme md-option:focus:not([disabled]):not([selected]) {  background: '{{background-200}}'; }md-sidenav.md-THEME_NAME-theme {  background-color: '{{background-color}}'; }md-slider.md-THEME_NAME-theme .md-track {  background-color: '{{foreground-3}}'; }md-slider.md-THEME_NAME-theme .md-track-ticks {  background-color: '{{foreground-4}}'; }md-slider.md-THEME_NAME-theme .md-focus-thumb {  background-color: '{{foreground-2}}'; }md-slider.md-THEME_NAME-theme .md-focus-ring {  background-color: '{{accent-color}}'; }md-slider.md-THEME_NAME-theme .md-disabled-thumb {  border-color: '{{background-color}}'; }md-slider.md-THEME_NAME-theme.md-min .md-thumb:after {  background-color: '{{background-color}}'; }md-slider.md-THEME_NAME-theme .md-track.md-track-fill {  background-color: '{{accent-color}}'; }md-slider.md-THEME_NAME-theme .md-thumb:after {  border-color: '{{accent-color}}';  background-color: '{{accent-color}}'; }md-slider.md-THEME_NAME-theme .md-sign {  background-color: '{{accent-color}}'; }  md-slider.md-THEME_NAME-theme .md-sign:after {    border-top-color: '{{accent-color}}'; }md-slider.md-THEME_NAME-theme .md-thumb-text {  color: '{{accent-contrast}}'; }md-slider.md-THEME_NAME-theme.md-warn .md-focus-ring {  background-color: '{{warn-color}}'; }md-slider.md-THEME_NAME-theme.md-warn .md-track.md-track-fill {  background-color: '{{warn-color}}'; }md-slider.md-THEME_NAME-theme.md-warn .md-thumb:after {  border-color: '{{warn-color}}';  background-color: '{{warn-color}}'; }md-slider.md-THEME_NAME-theme.md-warn .md-sign {  background-color: '{{warn-color}}'; }  md-slider.md-THEME_NAME-theme.md-warn .md-sign:after {    border-top-color: '{{warn-color}}'; }md-slider.md-THEME_NAME-theme.md-warn .md-thumb-text {  color: '{{warn-contrast}}'; }md-slider.md-THEME_NAME-theme.md-primary .md-focus-ring {  background-color: '{{primary-color}}'; }md-slider.md-THEME_NAME-theme.md-primary .md-track.md-track-fill {  background-color: '{{primary-color}}'; }md-slider.md-THEME_NAME-theme.md-primary .md-thumb:after {  border-color: '{{primary-color}}';  background-color: '{{primary-color}}'; }md-slider.md-THEME_NAME-theme.md-primary .md-sign {  background-color: '{{primary-color}}'; }  md-slider.md-THEME_NAME-theme.md-primary .md-sign:after {    border-top-color: '{{primary-color}}'; }md-slider.md-THEME_NAME-theme.md-primary .md-thumb-text {  color: '{{primary-contrast}}'; }md-slider.md-THEME_NAME-theme[disabled] .md-thumb:after {  border-color: '{{foreground-3}}'; }md-slider.md-THEME_NAME-theme[disabled]:not(.md-min) .md-thumb:after {  background-color: '{{foreground-3}}'; }.md-subheader.md-THEME_NAME-theme {  color: '{{ foreground-2-0.23 }}';  background-color: '{{background-color}}'; }  .md-subheader.md-THEME_NAME-theme.md-primary {    color: '{{primary-color}}'; }  .md-subheader.md-THEME_NAME-theme.md-accent {    color: '{{accent-color}}'; }  .md-subheader.md-THEME_NAME-theme.md-warn {    color: '{{warn-color}}'; }md-switch.md-THEME_NAME-theme .md-ink-ripple {  color: '{{background-500}}'; }md-switch.md-THEME_NAME-theme .md-thumb {  background-color: '{{background-50}}'; }md-switch.md-THEME_NAME-theme .md-bar {  background-color: '{{background-500}}'; }md-switch.md-THEME_NAME-theme.md-checked .md-ink-ripple {  color: '{{accent-color}}'; }md-switch.md-THEME_NAME-theme.md-checked .md-thumb {  background-color: '{{accent-color}}'; }md-switch.md-THEME_NAME-theme.md-checked .md-bar {  background-color: '{{accent-color-0.5}}'; }md-switch.md-THEME_NAME-theme.md-checked.md-focused .md-thumb:before {  background-color: '{{accent-color-0.26}}'; }md-switch.md-THEME_NAME-theme.md-checked.md-primary .md-ink-ripple {  color: '{{primary-color}}'; }md-switch.md-THEME_NAME-theme.md-checked.md-primary .md-thumb {  background-color: '{{primary-color}}'; }md-switch.md-THEME_NAME-theme.md-checked.md-primary .md-bar {  background-color: '{{primary-color-0.5}}'; }md-switch.md-THEME_NAME-theme.md-checked.md-primary.md-focused .md-thumb:before {  background-color: '{{primary-color-0.26}}'; }md-switch.md-THEME_NAME-theme.md-checked.md-warn .md-ink-ripple {  color: '{{warn-color}}'; }md-switch.md-THEME_NAME-theme.md-checked.md-warn .md-thumb {  background-color: '{{warn-color}}'; }md-switch.md-THEME_NAME-theme.md-checked.md-warn .md-bar {  background-color: '{{warn-color-0.5}}'; }md-switch.md-THEME_NAME-theme.md-checked.md-warn.md-focused .md-thumb:before {  background-color: '{{warn-color-0.26}}'; }md-switch.md-THEME_NAME-theme[disabled] .md-thumb {  background-color: '{{background-400}}'; }md-switch.md-THEME_NAME-theme[disabled] .md-bar {  background-color: '{{foreground-4}}'; }md-tabs.md-THEME_NAME-theme md-tabs-wrapper {  background-color: transparent;  border-color: '{{foreground-4}}'; }md-tabs.md-THEME_NAME-theme .md-paginator md-icon {  color: '{{primary-color}}'; }md-tabs.md-THEME_NAME-theme md-ink-bar {  color: '{{accent-color}}';  background: '{{accent-color}}'; }md-tabs.md-THEME_NAME-theme .md-tab {  color: '{{foreground-2}}'; }  md-tabs.md-THEME_NAME-theme .md-tab[disabled], md-tabs.md-THEME_NAME-theme .md-tab[disabled] md-icon {    color: '{{foreground-3}}'; }  md-tabs.md-THEME_NAME-theme .md-tab.md-active, md-tabs.md-THEME_NAME-theme .md-tab.md-active md-icon, md-tabs.md-THEME_NAME-theme .md-tab.md-focused, md-tabs.md-THEME_NAME-theme .md-tab.md-focused md-icon {    color: '{{primary-color}}'; }  md-tabs.md-THEME_NAME-theme .md-tab.md-focused {    background: '{{primary-color-0.1}}'; }  md-tabs.md-THEME_NAME-theme .md-tab .md-ripple-container {    color: '{{accent-100}}'; }md-tabs.md-THEME_NAME-theme.md-accent > md-tabs-wrapper {  background-color: '{{accent-color}}'; }  md-tabs.md-THEME_NAME-theme.md-accent > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]) {    color: '{{accent-100}}'; }    md-tabs.md-THEME_NAME-theme.md-accent > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-active, md-tabs.md-THEME_NAME-theme.md-accent > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-active md-icon, md-tabs.md-THEME_NAME-theme.md-accent > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-focused, md-tabs.md-THEME_NAME-theme.md-accent > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-focused md-icon {      color: '{{accent-contrast}}'; }    md-tabs.md-THEME_NAME-theme.md-accent > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-focused {      background: '{{accent-contrast-0.1}}'; }  md-tabs.md-THEME_NAME-theme.md-accent > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-ink-bar {    color: '{{primary-600-1}}';    background: '{{primary-600-1}}'; }md-tabs.md-THEME_NAME-theme.md-primary > md-tabs-wrapper {  background-color: '{{primary-color}}'; }  md-tabs.md-THEME_NAME-theme.md-primary > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]) {    color: '{{primary-100}}'; }    md-tabs.md-THEME_NAME-theme.md-primary > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-active, md-tabs.md-THEME_NAME-theme.md-primary > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-active md-icon, md-tabs.md-THEME_NAME-theme.md-primary > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-focused, md-tabs.md-THEME_NAME-theme.md-primary > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-focused md-icon {      color: '{{primary-contrast}}'; }    md-tabs.md-THEME_NAME-theme.md-primary > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-focused {      background: '{{primary-contrast-0.1}}'; }md-tabs.md-THEME_NAME-theme.md-warn > md-tabs-wrapper {  background-color: '{{warn-color}}'; }  md-tabs.md-THEME_NAME-theme.md-warn > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]) {    color: '{{warn-100}}'; }    md-tabs.md-THEME_NAME-theme.md-warn > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-active, md-tabs.md-THEME_NAME-theme.md-warn > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-active md-icon, md-tabs.md-THEME_NAME-theme.md-warn > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-focused, md-tabs.md-THEME_NAME-theme.md-warn > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-focused md-icon {      color: '{{warn-contrast}}'; }    md-tabs.md-THEME_NAME-theme.md-warn > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-focused {      background: '{{warn-contrast-0.1}}'; }md-toolbar > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper {  background-color: '{{primary-color}}'; }  md-toolbar > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]) {    color: '{{primary-100}}'; }    md-toolbar > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-active, md-toolbar > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-active md-icon, md-toolbar > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-focused, md-toolbar > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-focused md-icon {      color: '{{primary-contrast}}'; }    md-toolbar > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-focused {      background: '{{primary-contrast-0.1}}'; }md-toolbar.md-accent > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper {  background-color: '{{accent-color}}'; }  md-toolbar.md-accent > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]) {    color: '{{accent-100}}'; }    md-toolbar.md-accent > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-active, md-toolbar.md-accent > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-active md-icon, md-toolbar.md-accent > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-focused, md-toolbar.md-accent > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-focused md-icon {      color: '{{accent-contrast}}'; }    md-toolbar.md-accent > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-focused {      background: '{{accent-contrast-0.1}}'; }  md-toolbar.md-accent > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-ink-bar {    color: '{{primary-600-1}}';    background: '{{primary-600-1}}'; }md-toolbar.md-warn > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper {  background-color: '{{warn-color}}'; }  md-toolbar.md-warn > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]) {    color: '{{warn-100}}'; }    md-toolbar.md-warn > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-active, md-toolbar.md-warn > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-active md-icon, md-toolbar.md-warn > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-focused, md-toolbar.md-warn > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-focused md-icon {      color: '{{warn-contrast}}'; }    md-toolbar.md-warn > md-tabs.md-THEME_NAME-theme > md-tabs-wrapper > md-tabs-canvas > md-pagination-wrapper > md-tab-item:not([disabled]).md-focused {      background: '{{warn-contrast-0.1}}'; }md-toast.md-THEME_NAME-theme .md-toast-content {  background-color: #323232;  color: '{{background-50}}'; }  md-toast.md-THEME_NAME-theme .md-toast-content .md-button {    color: '{{background-50}}'; }    md-toast.md-THEME_NAME-theme .md-toast-content .md-button.md-highlight {      color: '{{primary-A200}}'; }      md-toast.md-THEME_NAME-theme .md-toast-content .md-button.md-highlight.md-accent {        color: '{{accent-A200}}'; }      md-toast.md-THEME_NAME-theme .md-toast-content .md-button.md-highlight.md-warn {        color: '{{warn-A200}}'; }md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar) {  background-color: '{{primary-color}}';  color: '{{primary-contrast}}'; }  md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar) md-icon {    color: '{{primary-contrast}}'; }  md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar) .md-button:not(.md-raised) {    color: '{{primary-contrast}}'; }  md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-accent {    background-color: '{{accent-color}}';    color: '{{accent-contrast}}'; }  md-toolbar.md-THEME_NAME-theme:not(.md-menu-toolbar).md-warn {    background-color: '{{warn-color}}';    color: '{{warn-contrast}}'; }md-tooltip.md-THEME_NAME-theme {  color: '{{background-A100}}'; }  md-tooltip.md-THEME_NAME-theme .md-content {    background-color: '{{foreground-2}}'; }"); 
 24858  })();
 24859  
 24860  
 24861  })(window, window.angular);;window.ngMaterial={version:{full: "1.0.7"}};