arnaucube
arnaucube/socketioMEANseedPublic Self-hosted repository
socketioMEANseed/www/bower_components/angular-material/modules/js/select/select.js
www/bower_components/angular-material/modules/js/select/select.jsRaw
   1/*!
   2 * Angular Material Design
   3 * https://github.com/angular/material
   4 * @license MIT
   5 * v1.1.3
   6 */
   7(function( window, angular, undefined ){
   8"use strict";
   9
  10/**
  11 * @ngdoc module
  12 * @name material.components.select
  13 */
  14
  15/***************************************************
  16
  17 ### TODO - POST RC1 ###
  18 - [ ] Abstract placement logic in $mdSelect service to $mdMenu service
  19
  20 ***************************************************/
  21
  22SelectDirective['$inject'] = ["$mdSelect", "$mdUtil", "$mdConstant", "$mdTheming", "$mdAria", "$parse", "$sce", "$injector"];
  23SelectMenuDirective['$inject'] = ["$parse", "$mdUtil", "$mdConstant", "$mdTheming"];
  24OptionDirective['$inject'] = ["$mdButtonInkRipple", "$mdUtil"];
  25SelectProvider['$inject'] = ["$$interimElementProvider"];
  26var SELECT_EDGE_MARGIN = 8;
  27var selectNextId = 0;
  28var CHECKBOX_SELECTION_INDICATOR =
  29  angular.element('<div class="md-container"><div class="md-icon"></div></div>');
  30
  31angular.module('material.components.select', [
  32    'material.core',
  33    'material.components.backdrop'
  34  ])
  35  .directive('mdSelect', SelectDirective)
  36  .directive('mdSelectMenu', SelectMenuDirective)
  37  .directive('mdOption', OptionDirective)
  38  .directive('mdOptgroup', OptgroupDirective)
  39  .directive('mdSelectHeader', SelectHeaderDirective)
  40  .provider('$mdSelect', SelectProvider);
  41
  42/**
  43 * @ngdoc directive
  44 * @name mdSelect
  45 * @restrict E
  46 * @module material.components.select
  47 *
  48 * @description Displays a select box, bound to an ng-model.
  49 *
  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.
  52 *
  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.
  55 *
  56 * ### Option Params
  57 *
  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
  61 * defined.
  62 *
  63 * **Automatically Applied**
  64 *
  65 *  - `<md-option>`
  66 *  - `<md-option value>`
  67 *  - `<md-option value="">`
  68 *  - `<md-option ng-value>`
  69 *  - `<md-option ng-value="">`
  70 *
  71 * **NOT Automatically Applied**
  72 *
  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>&lt;md-option ng-value="{{someValueThatMightBeUndefined}}"&gt;</code>
  78 *
  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.
  81 *
  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"`.
  84 *
  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).
 104 *
 105 * @usage
 106 * With a placeholder (label and aria-label are added dynamically)
 107 * <hljs lang="html">
 108 *   <md-input-container>
 109 *     <md-select
 110 *       ng-model="someModel"
 111 *       placeholder="Select a state">
 112 *       <md-option ng-value="opt" ng-repeat="opt in neighborhoods2">{{ opt }}</md-option>
 113 *     </md-select>
 114 *   </md-input-container>
 115 * </hljs>
 116 *
 117 * With an explicit label
 118 * <hljs lang="html">
 119 *   <md-input-container>
 120 *     <label>State</label>
 121 *     <md-select
 122 *       ng-model="someModel">
 123 *       <md-option ng-value="opt" ng-repeat="opt in neighborhoods2">{{ opt }}</md-option>
 124 *     </md-select>
 125 *   </md-input-container>
 126 * </hljs>
 127 *
 128 * With a select-header
 129 *
 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.
 134 *
 135 * When using the md-select-header the labels that would previously be added to the
 136 * OptGroupDirective are ignored.
 137 *
 138 * <hljs lang="html">
 139 *   <md-input-container>
 140 *     <md-select ng-model="someModel">
 141 *       <md-select-header>
 142 *         <span> Neighborhoods - </span>
 143 *       </md-select-header>
 144 *       <md-option ng-value="opt" ng-repeat="opt in neighborhoods2">{{ opt }}</md-option>
 145 *     </md-select>
 146 *   </md-input-container>
 147 * </hljs>
 148 *
 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:
 152 * <hljs lang="js">
 153 * angular.controller('MyCtrl', function($scope) {
 154 *   $scope.users = [
 155 *     { id: 1, name: 'Bob' },
 156 *     { id: 2, name: 'Alice' },
 157 *     { id: 3, name: 'Steve' }
 158 *   ];
 159 *   $scope.selectedUser = { id: 1, name: 'Bob' };
 160 * });
 161 * </hljs>
 162 * <hljs lang="html">
 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>
 166 *   </md-select>
 167 * </div>
 168 * </hljs>
 169 *
 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);`;
 173 *
 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.
 178 *
 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.
 185 *
 186 * Working HTML:
 187 * <hljs lang="html">
 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>
 191 *   </md-select>
 192 * </div>
 193 * </hljs>
 194 */
 195function SelectDirective($mdSelect, $mdUtil, $mdConstant, $mdTheming, $mdAria, $parse, $sce,
 196    $injector) {
 197  var keyCodes = $mdConstant.KEY_CODE;
 198  var NAVIGATION_KEYS = [keyCodes.SPACE, keyCodes.ENTER, keyCodes.UP_ARROW, keyCodes.DOWN_ARROW];
 199
 200  return {
 201    restrict: 'E',
 202    require: ['^?mdInputContainer', 'mdSelect', 'ngModel', '?^form'],
 203    compile: compile,
 204    controller: function() {
 205    } // empty placeholder controller to be initialized in link
 206  };
 207
 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());
 215    }
 216
 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()));
 220    }
 221
 222
 223    // Add progress spinner for md-options-loading
 224    if (attr.mdOnOpen) {
 225
 226      // Show progress indicator while loading async
 227      // Use ng-hide for `display:none` so the indicator does not interfere with the options list
 228      element
 229        .find('md-content')
 230        .prepend(angular.element(
 231          '<div>' +
 232          ' <md-progress-circular md-mode="indeterminate" ng-if="$$loadingAsyncDone === false" md-diameter="25px"></md-progress-circular>' +
 233          '</div>'
 234        ));
 235
 236      // Hide list [of item options] while loading async
 237      element
 238        .find('md-option')
 239        .attr('ng-show', '$$loadingAsyncDone');
 240    }
 241
 242    if (attr.name) {
 243      var autofillClone = angular.element('<select class="md-visually-hidden">');
 244      autofillClone.attr({
 245        'name': attr.name,
 246        'aria-hidden': 'true',
 247        'tabindex': '-1'
 248      });
 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);
 255      });
 256
 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>'
 264      );
 265
 266      element.parent().append(autofillClone);
 267    }
 268
 269    var isMultiple = $mdUtil.parseAttributeBoolean(attr.multiple);
 270
 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>' +
 276      '</div>';
 277
 278    selectTemplate = $mdUtil.supplant(selectTemplate, [multipleContent, element.html()]);
 279    element.empty().append(valueEl);
 280    element.append(selectTemplate);
 281
 282    if(!attr.tabindex){
 283      attr.$set('tabindex', 0);
 284    }
 285
 286    return function postLink(scope, element, attr, ctrls) {
 287      var untouched = true;
 288      var isDisabled, ariaLabelBase;
 289
 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);
 298
 299      if (disableAsterisk) {
 300        element.addClass('md-no-asterisk');
 301      }
 302
 303      if (containerCtrl) {
 304        var isErrorGetter = containerCtrl.isErrorGetter || function() {
 305          return ngModelCtrl.$invalid && (ngModelCtrl.$touched || (formCtrl && formCtrl.$submitted));
 306        };
 307
 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!");
 313          }
 314        }
 315
 316        containerCtrl.input = element;
 317        if (!containerCtrl.label) {
 318          $mdAria.expect(element, 'aria-label', element.attr('placeholder'));
 319        }
 320
 321        scope.$watch(isErrorGetter, containerCtrl.setInvalid);
 322      }
 323
 324      var selectContainer, selectScope, selectMenuCtrl;
 325
 326      findSelectContainer();
 327      $mdTheming(element);
 328
 329      if (formCtrl && angular.isDefined(attr.multiple)) {
 330        $mdUtil.nextTick(function() {
 331          var hasModelValue = ngModelCtrl.$modelValue || ngModelCtrl.$viewValue;
 332          if (hasModelValue) {
 333            formCtrl.$setPristine();
 334          }
 335        });
 336      }
 337
 338      var originalRender = ngModelCtrl.$render;
 339      ngModelCtrl.$render = function() {
 340        originalRender();
 341        syncLabelText();
 342        syncAriaLabel();
 343        inputCheckValue();
 344      };
 345
 346      attr.$observe('placeholder', ngModelCtrl.$render);
 347
 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);
 353        });
 354      }
 355
 356      mdSelectCtrl.setLabelText = function(text) {
 357        mdSelectCtrl.setIsPlaceholder(!text);
 358
 359        // Whether the select label has been given via user content rather than the internal
 360        // template of <md-option>
 361        var isSelectLabelFromUser = false;
 362
 363        if (attr.mdSelectedText && attr.mdSelectedHtml) {
 364          throw Error('md-select cannot have both `md-selected-text` and `md-selected-html`');
 365        }
 366
 367        if (attr.mdSelectedText || attr.mdSelectedHtml) {
 368          text = $parse(attr.mdSelectedText || attr.mdSelectedHtml)(scope);
 369          isSelectLabelFromUser = true;
 370        } else if (!text) {
 371          // Use placeholder attribute, otherwise fallback to the md-input-container label
 372          var tmpPlaceholder = attr.placeholder ||
 373              (containerCtrl && containerCtrl.label ? containerCtrl.label.text() : '');
 374
 375          text = tmpPlaceholder || '';
 376          isSelectLabelFromUser = true;
 377        }
 378
 379        var target = valueEl.children().eq(0);
 380
 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) {
 387          target.text(text);
 388        } else {
 389          // If we've reached this point, the text is not user-provided.
 390          target.html(text);
 391        }
 392      };
 393
 394      mdSelectCtrl.setIsPlaceholder = function(isPlaceholder) {
 395        if (isPlaceholder) {
 396          valueEl.addClass('md-select-placeholder');
 397          if (containerCtrl && containerCtrl.label) {
 398            containerCtrl.label.addClass('md-placeholder');
 399          }
 400        } else {
 401          valueEl.removeClass('md-select-placeholder');
 402          if (containerCtrl && containerCtrl.label) {
 403            containerCtrl.label.removeClass('md-placeholder');
 404          }
 405        }
 406      };
 407
 408      if (!isReadonly) {
 409        element
 410          .on('focus', function(ev) {
 411            // Always focus the container (if we have one) so floating labels and other styles are
 412            // applied properly
 413            containerCtrl && containerCtrl.setFocused(true);
 414          });
 415
 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) {
 419          if (untouched) {
 420            untouched = false;
 421            if (selectScope._mdSelectIsOpen) {
 422              event.stopImmediatePropagation();
 423            }
 424          }
 425
 426          if (selectScope._mdSelectIsOpen) return;
 427          containerCtrl && containerCtrl.setFocused(false);
 428          inputCheckValue();
 429        });
 430      }
 431
 432      mdSelectCtrl.triggerClose = function() {
 433        $parse(attr.mdOnClose)(scope);
 434      };
 435
 436      scope.$$postDigest(function() {
 437        initAriaLabel();
 438        syncLabelText();
 439        syncAriaLabel();
 440      });
 441
 442      function initAriaLabel() {
 443        var labelText = element.attr('aria-label') || element.attr('placeholder');
 444        if (!labelText && containerCtrl && containerCtrl.label) {
 445          labelText = containerCtrl.label.text();
 446        }
 447        ariaLabelBase = labelText;
 448        $mdAria.expect(element, 'aria-label', labelText);
 449      }
 450
 451      scope.$watch(function() {
 452        return selectMenuCtrl.selectedLabels();
 453      }, syncLabelText);
 454
 455      function syncLabelText() {
 456        if (selectContainer) {
 457          selectMenuCtrl = selectMenuCtrl || selectContainer.find('md-select-menu').controller('mdSelectMenu');
 458          mdSelectCtrl.setLabelText(selectMenuCtrl.selectedLabels());
 459        }
 460      }
 461
 462      function syncAriaLabel() {
 463        if (!ariaLabelBase) return;
 464        var ariaLabels = selectMenuCtrl.selectedLabels({mode: 'aria'});
 465        element.attr('aria-label', ariaLabels.length ? ariaLabelBase + ': ' + ariaLabels : ariaLabelBase);
 466      }
 467
 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
 476          if (multiple) {
 477            element.attr('multiple', 'multiple');
 478          } else {
 479            element.removeAttr('multiple');
 480          }
 481          element.attr('aria-multiselectable', multiple ? 'true' : 'false');
 482          if (selectContainer) {
 483            selectMenuCtrl.setMultiple(multiple);
 484            originalRender = ngModelCtrl.$render;
 485            ngModelCtrl.$render = function() {
 486              originalRender();
 487              syncLabelText();
 488              syncAriaLabel();
 489              inputCheckValue();
 490            };
 491            ngModelCtrl.$render();
 492          }
 493        });
 494      });
 495
 496      attr.$observe('disabled', function(disabled) {
 497        if (angular.isString(disabled)) {
 498          disabled = true;
 499        }
 500        // Prevent click event being registered twice
 501        if (isDisabled !== undefined && isDisabled === disabled) {
 502          return;
 503        }
 504        isDisabled = disabled;
 505        if (disabled) {
 506          element
 507            .attr({'aria-disabled': 'true'})
 508            .removeAttr('tabindex')
 509            .off('click', openSelect)
 510            .off('keydown', handleKeypress);
 511        } else {
 512          element
 513            .attr({'tabindex': attr.tabindex, 'aria-disabled': 'false'})
 514            .on('click', openSelect)
 515            .on('keydown', handleKeypress);
 516        }
 517      });
 518
 519      if (!attr.hasOwnProperty('disabled') && !attr.hasOwnProperty('ngDisabled')) {
 520        element.attr({'aria-disabled': 'false'});
 521        element.on('click', openSelect);
 522        element.on('keydown', handleKeypress);
 523      }
 524
 525      var ariaAttrs = {
 526        role: 'listbox',
 527        'aria-expanded': 'false',
 528        'aria-multiselectable': isMultiple && !attr.ngMultiple ? 'true' : 'false'
 529      };
 530
 531      if (!element[0].hasAttribute('id')) {
 532        ariaAttrs.id = 'select_' + $mdUtil.nextUid();
 533      }
 534
 535      var containerId = 'select_container_' + $mdUtil.nextUid();
 536      selectContainer.attr('id', containerId);
 537      ariaAttrs['aria-owns'] = containerId;
 538      element.attr(ariaAttrs);
 539
 540      scope.$on('$destroy', function() {
 541        $mdSelect
 542          .destroy()
 543          .finally(function() {
 544            if (containerCtrl) {
 545              containerCtrl.setFocused(false);
 546              containerCtrl.setHasValue(false);
 547              containerCtrl.input = null;
 548            }
 549            ngModelCtrl.$setTouched();
 550          });
 551      });
 552
 553
 554
 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);
 559      }
 560
 561      function findSelectContainer() {
 562        selectContainer = angular.element(
 563          element[0].querySelector('.md-select-menu-container')
 564        );
 565        selectScope = scope;
 566        if (attr.mdContainerClass) {
 567          var value = selectContainer[0].getAttribute('class') + ' ' + attr.mdContainerClass;
 568          selectContainer[0].setAttribute('class', value);
 569        }
 570        selectMenuCtrl = selectContainer.find('md-select-menu').controller('mdSelectMenu');
 571        selectMenuCtrl.init(ngModelCtrl, attr.ngModel);
 572        element.on('$destroy', function() {
 573          selectContainer.remove();
 574        });
 575      }
 576
 577      function handleKeypress(e) {
 578        if ($mdConstant.isNavigationKey(e)) {
 579          // prevent page scrolling on interaction
 580          e.preventDefault();
 581          openSelect(e);
 582        } else {
 583          if ($mdConstant.isInputKey(e) || $mdConstant.isNumPadKey(e)) {
 584            e.preventDefault();
 585
 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]);
 591            }
 592            selectMenuCtrl.select(optionCtrl.hashKey, optionCtrl.value);
 593            selectMenuCtrl.refreshViewValue();
 594          }
 595        }
 596      }
 597
 598      function openSelect() {
 599        selectScope._mdSelectIsOpen = true;
 600        element.attr('aria-expanded', 'true');
 601
 602        $mdSelect.show({
 603          scope: selectScope,
 604          preserveScope: true,
 605          skipCompile: true,
 606          element: selectContainer,
 607          target: element[0],
 608          selectCtrl: mdSelectCtrl,
 609          preserveElement: true,
 610          hasBackdrop: true,
 611          loadingAsync: attr.mdOnOpen ? scope.$eval(attr.mdOnOpen) || true : false
 612        }).finally(function() {
 613          selectScope._mdSelectIsOpen = false;
 614          element.focus();
 615          element.attr('aria-expanded', 'false');
 616          ngModelCtrl.$setTouched();
 617        });
 618      }
 619
 620    };
 621  }
 622}
 623
 624function 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
 627  // parent scope.
 628  SelectMenuController['$inject'] = ["$scope", "$attrs", "$element"];
 629  return {
 630    restrict: 'E',
 631    require: ['mdSelectMenu'],
 632    scope: false,
 633    controller: SelectMenuController,
 634    link: {pre: preLink}
 635  };
 636
 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];
 641
 642    element.addClass('_md');     // private md component indicator for styling
 643
 644    $mdTheming(element);
 645    element.on('click', clickListener);
 646    element.on('keypress', keyListener);
 647
 648    function keyListener(e) {
 649      if (e.keyCode == 13 || e.keyCode == 32) {
 650        clickListener(e);
 651      }
 652    }
 653
 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();
 660        return false;
 661      }
 662
 663      var optionHashKey = selectCtrl.hashGetter(optionCtrl.value);
 664      var isSelected = angular.isDefined(selectCtrl.selected[optionHashKey]);
 665
 666      scope.$apply(function() {
 667        if (selectCtrl.isMultiple) {
 668          if (isSelected) {
 669            selectCtrl.deselect(optionHashKey);
 670          } else {
 671            selectCtrl.select(optionHashKey, optionCtrl.value);
 672          }
 673        } else {
 674          if (!isSelected) {
 675            selectCtrl.deselect(Object.keys(selectCtrl.selected)[0]);
 676            selectCtrl.select(optionHashKey, optionCtrl.value);
 677          }
 678        }
 679        selectCtrl.refreshViewValue();
 680      });
 681    }
 682  }
 683
 684  function SelectMenuController($scope, $attrs, $element) {
 685    var self = this;
 686    self.isMultiple = angular.isDefined($attrs.multiple);
 687    // selected is an object with keys matching all of the selected options' hashed values
 688    self.selected = {};
 689    // options is an object with keys matching every option's hash value,
 690    // and values matching every option's controller.
 691    self.options = {};
 692
 693    $scope.$watchCollection(function() {
 694      return self.options;
 695    }, function() {
 696      self.ngModel.$render();
 697    });
 698
 699    var deregisterCollectionWatch;
 700    var defaultIsEmpty;
 701    self.setMultiple = function(isMultiple) {
 702      var ngModel = self.ngModel;
 703      defaultIsEmpty = defaultIsEmpty || ngModel.$isEmpty;
 704
 705      self.isMultiple = isMultiple;
 706      if (deregisterCollectionWatch) deregisterCollectionWatch();
 707
 708      if (self.isMultiple) {
 709        ngModel.$validators['md-multiple'] = validateArray;
 710        ngModel.$render = renderMultiple;
 711
 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();
 717        });
 718
 719        ngModel.$isEmpty = function(value) {
 720          return !value || value.length === 0;
 721        };
 722      } else {
 723        delete ngModel.$validators['md-multiple'];
 724        ngModel.$render = renderSingular;
 725      }
 726
 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 || []);
 731      }
 732    };
 733
 734    var searchStr = '';
 735    var clearSearchTimeout, optNodes, optText;
 736    var CLEAR_SEARCH_AFTER = 300;
 737
 738    self.optNodeForKeyboardSearch = function(e) {
 739      clearSearchTimeout && clearTimeout(clearSearchTimeout);
 740      clearSearchTimeout = setTimeout(function() {
 741        clearSearchTimeout = undefined;
 742        searchStr = '';
 743        optText = undefined;
 744        optNodes = undefined;
 745      }, CLEAR_SEARCH_AFTER);
 746
 747      // Support 1-9 on numpad
 748      var keyCode = e.keyCode - ($mdConstant.isNumPadKey(e) ? 48 : 0);
 749
 750      searchStr += String.fromCharCode(keyCode);
 751      var search = new RegExp('^' + searchStr, 'i');
 752      if (!optNodes) {
 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();
 757        });
 758      }
 759      for (var i = 0; i < optText.length; ++i) {
 760        if (search.test(optText[i])) {
 761          return optNodes[i];
 762        }
 763      }
 764    };
 765
 766    self.init = function(ngModel, binding) {
 767      self.ngModel = ngModel;
 768      self.modelBinding = binding;
 769
 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)];
 775      };
 776
 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');
 780
 781      if (trackByOption) {
 782        var trackByLocals = {};
 783        var trackByParsed = $parse(trackByOption);
 784        self.hashGetter = function(value, valueScope) {
 785          trackByLocals.$value = value;
 786          return trackByParsed(valueScope || $scope, trackByLocals);
 787        };
 788        // If the user doesn't provide a trackBy, we automatically generate an id for every
 789        // value passed in
 790      } else {
 791        self.hashGetter = function getHashValue(value) {
 792          if (angular.isObject(value)) {
 793            return 'object_' + (value.$$mdSelectId || (value.$$mdSelectId = ++selectNextId));
 794          }
 795          return value;
 796        };
 797      }
 798      self.setMultiple(self.isMultiple);
 799    };
 800
 801    self.selectedLabels = function(opts) {
 802      opts = opts || {};
 803      var mode = opts.mode || 'html';
 804      var selectedOptionEls = $mdUtil.nodesToArray($element[0].querySelectorAll('md-option[selected]'));
 805      if (selectedOptionEls.length) {
 806        var mapFn;
 807
 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')) {
 814              return '';
 815            }
 816
 817            var html = el.innerHTML;
 818
 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, '');
 823            }
 824
 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, '');
 830            }
 831
 832            return html;
 833          };
 834        } else if (mode == 'aria') {
 835          mapFn = function(el) { return el.hasAttribute('aria-label') ? el.getAttribute('aria-label') : el.textContent; };
 836        }
 837
 838        // Ensure there are no duplicates; see https://github.com/angular/material/issues/9442
 839        return $mdUtil.uniq(selectedOptionEls.map(mapFn)).join(', ');
 840      } else {
 841        return '';
 842      }
 843    };
 844
 845    self.select = function(hashKey, hashedValue) {
 846      var option = self.options[hashKey];
 847      option && option.setSelected(true);
 848      self.selected[hashKey] = hashedValue;
 849    };
 850    self.deselect = function(hashKey) {
 851      var option = self.options[hashKey];
 852      option && option.setSelected(false);
 853      delete self.selected[hashKey];
 854    };
 855
 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.');
 860      }
 861
 862      self.options[hashKey] = optionCtrl;
 863
 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);
 867
 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();
 874        }
 875
 876        self.refreshViewValue();
 877      }
 878    };
 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.
 883    };
 884
 885    self.refreshViewValue = function() {
 886      var values = [];
 887      var option;
 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);
 892        } else {
 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]);
 899        }
 900      }
 901      var usingTrackBy = $mdUtil.getModelOption(self.ngModel, 'trackBy');
 902
 903      var newVal = self.isMultiple ? values : values[0];
 904      var prevVal = self.ngModel.$modelValue;
 905
 906      if (usingTrackBy ? !angular.equals(prevVal, newVal) : (prevVal + '') !== newVal) {
 907        self.ngModel.$setViewValue(newVal);
 908        self.ngModel.$render();
 909      }
 910    };
 911
 912    function renderMultiple() {
 913      var newSelectedValues = self.ngModel.$modelValue || self.ngModel.$viewValue || [];
 914      if (!angular.isArray(newSelectedValues)) return;
 915
 916      var oldSelected = Object.keys(self.selected);
 917
 918      var newSelectedHashes = newSelectedValues.map(self.hashGetter);
 919      var deselected = oldSelected.filter(function(hash) {
 920        return newSelectedHashes.indexOf(hash) === -1;
 921      });
 922
 923      deselected.forEach(self.deselect);
 924      newSelectedHashes.forEach(function(hashKey, i) {
 925        self.select(hashKey, newSelectedValues[i]);
 926      });
 927    }
 928
 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);
 933    }
 934  }
 935
 936}
 937
 938function OptionDirective($mdButtonInkRipple, $mdUtil) {
 939
 940  OptionController['$inject'] = ["$element"];
 941  return {
 942    restrict: 'E',
 943    require: ['mdOption', '^^mdSelectMenu'],
 944    controller: OptionController,
 945    compile: compile
 946  };
 947
 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()));
 951
 952    element.attr('tabindex', attr.tabindex || '0');
 953
 954    if (!hasDefinedValue(attr)) {
 955      element.attr('md-option-empty', '');
 956    }
 957
 958    return postLink;
 959  }
 960
 961  function hasDefinedValue(attr) {
 962    var value = attr.value;
 963    var ngValue = attr.ngValue;
 964
 965    return value || ngValue;
 966  }
 967
 968  function postLink(scope, element, attr, ctrls) {
 969    var optionCtrl = ctrls[0];
 970    var selectCtrl = ctrls[1];
 971
 972    if (selectCtrl.isMultiple) {
 973      element.addClass('md-checkbox-enabled');
 974      element.prepend(CHECKBOX_SELECTION_INDICATOR.clone());
 975    }
 976
 977    if (angular.isDefined(attr.ngValue)) {
 978      scope.$watch(attr.ngValue, setOptionValue);
 979    } else if (angular.isDefined(attr.value)) {
 980      setOptionValue(attr.value);
 981    } else {
 982      scope.$watch(function() {
 983        return element.text().trim();
 984      }, setOptionValue);
 985    }
 986
 987    attr.$observe('disabled', function(disabled) {
 988      if (disabled) {
 989        element.attr('tabindex', '-1');
 990      } else {
 991        element.attr('tabindex', '0');
 992      }
 993    });
 994
 995    scope.$$postDigest(function() {
 996      attr.$observe('selected', function(selected) {
 997        if (!angular.isDefined(selected)) return;
 998        if (typeof selected == 'string') selected = true;
 999        if (selected) {
1000          if (!selectCtrl.isMultiple) {
1001            selectCtrl.deselect(Object.keys(selectCtrl.selected)[0]);
1002          }
1003          selectCtrl.select(optionCtrl.hashKey, optionCtrl.value);
1004        } else {
1005          selectCtrl.deselect(optionCtrl.hashKey);
1006        }
1007        selectCtrl.refreshViewValue();
1008      });
1009    });
1010
1011    $mdButtonInkRipple.attach(scope, element);
1012    configureAria();
1013
1014    function setOptionValue(newValue, oldValue, prevAttempt) {
1015      if (!selectCtrl.hashGetter) {
1016        if (!prevAttempt) {
1017          scope.$$postDigest(function() {
1018            setOptionValue(newValue, oldValue, true);
1019          });
1020        }
1021        return;
1022      }
1023      var oldHashKey = selectCtrl.hashGetter(oldValue, scope);
1024      var newHashKey = selectCtrl.hashGetter(newValue, scope);
1025
1026      optionCtrl.hashKey = newHashKey;
1027      optionCtrl.value = newValue;
1028
1029      selectCtrl.removeOption(oldHashKey, optionCtrl);
1030      selectCtrl.addOption(newHashKey, optionCtrl);
1031    }
1032
1033    scope.$on('$destroy', function() {
1034      selectCtrl.removeOption(optionCtrl.hashKey, optionCtrl);
1035    });
1036
1037    function configureAria() {
1038      var ariaAttrs = {
1039        'role': 'option',
1040        'aria-selected': 'false'
1041      };
1042
1043      if (!element[0].hasAttribute('id')) {
1044        ariaAttrs.id = 'select_option_' + $mdUtil.nextUid();
1045      }
1046      element.attr(ariaAttrs);
1047    }
1048  }
1049
1050  function OptionController($element) {
1051    this.selected = false;
1052    this.setSelected = function(isSelected) {
1053      if (isSelected && !this.selected) {
1054        $element.attr({
1055          'selected': 'selected',
1056          'aria-selected': 'true'
1057        });
1058      } else if (!isSelected && this.selected) {
1059        $element.removeAttr('selected');
1060        $element.attr('aria-selected', 'false');
1061      }
1062      this.selected = isSelected;
1063    };
1064  }
1065
1066}
1067
1068function OptgroupDirective() {
1069  return {
1070    restrict: 'E',
1071    compile: compile
1072  };
1073  function compile(el, attrs) {
1074    // If we have a select header element, we don't want to add the normal label
1075    // header.
1076    if (!hasSelectHeader()) {
1077      setupLabelElement();
1078    }
1079
1080    function hasSelectHeader() {
1081      return el.parent().find('md-select-header').length;
1082    }
1083
1084    function setupLabelElement() {
1085      var labelElement = el.find('label');
1086      if (!labelElement.length) {
1087        labelElement = angular.element('<label>');
1088        el.prepend(labelElement);
1089      }
1090      labelElement.addClass('md-container-ignore');
1091      if (attrs.label) labelElement.text(attrs.label);
1092    }
1093  }
1094}
1095
1096function SelectHeaderDirective() {
1097  return {
1098    restrict: 'E',
1099  };
1100}
1101
1102function SelectProvider($$interimElementProvider) {
1103  selectDefaultOptions['$inject'] = ["$mdSelect", "$mdConstant", "$mdUtil", "$window", "$q", "$$rAF", "$animateCss", "$animate", "$document"];
1104  return $$interimElementProvider('$mdSelect')
1105    .setDefaults({
1106      methods: ['target'],
1107      options: selectDefaultOptions
1108    });
1109
1110  /* ngInject */
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;
1115
1116    return {
1117      parent: 'body',
1118      themable: true,
1119      onShow: onShow,
1120      onRemove: onRemove,
1121      hasBackdrop: true,
1122      disableParentScroll: true
1123    };
1124
1125    /**
1126     * Interim-element onRemove logic....
1127     */
1128    function onRemove(scope, element, opts) {
1129      opts = opts || { };
1130      opts.cleanupInteraction();
1131      opts.cleanupResizing();
1132      opts.hideBackdrop();
1133
1134      // For navigation $destroy events, do a quick, non-animated removal,
1135      // but for normal closes (from clicks, etc) animate the removal
1136
1137      return  (opts.$destroy === true) ? cleanElement() : animateRemoval().then( cleanElement );
1138
1139      /**
1140       * For normal closes (eg clicks), animate the removal.
1141       * For forced closes (like $destroy events from navigation),
1142       * skip the animations
1143       */
1144      function animateRemoval() {
1145        return $animateCss(element, {addClass: 'md-leave'}).start();
1146      }
1147
1148      /**
1149       * Restore the element to a closed state
1150       */
1151      function cleanElement() {
1152
1153        element.removeClass('md-active');
1154        element.attr('aria-hidden', 'true');
1155        element[0].style.display = 'none';
1156
1157        announceClosed(opts);
1158
1159        if (!opts.$destroy && opts.restoreFocus) {
1160          opts.target.focus();
1161        }
1162      }
1163
1164    }
1165
1166    /**
1167     * Interim-element onShow logic....
1168     */
1169    function onShow(scope, element, opts) {
1170
1171      watchAsyncLoad();
1172      sanitizeAndConfigure(scope, opts);
1173
1174      opts.hideBackdrop = showBackdrop(scope, element, opts);
1175
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();
1182
1183          return response;
1184        }, opts.hideBackdrop);
1185
1186      // ************************************
1187      // Closure Functions
1188      // ************************************
1189
1190      /**
1191       *  Attach the select DOM element(s) and animate to the correct positions
1192       *  and scalings...
1193       */
1194      function showDropDown(scope, element, opts) {
1195        opts.parent.append(element);
1196
1197        return $q(function(resolve, reject) {
1198
1199          try {
1200
1201            $animateCss(element, {removeClass: 'md-leave', duration: 0})
1202              .start()
1203              .then(positionAndFocusMenu)
1204              .then(resolve);
1205
1206          } catch (e) {
1207            reject(e);
1208          }
1209
1210        });
1211      }
1212
1213      /**
1214       * Initialize container and dropDown menu positions/scale, then animate
1215       * to show... and autoFocus.
1216       */
1217      function positionAndFocusMenu() {
1218        return $q(function(resolve) {
1219          if (opts.isRemoved) return $q.reject(false);
1220
1221          var info = calculateMenuPositions(scope, element, opts);
1222
1223          info.container.element.css(animator.toCss(info.container.styles));
1224          info.dropDown.element.css(animator.toCss(info.dropDown.styles));
1225
1226          $$rAF(function() {
1227            element.addClass('md-active');
1228            info.dropDown.element.css(animator.toCss({transform: ''}));
1229
1230            autoFocus(opts.focusedNode);
1231            resolve();
1232          });
1233
1234        });
1235      }
1236
1237      /**
1238       * Show modal backdrop element...
1239       */
1240      function showBackdrop(scope, element, options) {
1241
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);
1247        } else {
1248          options.disableParentScroll = false;
1249        }
1250
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});
1255        }
1256
1257        /**
1258         * Hide modal backdrop element...
1259         */
1260        return function hideBackdrop() {
1261          if (options.backdrop) options.backdrop.remove();
1262          if (options.disableParentScroll) options.restoreScroll();
1263
1264          delete options.restoreScroll;
1265        };
1266      }
1267
1268      /**
1269       *
1270       */
1271      function autoFocus(focusedNode) {
1272        if (focusedNode && !focusedNode.hasAttribute('disabled')) {
1273          focusedNode.focus();
1274        }
1275      }
1276
1277      /**
1278       * Check for valid opts and set some sane defaults
1279       */
1280      function sanitizeAndConfigure(scope, options) {
1281        var selectEl = element.find('md-select-menu');
1282
1283        if (!options.target) {
1284          throw new Error($mdUtil.supplant(ERROR_TARGET_EXPECTED, [options.target]));
1285        }
1286
1287        angular.extend(options, {
1288          isRemoved: false,
1289          target: angular.element(options.target), //make sure it's not a naked dom node
1290          parent: angular.element(options.parent),
1291          selectEl: selectEl,
1292          contentEl: element.find('md-content'),
1293          optionNodes: selectEl[0].getElementsByTagName('md-option')
1294        });
1295      }
1296
1297      /**
1298       * Configure various resize listeners for screen changes
1299       */
1300      function activateResizing() {
1301        var debouncedOnResize = (function(scope, target, options) {
1302
1303          return function() {
1304            if (options.isRemoved) return;
1305
1306            var updates = calculateMenuPositions(scope, target, options);
1307            var container = updates.container;
1308            var dropDown = updates.dropDown;
1309
1310            container.element.css(animator.toCss(container.styles));
1311            dropDown.element.css(animator.toCss(dropDown.styles));
1312          };
1313
1314        })(scope, element, opts);
1315
1316        var window = angular.element($window);
1317        window.on('resize', debouncedOnResize);
1318        window.on('orientationchange', debouncedOnResize);
1319
1320        // Publish deactivation closure...
1321        return function deactivateResizing() {
1322
1323          // Disable resizing handlers
1324          window.off('resize', debouncedOnResize);
1325          window.off('orientationchange', debouncedOnResize);
1326        };
1327      }
1328
1329      /**
1330       *  If asynchronously loading, watch and update internal
1331       *  '$$loadingAsyncDone' flag
1332       */
1333      function watchAsyncLoad() {
1334        if (opts.loadingAsync && !opts.isRemoved) {
1335          scope.$$loadingAsyncDone = false;
1336
1337          $q.when(opts.loadingAsync)
1338            .then(function() {
1339              scope.$$loadingAsyncDone = true;
1340              delete opts.loadingAsync;
1341            }).then(function() {
1342              $$rAF(positionAndFocusMenu);
1343            });
1344        }
1345      }
1346
1347      /**
1348       *
1349       */
1350      function activateInteraction() {
1351        if (opts.isRemoved) return;
1352
1353        var dropDown = opts.selectEl;
1354        var selectCtrl = dropDown.controller('mdSelectMenu') || {};
1355
1356        element.addClass('md-clickable');
1357
1358        // Close on backdrop click
1359        opts.backdrop && opts.backdrop.on('click', onBackdropClick);
1360
1361        // Escape to close
1362        // Cycling of options, and closing on enter
1363        dropDown.on('keydown', onMenuKeyDown);
1364        dropDown.on('click', checkCloseMenu);
1365
1366        return function cleanupInteraction() {
1367          opts.backdrop && opts.backdrop.off('click', onBackdropClick);
1368          dropDown.off('keydown', onMenuKeyDown);
1369          dropDown.off('click', checkCloseMenu);
1370
1371          element.removeClass('md-clickable');
1372          opts.isRemoved = true;
1373        };
1374
1375        // ************************************
1376        // Closure Functions
1377        // ************************************
1378
1379        function onBackdropClick(e) {
1380          e.preventDefault();
1381          e.stopPropagation();
1382          opts.restoreFocus = false;
1383          $mdUtil.nextTick($mdSelect.hide, true);
1384        }
1385
1386        function onMenuKeyDown(ev) {
1387          ev.preventDefault();
1388          ev.stopPropagation();
1389
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');
1398              if (option) {
1399                dropDown.triggerHandler({
1400                  type: 'click',
1401                  target: option
1402                });
1403                ev.preventDefault();
1404              }
1405              checkCloseMenu(ev);
1406              break;
1407            case keyCodes.TAB:
1408            case keyCodes.ESCAPE:
1409              ev.stopPropagation();
1410              ev.preventDefault();
1411              opts.restoreFocus = true;
1412              $mdUtil.nextTick($mdSelect.hide, true);
1413              break;
1414            default:
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();
1419              }
1420          }
1421        }
1422
1423        function focusOption(direction) {
1424          var optionsArray = $mdUtil.nodesToArray(opts.optionNodes);
1425          var index = optionsArray.indexOf(opts.focusedNode);
1426
1427          var newOption;
1428
1429          do {
1430            if (index === -1) {
1431              // We lost the previously focused element, reset to first option
1432              index = 0;
1433            } else if (direction === 'next' && index < optionsArray.length - 1) {
1434              index++;
1435            } else if (direction === 'prev' && index > 0) {
1436              index--;
1437            }
1438            newOption = optionsArray[index];
1439            if (newOption.hasAttribute('disabled')) newOption = undefined;
1440          } while (!newOption && index < optionsArray.length - 1 && index > 0);
1441
1442          newOption && newOption.focus();
1443          opts.focusedNode = newOption;
1444        }
1445
1446        function focusNextOption() {
1447          focusOption('next');
1448        }
1449
1450        function focusPrevOption() {
1451          focusOption('prev');
1452        }
1453
1454        function checkCloseMenu(ev) {
1455          if (ev && ( ev.type == 'click') && (ev.currentTarget != dropDown[0])) return;
1456          if ( mouseOnScrollbar() ) return;
1457
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;
1464
1465              $mdUtil.nextTick(function () {
1466                $mdSelect.hide(selectCtrl.ngModel.$viewValue);
1467              }, true);
1468            }
1469          }
1470          /**
1471           * check if the mouseup event was on a scrollbar
1472           */
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;
1482              }
1483            }
1484            return clickOnScrollbar;
1485          }
1486        }
1487      }
1488
1489    }
1490
1491    /**
1492     * To notify listeners that the Select menu has closed,
1493     * trigger the [optional] user-defined expression
1494     */
1495    function announceClosed(opts) {
1496      var mdSelect = opts.selectCtrl;
1497      if (mdSelect) {
1498        var menuController = opts.selectEl.controller('mdSelectMenu');
1499        mdSelect.setLabelText(menuController ? menuController.selectedLabels() : '');
1500        mdSelect.triggerClose();
1501      }
1502    }
1503
1504
1505    /**
1506     * Calculate the
1507     */
1508    function calculateMenuPositions(scope, element, opts) {
1509      var
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,
1518        bounds = {
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)
1523        },
1524        spaceAvailable = {
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)
1529        },
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),
1535        centeredNode;
1536
1537      var loading = isPromiseLike(opts.loadingAsync);
1538      if (!loading) {
1539        // If a selected node, center around that
1540        if (selectedNode) {
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)
1549        } else {
1550          centeredNode = contentNode.firstElementChild || contentNode;
1551        }
1552      } else {
1553        // If loading, center on progress indicator
1554        centeredNode = contentNode.firstElementChild || contentNode;
1555      }
1556
1557      if (contentNode.offsetWidth > maxWidth) {
1558        contentNode.style['max-width'] = maxWidth + 'px';
1559      } else {
1560        contentNode.style.maxWidth = null;
1561      }
1562      if (shouldOpenAroundTarget) {
1563        contentNode.style['min-width'] = targetRect.width + 'px';
1564      }
1565
1566      // Remove padding before we compute the position of the menu
1567      if (isScrollable) {
1568        selectNode.classList.add('md-overflow');
1569      }
1570
1571      var focusedNode = centeredNode;
1572      if ((focusedNode.tagName || '').toUpperCase() === 'MD-OPTGROUP') {
1573        focusedNode = optionNodes[0] || contentNode.firstElementChild || contentNode;
1574        centeredNode = focusedNode;
1575      }
1576      // Cache for autoFocus()
1577      opts.focusedNode = focusedNode;
1578
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);
1583
1584      if (centeredNode) {
1585        var centeredStyle = $window.getComputedStyle(centeredNode);
1586        centeredRect.paddingLeft = parseInt(centeredStyle.paddingLeft, 10) || 0;
1587        centeredRect.paddingRight = parseInt(centeredStyle.paddingRight, 10) || 0;
1588      }
1589
1590      if (isScrollable) {
1591        var scrollBuffer = contentNode.offsetHeight / 2;
1592        contentNode.scrollTop = centeredRect.top + centeredRect.height / 2 - scrollBuffer;
1593
1594        if (spaceAvailable.top < scrollBuffer) {
1595          contentNode.scrollTop = Math.min(
1596            centeredRect.top,
1597            contentNode.scrollTop + scrollBuffer - spaceAvailable.top
1598          );
1599        } else if (spaceAvailable.bottom < scrollBuffer) {
1600          contentNode.scrollTop = Math.max(
1601            centeredRect.top + centeredRect.height - selectMenuRect.height,
1602            contentNode.scrollTop - scrollBuffer + spaceAvailable.bottom
1603          );
1604        }
1605      }
1606
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%';
1615        }
1616      } else {
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;
1620
1621        transformOrigin = (centeredRect.left + targetRect.width / 2) + 'px ' +
1622          (centeredRect.top + centeredRect.height / 2 - contentNode.scrollTop) + 'px 0px';
1623
1624        minWidth = Math.min(targetRect.width + centeredRect.paddingLeft + centeredRect.paddingRight, maxWidth);
1625
1626        fontSize = window.getComputedStyle(targetNode)['font-size'];
1627      }
1628
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;
1633
1634      return {
1635        container: {
1636          element: angular.element(containerNode),
1637          styles: {
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
1642          }
1643        },
1644        dropDown: {
1645          element: angular.element(selectNode),
1646          styles: {
1647            transformOrigin: transformOrigin,
1648            transform: !opts.alreadyOpen ? $mdUtil.supplant('scale({0},{1})', [scaleX, scaleY]) : ""
1649          }
1650        }
1651      };
1652
1653    }
1654
1655  }
1656
1657  function isPromiseLike(obj) {
1658    return obj && angular.isFunction(obj.then);
1659  }
1660
1661  function clamp(min, n, max) {
1662    return Math.max(min, Math.min(n, max));
1663  }
1664
1665  function getOffsetRect(node) {
1666    return 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};
1672  }
1673
1674  function calculateScrollable(element, contentNode) {
1675    var isScrollable = false;
1676
1677    try {
1678      var oldDisplay = element[0].style.display;
1679
1680      // Set the element's display to block so that this calculation is correct
1681      element[0].style.display = 'block';
1682
1683      isScrollable = contentNode.scrollHeight > contentNode.offsetHeight;
1684
1685      // Reset it back afterwards
1686      element[0].style.display = oldDisplay;
1687    } finally {
1688      // Nothing to do
1689    }
1690    return isScrollable;
1691  }
1692}
1693
1694})(window, window.angular);