e1c5793948f57e745d9da28f1e0665eb86b85103
[vnfsdk/refrepo.git] /
1 /*!
2  * Angular Material Design
3  * https://github.com/angular/material
4  * @license MIT
5  * v1.1.3
6  */
7 goog.provide('ngmaterial.components.select');
8 goog.require('ngmaterial.components.backdrop');
9 goog.require('ngmaterial.core');
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
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;
27 var selectNextId = 0;
28 var CHECKBOX_SELECTION_INDICATOR =
29   angular.element('<div class="md-container"><div class="md-icon"></div></div>');
30
31 angular.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  */
195 function 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
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
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
938 function 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
1068 function 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
1096 function SelectHeaderDirective() {
1097   return {
1098     restrict: 'E',
1099   };
1100 }
1101
1102 function 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 ngmaterial.components.select = angular.module("material.components.select");