2  * Angular Material Design
 
   3  * https://github.com/angular/material
 
   7 goog.provide('ngmaterial.components.select');
 
   8 goog.require('ngmaterial.components.backdrop');
 
   9 goog.require('ngmaterial.core');
 
  12  * @name material.components.select
 
  15 /***************************************************
 
  17  ### TODO - POST RC1 ###
 
  18  - [ ] Abstract placement logic in $mdSelect service to $mdMenu service
 
  20  ***************************************************/
 
  22 SelectDirective['$inject'] = ["$mdSelect", "$mdUtil", "$mdConstant", "$mdTheming", "$mdAria", "$parse", "$sce", "$injector"];
 
  23 SelectMenuDirective['$inject'] = ["$parse", "$mdUtil", "$mdConstant", "$mdTheming"];
 
  24 OptionDirective['$inject'] = ["$mdButtonInkRipple", "$mdUtil"];
 
  25 SelectProvider['$inject'] = ["$$interimElementProvider"];
 
  26 var SELECT_EDGE_MARGIN = 8;
 
  28 var CHECKBOX_SELECTION_INDICATOR =
 
  29   angular.element('<div class="md-container"><div class="md-icon"></div></div>');
 
  31 angular.module('material.components.select', [
 
  33     'material.components.backdrop'
 
  35   .directive('mdSelect', SelectDirective)
 
  36   .directive('mdSelectMenu', SelectMenuDirective)
 
  37   .directive('mdOption', OptionDirective)
 
  38   .directive('mdOptgroup', OptgroupDirective)
 
  39   .directive('mdSelectHeader', SelectHeaderDirective)
 
  40   .provider('$mdSelect', SelectProvider);
 
  46  * @module material.components.select
 
  48  * @description Displays a select box, bound to an ng-model.
 
  50  * When the select is required and uses a floating label, then the label will automatically contain
 
  51  * an asterisk (`*`). This behavior can be disabled by using the `md-no-asterisk` attribute.
 
  53  * By default, the select will display with an underline to match other form elements. This can be
 
  54  * disabled by applying the `md-no-underline` CSS class.
 
  58  * When applied, `md-option-empty` will mark the option as "empty" allowing the option to  clear the
 
  59  * select and put it back in it's default state. You may supply this attribute on any option you
 
  60  * wish, however, it is automatically applied to an option whose `value` or `ng-value` are not
 
  63  * **Automatically Applied**
 
  66  *  - `<md-option value>`
 
  67  *  - `<md-option value="">`
 
  68  *  - `<md-option ng-value>`
 
  69  *  - `<md-option ng-value="">`
 
  71  * **NOT Automatically Applied**
 
  73  *  - `<md-option ng-value="1">`
 
  74  *  - `<md-option ng-value="''">`
 
  75  *  - `<md-option ng-value="undefined">`
 
  76  *  - `<md-option value="undefined">` (this evaluates to the string `"undefined"`)
 
  77  *  - <code ng-non-bindable><md-option ng-value="{{someValueThatMightBeUndefined}}"></code>
 
  79  * **Note:** A value of `undefined` ***is considered a valid value*** (and does not auto-apply this
 
  80  * attribute) since you may wish this to be your "Not Available" or "None" option.
 
  82  * **Note:** Using the `value` attribute (as opposed to `ng-value`) always evaluates to a string, so
 
  83  * `value="null"` will require the test `ng-if="myValue != 'null'"` rather than `ng-if="!myValue"`.
 
  85  * @param {expression} ng-model The model!
 
  86  * @param {boolean=} multiple When set to true, allows for more than one option to be selected. The model is an array with the selected choices.
 
  87  * @param {expression=} md-on-close Expression to be evaluated when the select is closed.
 
  88  * @param {expression=} md-on-open Expression to be evaluated when opening the select.
 
  89  * Will hide the select options and show a spinner until the evaluated promise resolves.
 
  90  * @param {expression=} md-selected-text Expression to be evaluated that will return a string
 
  91  * to be displayed as a placeholder in the select input box when it is closed. The value
 
  92  * will be treated as *text* (not html).
 
  93  * @param {expression=} md-selected-html Expression to be evaluated that will return a string
 
  94  * to be displayed as a placeholder in the select input box when it is closed. The value
 
  95  * will be treated as *html*. The value must either be explicitly marked as trustedHtml or
 
  96  * the ngSanitize module must be loaded.
 
  97  * @param {string=} placeholder Placeholder hint text.
 
  98  * @param md-no-asterisk {boolean=} When set to true, an asterisk will not be appended to the
 
  99  * floating label. **Note:** This attribute is only evaluated once; it is not watched.
 
 100  * @param {string=} aria-label Optional label for accessibility. Only necessary if no placeholder or
 
 101  * explicit label is present.
 
 102  * @param {string=} md-container-class Class list to get applied to the `.md-select-menu-container`
 
 103  * element (for custom styling).
 
 106  * With a placeholder (label and aria-label are added dynamically)
 
 108  *   <md-input-container>
 
 110  *       ng-model="someModel"
 
 111  *       placeholder="Select a state">
 
 112  *       <md-option ng-value="opt" ng-repeat="opt in neighborhoods2">{{ opt }}</md-option>
 
 114  *   </md-input-container>
 
 117  * With an explicit label
 
 119  *   <md-input-container>
 
 120  *     <label>State</label>
 
 122  *       ng-model="someModel">
 
 123  *       <md-option ng-value="opt" ng-repeat="opt in neighborhoods2">{{ opt }}</md-option>
 
 125  *   </md-input-container>
 
 128  * With a select-header
 
 130  * When a developer needs to put more than just a text label in the
 
 131  * md-select-menu, they should use the md-select-header.
 
 132  * The user can put custom HTML inside of the header and style it to their liking.
 
 133  * One common use case of this would be a sticky search bar.
 
 135  * When using the md-select-header the labels that would previously be added to the
 
 136  * OptGroupDirective are ignored.
 
 139  *   <md-input-container>
 
 140  *     <md-select ng-model="someModel">
 
 142  *         <span> Neighborhoods - </span>
 
 143  *       </md-select-header>
 
 144  *       <md-option ng-value="opt" ng-repeat="opt in neighborhoods2">{{ opt }}</md-option>
 
 146  *   </md-input-container>
 
 149  * ## Selects and object equality
 
 150  * When using a `md-select` to pick from a list of objects, it is important to realize how javascript handles
 
 151  * equality. Consider the following example:
 
 153  * angular.controller('MyCtrl', function($scope) {
 
 155  *     { id: 1, name: 'Bob' },
 
 156  *     { id: 2, name: 'Alice' },
 
 157  *     { id: 3, name: 'Steve' }
 
 159  *   $scope.selectedUser = { id: 1, name: 'Bob' };
 
 163  * <div ng-controller="MyCtrl">
 
 164  *   <md-select ng-model="selectedUser">
 
 165  *     <md-option ng-value="user" ng-repeat="user in users">{{ user.name }}</md-option>
 
 170  * At first one might expect that the select should be populated with "Bob" as the selected user. However,
 
 171  * this is not true. To determine whether something is selected,
 
 172  * `ngModelController` is looking at whether `$scope.selectedUser == (any user in $scope.users);`;
 
 174  * Javascript's `==` operator does not check for deep equality (ie. that all properties
 
 175  * on the object are the same), but instead whether the objects are *the same object in memory*.
 
 176  * In this case, we have two instances of identical objects, but they exist in memory as unique
 
 177  * entities. Because of this, the select will have no value populated for a selected user.
 
 179  * To get around this, `ngModelController` provides a `track by` option that allows us to specify a different
 
 180  * expression which will be used for the equality operator. As such, we can update our `html` to
 
 181  * make use of this by specifying the `ng-model-options="{trackBy: '$value.id'}"` on the `md-select`
 
 182  * element. This converts our equality expression to be
 
 183  * `$scope.selectedUser.id == (any id in $scope.users.map(function(u) { return u.id; }));`
 
 184  * which results in Bob being selected as desired.
 
 188  * <div ng-controller="MyCtrl">
 
 189  *   <md-select ng-model="selectedUser" ng-model-options="{trackBy: '$value.id'}">
 
 190  *     <md-option ng-value="user" ng-repeat="user in users">{{ user.name }}</md-option>
 
 195 function SelectDirective($mdSelect, $mdUtil, $mdConstant, $mdTheming, $mdAria, $parse, $sce,
 
 197   var keyCodes = $mdConstant.KEY_CODE;
 
 198   var NAVIGATION_KEYS = [keyCodes.SPACE, keyCodes.ENTER, keyCodes.UP_ARROW, keyCodes.DOWN_ARROW];
 
 202     require: ['^?mdInputContainer', 'mdSelect', 'ngModel', '?^form'],
 
 204     controller: function() {
 
 205     } // empty placeholder controller to be initialized in link
 
 208   function compile(element, attr) {
 
 209     // add the select value that will hold our placeholder or selected option value
 
 210     var valueEl = angular.element('<md-select-value><span></span></md-select-value>');
 
 211     valueEl.append('<span class="md-select-icon" aria-hidden="true"></span>');
 
 212     valueEl.addClass('md-select-value');
 
 213     if (!valueEl[0].hasAttribute('id')) {
 
 214       valueEl.attr('id', 'select_value_label_' + $mdUtil.nextUid());
 
 217     // There's got to be an md-content inside. If there's not one, let's add it.
 
 218     if (!element.find('md-content').length) {
 
 219       element.append(angular.element('<md-content>').append(element.contents()));
 
 223     // Add progress spinner for md-options-loading
 
 226       // Show progress indicator while loading async
 
 227       // Use ng-hide for `display:none` so the indicator does not interfere with the options list
 
 230         .prepend(angular.element(
 
 232           ' <md-progress-circular md-mode="indeterminate" ng-if="$$loadingAsyncDone === false" md-diameter="25px"></md-progress-circular>' +
 
 236       // Hide list [of item options] while loading async
 
 239         .attr('ng-show', '$$loadingAsyncDone');
 
 243       var autofillClone = angular.element('<select class="md-visually-hidden">');
 
 246         'aria-hidden': 'true',
 
 249       var opts = element.find('md-option');
 
 250       angular.forEach(opts, function(el) {
 
 251         var newEl = angular.element('<option>' + el.innerHTML + '</option>');
 
 252         if (el.hasAttribute('ng-value')) newEl.attr('ng-value', el.getAttribute('ng-value'));
 
 253         else if (el.hasAttribute('value')) newEl.attr('value', el.getAttribute('value'));
 
 254         autofillClone.append(newEl);
 
 257       // Adds an extra option that will hold the selected value for the
 
 258       // cases where the select is a part of a non-angular form. This can be done with a ng-model,
 
 259       // however if the `md-option` is being `ng-repeat`-ed, Angular seems to insert a similar
 
 260       // `option` node, but with a value of `? string: <value> ?` which would then get submitted.
 
 261       // This also goes around having to prepend a dot to the name attribute.
 
 262       autofillClone.append(
 
 263         '<option ng-value="' + attr.ngModel + '" selected></option>'
 
 266       element.parent().append(autofillClone);
 
 269     var isMultiple = $mdUtil.parseAttributeBoolean(attr.multiple);
 
 271     // Use everything that's left inside element.contents() as the contents of the menu
 
 272     var multipleContent = isMultiple ? 'multiple' : '';
 
 273     var selectTemplate = '' +
 
 274       '<div class="md-select-menu-container" aria-hidden="true">' +
 
 275       '<md-select-menu {0}>{1}</md-select-menu>' +
 
 278     selectTemplate = $mdUtil.supplant(selectTemplate, [multipleContent, element.html()]);
 
 279     element.empty().append(valueEl);
 
 280     element.append(selectTemplate);
 
 283       attr.$set('tabindex', 0);
 
 286     return function postLink(scope, element, attr, ctrls) {
 
 287       var untouched = true;
 
 288       var isDisabled, ariaLabelBase;
 
 290       var containerCtrl = ctrls[0];
 
 291       var mdSelectCtrl = ctrls[1];
 
 292       var ngModelCtrl = ctrls[2];
 
 293       var formCtrl = ctrls[3];
 
 294       // grab a reference to the select menu value label
 
 295       var valueEl = element.find('md-select-value');
 
 296       var isReadonly = angular.isDefined(attr.readonly);
 
 297       var disableAsterisk = $mdUtil.parseAttributeBoolean(attr.mdNoAsterisk);
 
 299       if (disableAsterisk) {
 
 300         element.addClass('md-no-asterisk');
 
 304         var isErrorGetter = containerCtrl.isErrorGetter || function() {
 
 305           return ngModelCtrl.$invalid && (ngModelCtrl.$touched || (formCtrl && formCtrl.$submitted));
 
 308         if (containerCtrl.input) {
 
 309           // We ignore inputs that are in the md-select-header (one
 
 310           // case where this might be useful would be adding as searchbox)
 
 311           if (element.find('md-select-header').find('input')[0] !== containerCtrl.input[0]) {
 
 312             throw new Error("<md-input-container> can only have *one* child <input>, <textarea> or <select> element!");
 
 316         containerCtrl.input = element;
 
 317         if (!containerCtrl.label) {
 
 318           $mdAria.expect(element, 'aria-label', element.attr('placeholder'));
 
 321         scope.$watch(isErrorGetter, containerCtrl.setInvalid);
 
 324       var selectContainer, selectScope, selectMenuCtrl;
 
 326       findSelectContainer();
 
 329       if (formCtrl && angular.isDefined(attr.multiple)) {
 
 330         $mdUtil.nextTick(function() {
 
 331           var hasModelValue = ngModelCtrl.$modelValue || ngModelCtrl.$viewValue;
 
 333             formCtrl.$setPristine();
 
 338       var originalRender = ngModelCtrl.$render;
 
 339       ngModelCtrl.$render = function() {
 
 346       attr.$observe('placeholder', ngModelCtrl.$render);
 
 348       if (containerCtrl && containerCtrl.label) {
 
 349         attr.$observe('required', function (value) {
 
 350           // Toggle the md-required class on the input containers label, because the input container is automatically
 
 351           // applying the asterisk indicator on the label.
 
 352           containerCtrl.label.toggleClass('md-required', value && !disableAsterisk);
 
 356       mdSelectCtrl.setLabelText = function(text) {
 
 357         mdSelectCtrl.setIsPlaceholder(!text);
 
 359         // Whether the select label has been given via user content rather than the internal
 
 360         // template of <md-option>
 
 361         var isSelectLabelFromUser = false;
 
 363         if (attr.mdSelectedText && attr.mdSelectedHtml) {
 
 364           throw Error('md-select cannot have both `md-selected-text` and `md-selected-html`');
 
 367         if (attr.mdSelectedText || attr.mdSelectedHtml) {
 
 368           text = $parse(attr.mdSelectedText || attr.mdSelectedHtml)(scope);
 
 369           isSelectLabelFromUser = true;
 
 371           // Use placeholder attribute, otherwise fallback to the md-input-container label
 
 372           var tmpPlaceholder = attr.placeholder ||
 
 373               (containerCtrl && containerCtrl.label ? containerCtrl.label.text() : '');
 
 375           text = tmpPlaceholder || '';
 
 376           isSelectLabelFromUser = true;
 
 379         var target = valueEl.children().eq(0);
 
 381         if (attr.mdSelectedHtml) {
 
 382             // Using getTrustedHtml will run the content through $sanitize if it is not already
 
 383             // explicitly trusted. If the ngSanitize module is not loaded, this will
 
 384             // *correctly* throw an sce error.
 
 385             target.html($sce.getTrustedHtml(text));
 
 386         } else if (isSelectLabelFromUser) {
 
 389           // If we've reached this point, the text is not user-provided.
 
 394       mdSelectCtrl.setIsPlaceholder = function(isPlaceholder) {
 
 396           valueEl.addClass('md-select-placeholder');
 
 397           if (containerCtrl && containerCtrl.label) {
 
 398             containerCtrl.label.addClass('md-placeholder');
 
 401           valueEl.removeClass('md-select-placeholder');
 
 402           if (containerCtrl && containerCtrl.label) {
 
 403             containerCtrl.label.removeClass('md-placeholder');
 
 410           .on('focus', function(ev) {
 
 411             // Always focus the container (if we have one) so floating labels and other styles are
 
 413             containerCtrl && containerCtrl.setFocused(true);
 
 416         // Attach before ngModel's blur listener to stop propagation of blur event
 
 417         // to prevent from setting $touched.
 
 418         element.on('blur', function(event) {
 
 421             if (selectScope._mdSelectIsOpen) {
 
 422               event.stopImmediatePropagation();
 
 426           if (selectScope._mdSelectIsOpen) return;
 
 427           containerCtrl && containerCtrl.setFocused(false);
 
 432       mdSelectCtrl.triggerClose = function() {
 
 433         $parse(attr.mdOnClose)(scope);
 
 436       scope.$$postDigest(function() {
 
 442       function initAriaLabel() {
 
 443         var labelText = element.attr('aria-label') || element.attr('placeholder');
 
 444         if (!labelText && containerCtrl && containerCtrl.label) {
 
 445           labelText = containerCtrl.label.text();
 
 447         ariaLabelBase = labelText;
 
 448         $mdAria.expect(element, 'aria-label', labelText);
 
 451       scope.$watch(function() {
 
 452         return selectMenuCtrl.selectedLabels();
 
 455       function syncLabelText() {
 
 456         if (selectContainer) {
 
 457           selectMenuCtrl = selectMenuCtrl || selectContainer.find('md-select-menu').controller('mdSelectMenu');
 
 458           mdSelectCtrl.setLabelText(selectMenuCtrl.selectedLabels());
 
 462       function syncAriaLabel() {
 
 463         if (!ariaLabelBase) return;
 
 464         var ariaLabels = selectMenuCtrl.selectedLabels({mode: 'aria'});
 
 465         element.attr('aria-label', ariaLabels.length ? ariaLabelBase + ': ' + ariaLabels : ariaLabelBase);
 
 468       var deregisterWatcher;
 
 469       attr.$observe('ngMultiple', function(val) {
 
 470         if (deregisterWatcher) deregisterWatcher();
 
 471         var parser = $parse(val);
 
 472         deregisterWatcher = scope.$watch(function() {
 
 473           return parser(scope);
 
 474         }, function(multiple, prevVal) {
 
 475           if (multiple === undefined && prevVal === undefined) return; // assume compiler did a good job
 
 477             element.attr('multiple', 'multiple');
 
 479             element.removeAttr('multiple');
 
 481           element.attr('aria-multiselectable', multiple ? 'true' : 'false');
 
 482           if (selectContainer) {
 
 483             selectMenuCtrl.setMultiple(multiple);
 
 484             originalRender = ngModelCtrl.$render;
 
 485             ngModelCtrl.$render = function() {
 
 491             ngModelCtrl.$render();
 
 496       attr.$observe('disabled', function(disabled) {
 
 497         if (angular.isString(disabled)) {
 
 500         // Prevent click event being registered twice
 
 501         if (isDisabled !== undefined && isDisabled === disabled) {
 
 504         isDisabled = disabled;
 
 507             .attr({'aria-disabled': 'true'})
 
 508             .removeAttr('tabindex')
 
 509             .off('click', openSelect)
 
 510             .off('keydown', handleKeypress);
 
 513             .attr({'tabindex': attr.tabindex, 'aria-disabled': 'false'})
 
 514             .on('click', openSelect)
 
 515             .on('keydown', handleKeypress);
 
 519       if (!attr.hasOwnProperty('disabled') && !attr.hasOwnProperty('ngDisabled')) {
 
 520         element.attr({'aria-disabled': 'false'});
 
 521         element.on('click', openSelect);
 
 522         element.on('keydown', handleKeypress);
 
 527         'aria-expanded': 'false',
 
 528         'aria-multiselectable': isMultiple && !attr.ngMultiple ? 'true' : 'false'
 
 531       if (!element[0].hasAttribute('id')) {
 
 532         ariaAttrs.id = 'select_' + $mdUtil.nextUid();
 
 535       var containerId = 'select_container_' + $mdUtil.nextUid();
 
 536       selectContainer.attr('id', containerId);
 
 537       ariaAttrs['aria-owns'] = containerId;
 
 538       element.attr(ariaAttrs);
 
 540       scope.$on('$destroy', function() {
 
 543           .finally(function() {
 
 545               containerCtrl.setFocused(false);
 
 546               containerCtrl.setHasValue(false);
 
 547               containerCtrl.input = null;
 
 549             ngModelCtrl.$setTouched();
 
 555       function inputCheckValue() {
 
 556         // The select counts as having a value if one or more options are selected,
 
 557         // or if the input's validity state says it has bad input (eg string in a number input)
 
 558         containerCtrl && containerCtrl.setHasValue(selectMenuCtrl.selectedLabels().length > 0 || (element[0].validity || {}).badInput);
 
 561       function findSelectContainer() {
 
 562         selectContainer = angular.element(
 
 563           element[0].querySelector('.md-select-menu-container')
 
 566         if (attr.mdContainerClass) {
 
 567           var value = selectContainer[0].getAttribute('class') + ' ' + attr.mdContainerClass;
 
 568           selectContainer[0].setAttribute('class', value);
 
 570         selectMenuCtrl = selectContainer.find('md-select-menu').controller('mdSelectMenu');
 
 571         selectMenuCtrl.init(ngModelCtrl, attr.ngModel);
 
 572         element.on('$destroy', function() {
 
 573           selectContainer.remove();
 
 577       function handleKeypress(e) {
 
 578         if ($mdConstant.isNavigationKey(e)) {
 
 579           // prevent page scrolling on interaction
 
 583           if ($mdConstant.isInputKey(e) || $mdConstant.isNumPadKey(e)) {
 
 586             var node = selectMenuCtrl.optNodeForKeyboardSearch(e);
 
 587             if (!node || node.hasAttribute('disabled')) return;
 
 588             var optionCtrl = angular.element(node).controller('mdOption');
 
 589             if (!selectMenuCtrl.isMultiple) {
 
 590               selectMenuCtrl.deselect(Object.keys(selectMenuCtrl.selected)[0]);
 
 592             selectMenuCtrl.select(optionCtrl.hashKey, optionCtrl.value);
 
 593             selectMenuCtrl.refreshViewValue();
 
 598       function openSelect() {
 
 599         selectScope._mdSelectIsOpen = true;
 
 600         element.attr('aria-expanded', 'true');
 
 606           element: selectContainer,
 
 608           selectCtrl: mdSelectCtrl,
 
 609           preserveElement: true,
 
 611           loadingAsync: attr.mdOnOpen ? scope.$eval(attr.mdOnOpen) || true : false
 
 612         }).finally(function() {
 
 613           selectScope._mdSelectIsOpen = false;
 
 615           element.attr('aria-expanded', 'false');
 
 616           ngModelCtrl.$setTouched();
 
 624 function SelectMenuDirective($parse, $mdUtil, $mdConstant, $mdTheming) {
 
 625   // We want the scope to be set to 'false' so an isolated scope is not created
 
 626   // which would interfere with the md-select-header's access to the
 
 628   SelectMenuController['$inject'] = ["$scope", "$attrs", "$element"];
 
 631     require: ['mdSelectMenu'],
 
 633     controller: SelectMenuController,
 
 637   // We use preLink instead of postLink to ensure that the select is initialized before
 
 638   // its child options run postLink.
 
 639   function preLink(scope, element, attr, ctrls) {
 
 640     var selectCtrl = ctrls[0];
 
 642     element.addClass('_md');     // private md component indicator for styling
 
 645     element.on('click', clickListener);
 
 646     element.on('keypress', keyListener);
 
 648     function keyListener(e) {
 
 649       if (e.keyCode == 13 || e.keyCode == 32) {
 
 654     function clickListener(ev) {
 
 655       var option = $mdUtil.getClosest(ev.target, 'md-option');
 
 656       var optionCtrl = option && angular.element(option).data('$mdOptionController');
 
 657       if (!option || !optionCtrl) return;
 
 658       if (option.hasAttribute('disabled')) {
 
 659         ev.stopImmediatePropagation();
 
 663       var optionHashKey = selectCtrl.hashGetter(optionCtrl.value);
 
 664       var isSelected = angular.isDefined(selectCtrl.selected[optionHashKey]);
 
 666       scope.$apply(function() {
 
 667         if (selectCtrl.isMultiple) {
 
 669             selectCtrl.deselect(optionHashKey);
 
 671             selectCtrl.select(optionHashKey, optionCtrl.value);
 
 675             selectCtrl.deselect(Object.keys(selectCtrl.selected)[0]);
 
 676             selectCtrl.select(optionHashKey, optionCtrl.value);
 
 679         selectCtrl.refreshViewValue();
 
 684   function SelectMenuController($scope, $attrs, $element) {
 
 686     self.isMultiple = angular.isDefined($attrs.multiple);
 
 687     // selected is an object with keys matching all of the selected options' hashed values
 
 689     // options is an object with keys matching every option's hash value,
 
 690     // and values matching every option's controller.
 
 693     $scope.$watchCollection(function() {
 
 696       self.ngModel.$render();
 
 699     var deregisterCollectionWatch;
 
 701     self.setMultiple = function(isMultiple) {
 
 702       var ngModel = self.ngModel;
 
 703       defaultIsEmpty = defaultIsEmpty || ngModel.$isEmpty;
 
 705       self.isMultiple = isMultiple;
 
 706       if (deregisterCollectionWatch) deregisterCollectionWatch();
 
 708       if (self.isMultiple) {
 
 709         ngModel.$validators['md-multiple'] = validateArray;
 
 710         ngModel.$render = renderMultiple;
 
 712         // watchCollection on the model because by default ngModel only watches the model's
 
 713         // reference. This allowed the developer to also push and pop from their array.
 
 714         $scope.$watchCollection(self.modelBinding, function(value) {
 
 715           if (validateArray(value)) renderMultiple(value);
 
 716           self.ngModel.$setPristine();
 
 719         ngModel.$isEmpty = function(value) {
 
 720           return !value || value.length === 0;
 
 723         delete ngModel.$validators['md-multiple'];
 
 724         ngModel.$render = renderSingular;
 
 727       function validateArray(modelValue, viewValue) {
 
 728         // If a value is truthy but not an array, reject it.
 
 729         // If value is undefined/falsy, accept that it's an empty array.
 
 730         return angular.isArray(modelValue || viewValue || []);
 
 735     var clearSearchTimeout, optNodes, optText;
 
 736     var CLEAR_SEARCH_AFTER = 300;
 
 738     self.optNodeForKeyboardSearch = function(e) {
 
 739       clearSearchTimeout && clearTimeout(clearSearchTimeout);
 
 740       clearSearchTimeout = setTimeout(function() {
 
 741         clearSearchTimeout = undefined;
 
 744         optNodes = undefined;
 
 745       }, CLEAR_SEARCH_AFTER);
 
 747       // Support 1-9 on numpad
 
 748       var keyCode = e.keyCode - ($mdConstant.isNumPadKey(e) ? 48 : 0);
 
 750       searchStr += String.fromCharCode(keyCode);
 
 751       var search = new RegExp('^' + searchStr, 'i');
 
 753         optNodes = $element.find('md-option');
 
 754         optText = new Array(optNodes.length);
 
 755         angular.forEach(optNodes, function(el, i) {
 
 756           optText[i] = el.textContent.trim();
 
 759       for (var i = 0; i < optText.length; ++i) {
 
 760         if (search.test(optText[i])) {
 
 766     self.init = function(ngModel, binding) {
 
 767       self.ngModel = ngModel;
 
 768       self.modelBinding = binding;
 
 770       // Setup a more robust version of isEmpty to ensure value is a valid option
 
 771       self.ngModel.$isEmpty = function($viewValue) {
 
 772         // We have to transform the viewValue into the hashKey, because otherwise the
 
 773         // OptionCtrl may not exist. Developers may have specified a trackBy function.
 
 774         return !self.options[self.hashGetter($viewValue)];
 
 777       // Allow users to provide `ng-model="foo" ng-model-options="{trackBy: 'foo.id'}"` so
 
 778       // that we can properly compare objects set on the model to the available options
 
 779       var trackByOption = $mdUtil.getModelOption(ngModel, 'trackBy');
 
 782         var trackByLocals = {};
 
 783         var trackByParsed = $parse(trackByOption);
 
 784         self.hashGetter = function(value, valueScope) {
 
 785           trackByLocals.$value = value;
 
 786           return trackByParsed(valueScope || $scope, trackByLocals);
 
 788         // If the user doesn't provide a trackBy, we automatically generate an id for every
 
 791         self.hashGetter = function getHashValue(value) {
 
 792           if (angular.isObject(value)) {
 
 793             return 'object_' + (value.$$mdSelectId || (value.$$mdSelectId = ++selectNextId));
 
 798       self.setMultiple(self.isMultiple);
 
 801     self.selectedLabels = function(opts) {
 
 803       var mode = opts.mode || 'html';
 
 804       var selectedOptionEls = $mdUtil.nodesToArray($element[0].querySelectorAll('md-option[selected]'));
 
 805       if (selectedOptionEls.length) {
 
 808         if (mode == 'html') {
 
 809           // Map the given element to its innerHTML string. If the element has a child ripple
 
 810           // container remove it from the HTML string, before returning the string.
 
 811           mapFn = function(el) {
 
 812             // If we do not have a `value` or `ng-value`, assume it is an empty option which clears the select
 
 813             if (el.hasAttribute('md-option-empty')) {
 
 817             var html = el.innerHTML;
 
 819             // Remove the ripple container from the selected option, copying it would cause a CSP violation.
 
 820             var rippleContainer = el.querySelector('.md-ripple-container');
 
 821             if (rippleContainer) {
 
 822               html = html.replace(rippleContainer.outerHTML, '');
 
 825             // Remove the checkbox container, because it will cause the label to wrap inside of the placeholder.
 
 826             // It should be not displayed inside of the label element.
 
 827             var checkboxContainer = el.querySelector('.md-container');
 
 828             if (checkboxContainer) {
 
 829               html = html.replace(checkboxContainer.outerHTML, '');
 
 834         } else if (mode == 'aria') {
 
 835           mapFn = function(el) { return el.hasAttribute('aria-label') ? el.getAttribute('aria-label') : el.textContent; };
 
 838         // Ensure there are no duplicates; see https://github.com/angular/material/issues/9442
 
 839         return $mdUtil.uniq(selectedOptionEls.map(mapFn)).join(', ');
 
 845     self.select = function(hashKey, hashedValue) {
 
 846       var option = self.options[hashKey];
 
 847       option && option.setSelected(true);
 
 848       self.selected[hashKey] = hashedValue;
 
 850     self.deselect = function(hashKey) {
 
 851       var option = self.options[hashKey];
 
 852       option && option.setSelected(false);
 
 853       delete self.selected[hashKey];
 
 856     self.addOption = function(hashKey, optionCtrl) {
 
 857       if (angular.isDefined(self.options[hashKey])) {
 
 858         throw new Error('Duplicate md-option values are not allowed in a select. ' +
 
 859           'Duplicate value "' + optionCtrl.value + '" found.');
 
 862       self.options[hashKey] = optionCtrl;
 
 864       // If this option's value was already in our ngModel, go ahead and select it.
 
 865       if (angular.isDefined(self.selected[hashKey])) {
 
 866         self.select(hashKey, optionCtrl.value);
 
 868         // When the current $modelValue of the ngModel Controller is using the same hash as
 
 869         // the current option, which will be added, then we can be sure, that the validation
 
 870         // of the option has occurred before the option was added properly.
 
 871         // This means, that we have to manually trigger a new validation of the current option.
 
 872         if (angular.isDefined(self.ngModel.$modelValue) && self.hashGetter(self.ngModel.$modelValue) === hashKey) {
 
 873           self.ngModel.$validate();
 
 876         self.refreshViewValue();
 
 879     self.removeOption = function(hashKey) {
 
 880       delete self.options[hashKey];
 
 881       // Don't deselect an option when it's removed - the user's ngModel should be allowed
 
 882       // to have values that do not match a currently available option.
 
 885     self.refreshViewValue = function() {
 
 888       for (var hashKey in self.selected) {
 
 889         // If this hashKey has an associated option, push that option's value to the model.
 
 890         if ((option = self.options[hashKey])) {
 
 891           values.push(option.value);
 
 893           // Otherwise, the given hashKey has no associated option, and we got it
 
 894           // from an ngModel value at an earlier time. Push the unhashed value of
 
 895           // this hashKey to the model.
 
 896           // This allows the developer to put a value in the model that doesn't yet have
 
 897           // an associated option.
 
 898           values.push(self.selected[hashKey]);
 
 901       var usingTrackBy = $mdUtil.getModelOption(self.ngModel, 'trackBy');
 
 903       var newVal = self.isMultiple ? values : values[0];
 
 904       var prevVal = self.ngModel.$modelValue;
 
 906       if (usingTrackBy ? !angular.equals(prevVal, newVal) : (prevVal + '') !== newVal) {
 
 907         self.ngModel.$setViewValue(newVal);
 
 908         self.ngModel.$render();
 
 912     function renderMultiple() {
 
 913       var newSelectedValues = self.ngModel.$modelValue || self.ngModel.$viewValue || [];
 
 914       if (!angular.isArray(newSelectedValues)) return;
 
 916       var oldSelected = Object.keys(self.selected);
 
 918       var newSelectedHashes = newSelectedValues.map(self.hashGetter);
 
 919       var deselected = oldSelected.filter(function(hash) {
 
 920         return newSelectedHashes.indexOf(hash) === -1;
 
 923       deselected.forEach(self.deselect);
 
 924       newSelectedHashes.forEach(function(hashKey, i) {
 
 925         self.select(hashKey, newSelectedValues[i]);
 
 929     function renderSingular() {
 
 930       var value = self.ngModel.$viewValue || self.ngModel.$modelValue;
 
 931       Object.keys(self.selected).forEach(self.deselect);
 
 932       self.select(self.hashGetter(value), value);
 
 938 function OptionDirective($mdButtonInkRipple, $mdUtil) {
 
 940   OptionController['$inject'] = ["$element"];
 
 943     require: ['mdOption', '^^mdSelectMenu'],
 
 944     controller: OptionController,
 
 948   function compile(element, attr) {
 
 949     // Manual transclusion to avoid the extra inner <span> that ng-transclude generates
 
 950     element.append(angular.element('<div class="md-text">').append(element.contents()));
 
 952     element.attr('tabindex', attr.tabindex || '0');
 
 954     if (!hasDefinedValue(attr)) {
 
 955       element.attr('md-option-empty', '');
 
 961   function hasDefinedValue(attr) {
 
 962     var value = attr.value;
 
 963     var ngValue = attr.ngValue;
 
 965     return value || ngValue;
 
 968   function postLink(scope, element, attr, ctrls) {
 
 969     var optionCtrl = ctrls[0];
 
 970     var selectCtrl = ctrls[1];
 
 972     if (selectCtrl.isMultiple) {
 
 973       element.addClass('md-checkbox-enabled');
 
 974       element.prepend(CHECKBOX_SELECTION_INDICATOR.clone());
 
 977     if (angular.isDefined(attr.ngValue)) {
 
 978       scope.$watch(attr.ngValue, setOptionValue);
 
 979     } else if (angular.isDefined(attr.value)) {
 
 980       setOptionValue(attr.value);
 
 982       scope.$watch(function() {
 
 983         return element.text().trim();
 
 987     attr.$observe('disabled', function(disabled) {
 
 989         element.attr('tabindex', '-1');
 
 991         element.attr('tabindex', '0');
 
 995     scope.$$postDigest(function() {
 
 996       attr.$observe('selected', function(selected) {
 
 997         if (!angular.isDefined(selected)) return;
 
 998         if (typeof selected == 'string') selected = true;
 
1000           if (!selectCtrl.isMultiple) {
 
1001             selectCtrl.deselect(Object.keys(selectCtrl.selected)[0]);
 
1003           selectCtrl.select(optionCtrl.hashKey, optionCtrl.value);
 
1005           selectCtrl.deselect(optionCtrl.hashKey);
 
1007         selectCtrl.refreshViewValue();
 
1011     $mdButtonInkRipple.attach(scope, element);
 
1014     function setOptionValue(newValue, oldValue, prevAttempt) {
 
1015       if (!selectCtrl.hashGetter) {
 
1017           scope.$$postDigest(function() {
 
1018             setOptionValue(newValue, oldValue, true);
 
1023       var oldHashKey = selectCtrl.hashGetter(oldValue, scope);
 
1024       var newHashKey = selectCtrl.hashGetter(newValue, scope);
 
1026       optionCtrl.hashKey = newHashKey;
 
1027       optionCtrl.value = newValue;
 
1029       selectCtrl.removeOption(oldHashKey, optionCtrl);
 
1030       selectCtrl.addOption(newHashKey, optionCtrl);
 
1033     scope.$on('$destroy', function() {
 
1034       selectCtrl.removeOption(optionCtrl.hashKey, optionCtrl);
 
1037     function configureAria() {
 
1040         'aria-selected': 'false'
 
1043       if (!element[0].hasAttribute('id')) {
 
1044         ariaAttrs.id = 'select_option_' + $mdUtil.nextUid();
 
1046       element.attr(ariaAttrs);
 
1050   function OptionController($element) {
 
1051     this.selected = false;
 
1052     this.setSelected = function(isSelected) {
 
1053       if (isSelected && !this.selected) {
 
1055           'selected': 'selected',
 
1056           'aria-selected': 'true'
 
1058       } else if (!isSelected && this.selected) {
 
1059         $element.removeAttr('selected');
 
1060         $element.attr('aria-selected', 'false');
 
1062       this.selected = isSelected;
 
1068 function OptgroupDirective() {
 
1073   function compile(el, attrs) {
 
1074     // If we have a select header element, we don't want to add the normal label
 
1076     if (!hasSelectHeader()) {
 
1077       setupLabelElement();
 
1080     function hasSelectHeader() {
 
1081       return el.parent().find('md-select-header').length;
 
1084     function setupLabelElement() {
 
1085       var labelElement = el.find('label');
 
1086       if (!labelElement.length) {
 
1087         labelElement = angular.element('<label>');
 
1088         el.prepend(labelElement);
 
1090       labelElement.addClass('md-container-ignore');
 
1091       if (attrs.label) labelElement.text(attrs.label);
 
1096 function SelectHeaderDirective() {
 
1102 function SelectProvider($$interimElementProvider) {
 
1103   selectDefaultOptions['$inject'] = ["$mdSelect", "$mdConstant", "$mdUtil", "$window", "$q", "$$rAF", "$animateCss", "$animate", "$document"];
 
1104   return $$interimElementProvider('$mdSelect')
 
1106       methods: ['target'],
 
1107       options: selectDefaultOptions
 
1111   function selectDefaultOptions($mdSelect, $mdConstant, $mdUtil, $window, $q, $$rAF, $animateCss, $animate, $document) {
 
1112     var ERROR_TARGET_EXPECTED = "$mdSelect.show() expected a target element in options.target but got '{0}'!";
 
1113     var animator = $mdUtil.dom.animator;
 
1114     var keyCodes = $mdConstant.KEY_CODE;
 
1122       disableParentScroll: true
 
1126      * Interim-element onRemove logic....
 
1128     function onRemove(scope, element, opts) {
 
1130       opts.cleanupInteraction();
 
1131       opts.cleanupResizing();
 
1132       opts.hideBackdrop();
 
1134       // For navigation $destroy events, do a quick, non-animated removal,
 
1135       // but for normal closes (from clicks, etc) animate the removal
 
1137       return  (opts.$destroy === true) ? cleanElement() : animateRemoval().then( cleanElement );
 
1140        * For normal closes (eg clicks), animate the removal.
 
1141        * For forced closes (like $destroy events from navigation),
 
1142        * skip the animations
 
1144       function animateRemoval() {
 
1145         return $animateCss(element, {addClass: 'md-leave'}).start();
 
1149        * Restore the element to a closed state
 
1151       function cleanElement() {
 
1153         element.removeClass('md-active');
 
1154         element.attr('aria-hidden', 'true');
 
1155         element[0].style.display = 'none';
 
1157         announceClosed(opts);
 
1159         if (!opts.$destroy && opts.restoreFocus) {
 
1160           opts.target.focus();
 
1167      * Interim-element onShow logic....
 
1169     function onShow(scope, element, opts) {
 
1172       sanitizeAndConfigure(scope, opts);
 
1174       opts.hideBackdrop = showBackdrop(scope, element, opts);
 
1176       return showDropDown(scope, element, opts)
 
1177         .then(function(response) {
 
1178           element.attr('aria-hidden', 'false');
 
1179           opts.alreadyOpen = true;
 
1180           opts.cleanupInteraction = activateInteraction();
 
1181           opts.cleanupResizing = activateResizing();
 
1184         }, opts.hideBackdrop);
 
1186       // ************************************
 
1187       // Closure Functions
 
1188       // ************************************
 
1191        *  Attach the select DOM element(s) and animate to the correct positions
 
1194       function showDropDown(scope, element, opts) {
 
1195         opts.parent.append(element);
 
1197         return $q(function(resolve, reject) {
 
1201             $animateCss(element, {removeClass: 'md-leave', duration: 0})
 
1203               .then(positionAndFocusMenu)
 
1214        * Initialize container and dropDown menu positions/scale, then animate
 
1215        * to show... and autoFocus.
 
1217       function positionAndFocusMenu() {
 
1218         return $q(function(resolve) {
 
1219           if (opts.isRemoved) return $q.reject(false);
 
1221           var info = calculateMenuPositions(scope, element, opts);
 
1223           info.container.element.css(animator.toCss(info.container.styles));
 
1224           info.dropDown.element.css(animator.toCss(info.dropDown.styles));
 
1227             element.addClass('md-active');
 
1228             info.dropDown.element.css(animator.toCss({transform: ''}));
 
1230             autoFocus(opts.focusedNode);
 
1238        * Show modal backdrop element...
 
1240       function showBackdrop(scope, element, options) {
 
1242         // If we are not within a dialog...
 
1243         if (options.disableParentScroll && !$mdUtil.getClosest(options.target, 'MD-DIALOG')) {
 
1244           // !! DO this before creating the backdrop; since disableScrollAround()
 
1245           //    configures the scroll offset; which is used by mdBackDrop postLink()
 
1246           options.restoreScroll = $mdUtil.disableScrollAround(options.element, options.parent);
 
1248           options.disableParentScroll = false;
 
1251         if (options.hasBackdrop) {
 
1252           // Override duration to immediately show invisible backdrop
 
1253           options.backdrop = $mdUtil.createBackdrop(scope, "md-select-backdrop md-click-catcher");
 
1254           $animate.enter(options.backdrop, $document[0].body, null, {duration: 0});
 
1258          * Hide modal backdrop element...
 
1260         return function hideBackdrop() {
 
1261           if (options.backdrop) options.backdrop.remove();
 
1262           if (options.disableParentScroll) options.restoreScroll();
 
1264           delete options.restoreScroll;
 
1271       function autoFocus(focusedNode) {
 
1272         if (focusedNode && !focusedNode.hasAttribute('disabled')) {
 
1273           focusedNode.focus();
 
1278        * Check for valid opts and set some sane defaults
 
1280       function sanitizeAndConfigure(scope, options) {
 
1281         var selectEl = element.find('md-select-menu');
 
1283         if (!options.target) {
 
1284           throw new Error($mdUtil.supplant(ERROR_TARGET_EXPECTED, [options.target]));
 
1287         angular.extend(options, {
 
1289           target: angular.element(options.target), //make sure it's not a naked dom node
 
1290           parent: angular.element(options.parent),
 
1292           contentEl: element.find('md-content'),
 
1293           optionNodes: selectEl[0].getElementsByTagName('md-option')
 
1298        * Configure various resize listeners for screen changes
 
1300       function activateResizing() {
 
1301         var debouncedOnResize = (function(scope, target, options) {
 
1304             if (options.isRemoved) return;
 
1306             var updates = calculateMenuPositions(scope, target, options);
 
1307             var container = updates.container;
 
1308             var dropDown = updates.dropDown;
 
1310             container.element.css(animator.toCss(container.styles));
 
1311             dropDown.element.css(animator.toCss(dropDown.styles));
 
1314         })(scope, element, opts);
 
1316         var window = angular.element($window);
 
1317         window.on('resize', debouncedOnResize);
 
1318         window.on('orientationchange', debouncedOnResize);
 
1320         // Publish deactivation closure...
 
1321         return function deactivateResizing() {
 
1323           // Disable resizing handlers
 
1324           window.off('resize', debouncedOnResize);
 
1325           window.off('orientationchange', debouncedOnResize);
 
1330        *  If asynchronously loading, watch and update internal
 
1331        *  '$$loadingAsyncDone' flag
 
1333       function watchAsyncLoad() {
 
1334         if (opts.loadingAsync && !opts.isRemoved) {
 
1335           scope.$$loadingAsyncDone = false;
 
1337           $q.when(opts.loadingAsync)
 
1339               scope.$$loadingAsyncDone = true;
 
1340               delete opts.loadingAsync;
 
1341             }).then(function() {
 
1342               $$rAF(positionAndFocusMenu);
 
1350       function activateInteraction() {
 
1351         if (opts.isRemoved) return;
 
1353         var dropDown = opts.selectEl;
 
1354         var selectCtrl = dropDown.controller('mdSelectMenu') || {};
 
1356         element.addClass('md-clickable');
 
1358         // Close on backdrop click
 
1359         opts.backdrop && opts.backdrop.on('click', onBackdropClick);
 
1362         // Cycling of options, and closing on enter
 
1363         dropDown.on('keydown', onMenuKeyDown);
 
1364         dropDown.on('click', checkCloseMenu);
 
1366         return function cleanupInteraction() {
 
1367           opts.backdrop && opts.backdrop.off('click', onBackdropClick);
 
1368           dropDown.off('keydown', onMenuKeyDown);
 
1369           dropDown.off('click', checkCloseMenu);
 
1371           element.removeClass('md-clickable');
 
1372           opts.isRemoved = true;
 
1375         // ************************************
 
1376         // Closure Functions
 
1377         // ************************************
 
1379         function onBackdropClick(e) {
 
1381           e.stopPropagation();
 
1382           opts.restoreFocus = false;
 
1383           $mdUtil.nextTick($mdSelect.hide, true);
 
1386         function onMenuKeyDown(ev) {
 
1387           ev.preventDefault();
 
1388           ev.stopPropagation();
 
1390           switch (ev.keyCode) {
 
1391             case keyCodes.UP_ARROW:
 
1392               return focusPrevOption();
 
1393             case keyCodes.DOWN_ARROW:
 
1394               return focusNextOption();
 
1395             case keyCodes.SPACE:
 
1396             case keyCodes.ENTER:
 
1397               var option = $mdUtil.getClosest(ev.target, 'md-option');
 
1399                 dropDown.triggerHandler({
 
1403                 ev.preventDefault();
 
1408             case keyCodes.ESCAPE:
 
1409               ev.stopPropagation();
 
1410               ev.preventDefault();
 
1411               opts.restoreFocus = true;
 
1412               $mdUtil.nextTick($mdSelect.hide, true);
 
1415               if ($mdConstant.isInputKey(ev) || $mdConstant.isNumPadKey(ev)) {
 
1416                 var optNode = dropDown.controller('mdSelectMenu').optNodeForKeyboardSearch(ev);
 
1417                 opts.focusedNode = optNode || opts.focusedNode;
 
1418                 optNode && optNode.focus();
 
1423         function focusOption(direction) {
 
1424           var optionsArray = $mdUtil.nodesToArray(opts.optionNodes);
 
1425           var index = optionsArray.indexOf(opts.focusedNode);
 
1431               // We lost the previously focused element, reset to first option
 
1433             } else if (direction === 'next' && index < optionsArray.length - 1) {
 
1435             } else if (direction === 'prev' && index > 0) {
 
1438             newOption = optionsArray[index];
 
1439             if (newOption.hasAttribute('disabled')) newOption = undefined;
 
1440           } while (!newOption && index < optionsArray.length - 1 && index > 0);
 
1442           newOption && newOption.focus();
 
1443           opts.focusedNode = newOption;
 
1446         function focusNextOption() {
 
1447           focusOption('next');
 
1450         function focusPrevOption() {
 
1451           focusOption('prev');
 
1454         function checkCloseMenu(ev) {
 
1455           if (ev && ( ev.type == 'click') && (ev.currentTarget != dropDown[0])) return;
 
1456           if ( mouseOnScrollbar() ) return;
 
1458           var option = $mdUtil.getClosest(ev.target, 'md-option');
 
1459           if (option && option.hasAttribute && !option.hasAttribute('disabled')) {
 
1460             ev.preventDefault();
 
1461             ev.stopPropagation();
 
1462             if (!selectCtrl.isMultiple) {
 
1463               opts.restoreFocus = true;
 
1465               $mdUtil.nextTick(function () {
 
1466                 $mdSelect.hide(selectCtrl.ngModel.$viewValue);
 
1471            * check if the mouseup event was on a scrollbar
 
1473           function mouseOnScrollbar() {
 
1474             var clickOnScrollbar = false;
 
1475             if (ev && (ev.currentTarget.children.length > 0)) {
 
1476               var child = ev.currentTarget.children[0];
 
1477               var hasScrollbar = child.scrollHeight > child.clientHeight;
 
1478               if (hasScrollbar && child.children.length > 0) {
 
1479                 var relPosX = ev.pageX - ev.currentTarget.getBoundingClientRect().left;
 
1480                 if (relPosX > child.querySelector('md-option').offsetWidth)
 
1481                   clickOnScrollbar = true;
 
1484             return clickOnScrollbar;
 
1492      * To notify listeners that the Select menu has closed,
 
1493      * trigger the [optional] user-defined expression
 
1495     function announceClosed(opts) {
 
1496       var mdSelect = opts.selectCtrl;
 
1498         var menuController = opts.selectEl.controller('mdSelectMenu');
 
1499         mdSelect.setLabelText(menuController ? menuController.selectedLabels() : '');
 
1500         mdSelect.triggerClose();
 
1508     function calculateMenuPositions(scope, element, opts) {
 
1510         containerNode = element[0],
 
1511         targetNode = opts.target[0].children[0], // target the label
 
1512         parentNode = $document[0].body,
 
1513         selectNode = opts.selectEl[0],
 
1514         contentNode = opts.contentEl[0],
 
1515         parentRect = parentNode.getBoundingClientRect(),
 
1516         targetRect = targetNode.getBoundingClientRect(),
 
1517         shouldOpenAroundTarget = false,
 
1519           left: parentRect.left + SELECT_EDGE_MARGIN,
 
1520           top: SELECT_EDGE_MARGIN,
 
1521           bottom: parentRect.height - SELECT_EDGE_MARGIN,
 
1522           right: parentRect.width - SELECT_EDGE_MARGIN - ($mdUtil.floatingScrollbars() ? 16 : 0)
 
1525           top: targetRect.top - bounds.top,
 
1526           left: targetRect.left - bounds.left,
 
1527           right: bounds.right - (targetRect.left + targetRect.width),
 
1528           bottom: bounds.bottom - (targetRect.top + targetRect.height)
 
1530         maxWidth = parentRect.width - SELECT_EDGE_MARGIN * 2,
 
1531         selectedNode = selectNode.querySelector('md-option[selected]'),
 
1532         optionNodes = selectNode.getElementsByTagName('md-option'),
 
1533         optgroupNodes = selectNode.getElementsByTagName('md-optgroup'),
 
1534         isScrollable = calculateScrollable(element, contentNode),
 
1537       var loading = isPromiseLike(opts.loadingAsync);
 
1539         // If a selected node, center around that
 
1541           centeredNode = selectedNode;
 
1542           // If there are option groups, center around the first option group
 
1543         } else if (optgroupNodes.length) {
 
1544           centeredNode = optgroupNodes[0];
 
1545           // Otherwise - if we are not loading async - center around the first optionNode
 
1546         } else if (optionNodes.length) {
 
1547           centeredNode = optionNodes[0];
 
1548           // In case there are no options, center on whatever's in there... (eg progress indicator)
 
1550           centeredNode = contentNode.firstElementChild || contentNode;
 
1553         // If loading, center on progress indicator
 
1554         centeredNode = contentNode.firstElementChild || contentNode;
 
1557       if (contentNode.offsetWidth > maxWidth) {
 
1558         contentNode.style['max-width'] = maxWidth + 'px';
 
1560         contentNode.style.maxWidth = null;
 
1562       if (shouldOpenAroundTarget) {
 
1563         contentNode.style['min-width'] = targetRect.width + 'px';
 
1566       // Remove padding before we compute the position of the menu
 
1568         selectNode.classList.add('md-overflow');
 
1571       var focusedNode = centeredNode;
 
1572       if ((focusedNode.tagName || '').toUpperCase() === 'MD-OPTGROUP') {
 
1573         focusedNode = optionNodes[0] || contentNode.firstElementChild || contentNode;
 
1574         centeredNode = focusedNode;
 
1576       // Cache for autoFocus()
 
1577       opts.focusedNode = focusedNode;
 
1579       // Get the selectMenuRect *after* max-width is possibly set above
 
1580       containerNode.style.display = 'block';
 
1581       var selectMenuRect = selectNode.getBoundingClientRect();
 
1582       var centeredRect = getOffsetRect(centeredNode);
 
1585         var centeredStyle = $window.getComputedStyle(centeredNode);
 
1586         centeredRect.paddingLeft = parseInt(centeredStyle.paddingLeft, 10) || 0;
 
1587         centeredRect.paddingRight = parseInt(centeredStyle.paddingRight, 10) || 0;
 
1591         var scrollBuffer = contentNode.offsetHeight / 2;
 
1592         contentNode.scrollTop = centeredRect.top + centeredRect.height / 2 - scrollBuffer;
 
1594         if (spaceAvailable.top < scrollBuffer) {
 
1595           contentNode.scrollTop = Math.min(
 
1597             contentNode.scrollTop + scrollBuffer - spaceAvailable.top
 
1599         } else if (spaceAvailable.bottom < scrollBuffer) {
 
1600           contentNode.scrollTop = Math.max(
 
1601             centeredRect.top + centeredRect.height - selectMenuRect.height,
 
1602             contentNode.scrollTop - scrollBuffer + spaceAvailable.bottom
 
1607       var left, top, transformOrigin, minWidth, fontSize;
 
1608       if (shouldOpenAroundTarget) {
 
1609         left = targetRect.left;
 
1610         top = targetRect.top + targetRect.height;
 
1611         transformOrigin = '50% 0';
 
1612         if (top + selectMenuRect.height > bounds.bottom) {
 
1613           top = targetRect.top - selectMenuRect.height;
 
1614           transformOrigin = '50% 100%';
 
1617         left = (targetRect.left + centeredRect.left - centeredRect.paddingLeft) + 2;
 
1618         top = Math.floor(targetRect.top + targetRect.height / 2 - centeredRect.height / 2 -
 
1619             centeredRect.top + contentNode.scrollTop) + 2;
 
1621         transformOrigin = (centeredRect.left + targetRect.width / 2) + 'px ' +
 
1622           (centeredRect.top + centeredRect.height / 2 - contentNode.scrollTop) + 'px 0px';
 
1624         minWidth = Math.min(targetRect.width + centeredRect.paddingLeft + centeredRect.paddingRight, maxWidth);
 
1626         fontSize = window.getComputedStyle(targetNode)['font-size'];
 
1629       // Keep left and top within the window
 
1630       var containerRect = containerNode.getBoundingClientRect();
 
1631       var scaleX = Math.round(100 * Math.min(targetRect.width / selectMenuRect.width, 1.0)) / 100;
 
1632       var scaleY = Math.round(100 * Math.min(targetRect.height / selectMenuRect.height, 1.0)) / 100;
 
1636           element: angular.element(containerNode),
 
1638             left: Math.floor(clamp(bounds.left, left, bounds.right - containerRect.width)),
 
1639             top: Math.floor(clamp(bounds.top, top, bounds.bottom - containerRect.height)),
 
1640             'min-width': minWidth,
 
1641             'font-size': fontSize
 
1645           element: angular.element(selectNode),
 
1647             transformOrigin: transformOrigin,
 
1648             transform: !opts.alreadyOpen ? $mdUtil.supplant('scale({0},{1})', [scaleX, scaleY]) : ""
 
1657   function isPromiseLike(obj) {
 
1658     return obj && angular.isFunction(obj.then);
 
1661   function clamp(min, n, max) {
 
1662     return Math.max(min, Math.min(n, max));
 
1665   function getOffsetRect(node) {
 
1667       left: node.offsetLeft,
 
1668       top: node.offsetTop,
 
1669       width: node.offsetWidth,
 
1670       height: node.offsetHeight
 
1671     } : {left: 0, top: 0, width: 0, height: 0};
 
1674   function calculateScrollable(element, contentNode) {
 
1675     var isScrollable = false;
 
1678       var oldDisplay = element[0].style.display;
 
1680       // Set the element's display to block so that this calculation is correct
 
1681       element[0].style.display = 'block';
 
1683       isScrollable = contentNode.scrollHeight > contentNode.offsetHeight;
 
1685       // Reset it back afterwards
 
1686       element[0].style.display = oldDisplay;
 
1690     return isScrollable;
 
1694 ngmaterial.components.select = angular.module("material.components.select");