nexus site path corrected
[portal.git] / ecomp-portal-FE / client / bower_components / angular-material / modules / closure / select / select.js
1 /*!
2  * Angular Material Design
3  * https://github.com/angular/material
4  * @license MIT
5  * v0.9.8
6  */
7 goog.provide('ng.material.components.select');
8 goog.require('ng.material.components.backdrop');
9 goog.require('ng.material.core');
10 /**
11  * @ngdoc module
12  * @name material.components.select
13  */
14
15 /***************************************************
16
17 ### TODO ###
18 **DOCUMENTATION AND DEMOS**
19
20 - [ ] ng-model with child mdOptions (basic)
21 - [ ] ng-model="foo" ng-model-options="{ trackBy: '$value.id' }" for objects
22 - [ ] mdOption with value
23 - [ ] Usage with input inside
24
25 ### TODO - POST RC1 ###
26 - [ ] Abstract placement logic in $mdSelect service to $mdMenu service
27
28 ***************************************************/
29
30 var SELECT_EDGE_MARGIN = 8;
31 var selectNextId = 0;
32
33 angular.module('material.components.select', [
34   'material.core',
35   'material.components.backdrop'
36 ])
37 .directive('mdSelect', SelectDirective)
38 .directive('mdSelectMenu', SelectMenuDirective)
39 .directive('mdOption', OptionDirective)
40 .directive('mdOptgroup', OptgroupDirective)
41 .provider('$mdSelect', SelectProvider);
42
43
44 /**
45  * @ngdoc directive
46  * @name mdSelect
47  * @restrict E
48  * @module material.components.select
49  *
50  * @description Displays a select box, bound to an ng-model.
51  *
52  * @param {expression} ng-model The model!
53  * @param {boolean=} multiple Whether it's multiple.
54  * @param {string=} placeholder Placeholder hint text.
55  * @param {string=} aria-label Optional label for accessibility. Only necessary if no placeholder or
56  * explicit label is present.
57  *
58  * @usage
59  * With a placeholder (label and aria-label are added dynamically)
60  * <hljs lang="html">
61  *   <md-select
62  *     ng-model="someModel"
63  *     placeholder="Select a state">
64  *     <md-option ng-value="opt" ng-repeat="opt in neighborhoods2">{{ opt }}</md-option>
65  *   </md-select>
66  * </hljs>
67  *
68  * With an explicit label
69  * <hljs lang="html">
70  *   <md-select
71  *     ng-model="someModel">
72  *     <md-select-label>Select a state</md-select-label>
73  *     <md-option ng-value="opt" ng-repeat="opt in neighborhoods2">{{ opt }}</md-option>
74  *   </md-select>
75  * </hljs>
76  */
77 function SelectDirective($mdSelect, $mdUtil, $mdTheming, $mdAria, $interpolate, $compile, $parse) {
78   return {
79     restrict: 'E',
80     require: ['mdSelect', 'ngModel', '?^form'],
81     compile: compile,
82     controller: function() { } // empty placeholder controller to be initialized in link
83   };
84
85   function compile(element, attr) {
86     // The user is allowed to provide a label for the select as md-select-label child
87     var labelEl = element.find('md-select-label').remove();
88
89     // If not provided, we automatically make one
90     if (!labelEl.length) {
91       labelEl = angular.element('<md-select-label><span></span></md-select-label>');
92     } else {
93       if (!labelEl[0].firstElementChild) {
94         var spanWrapper = angular.element('<span>');
95         spanWrapper.append(labelEl.contents());
96         labelEl.append(spanWrapper);
97       }
98     }
99     labelEl.append('<span class="md-select-icon" aria-hidden="true"></span>');
100     labelEl.addClass('md-select-label');
101     if (!labelEl[0].hasAttribute('id')) {
102       labelEl.attr('id', 'select_label_' + $mdUtil.nextUid());
103     }
104
105     // There's got to be an md-content inside. If there's not one, let's add it.
106     if (!element.find('md-content').length) {
107       element.append( angular.element('<md-content>').append(element.contents()) );
108     }
109
110     // Add progress spinner for md-options-loading
111     if (attr.mdOnOpen) {
112       element.find('md-content').prepend(
113         angular.element('<md-progress-circular>')
114                .attr('md-mode', 'indeterminate')
115                .attr('ng-hide', '$$loadingAsyncDone')
116                .wrap('<div>')
117                .parent()
118       );
119     }
120
121     if (attr.name) {
122       var autofillClone = angular.element('<select class="md-visually-hidden">');
123       autofillClone.attr({
124         'name': '.' + attr.name,
125         'ng-model': attr.ngModel,
126         'aria-hidden': 'true',
127         'tabindex': '-1'
128       });
129       var opts = element.find('md-option');
130       angular.forEach(opts, function(el) {
131         var newEl = angular.element('<option>' + el.innerHTML + '</option>');
132         if (el.hasAttribute('ng-value')) newEl.attr('ng-value', el.getAttribute('ng-value'));
133         else if (el.hasAttribute('value')) newEl.attr('value', el.getAttribute('value'));
134         autofillClone.append(newEl);
135       });
136
137       element.parent().append(autofillClone);
138     }
139
140     // Use everything that's left inside element.contents() as the contents of the menu
141     var selectTemplate = '<div class="md-select-menu-container">' +
142         '<md-select-menu ' +
143         (angular.isDefined(attr.multiple) ? 'multiple' : '') + '>' +
144           element.html() +
145         '</md-select-menu></div>';
146
147     element.empty().append(labelEl);
148
149     attr.tabindex = attr.tabindex || '0';
150
151     return function postLink(scope, element, attr, ctrls) {
152       var isOpen;
153       var isDisabled;
154
155       var mdSelectCtrl = ctrls[0];
156       var ngModel = ctrls[1];
157       var formCtrl = ctrls[2];
158
159       var labelEl = element.find('md-select-label');
160       var customLabel = labelEl.text().length !== 0;
161       var selectContainer, selectScope, selectMenuCtrl;
162       createSelect();
163
164       $mdTheming(element);
165
166       if (attr.name && formCtrl) {
167         var selectEl = element.parent()[0].querySelector('select[name=".' + attr.name + '"]')
168         formCtrl.$removeControl(angular.element(selectEl).controller());
169       }
170
171       var originalRender = ngModel.$render;
172       ngModel.$render = function() {
173         originalRender();
174         syncLabelText();
175       };
176
177       mdSelectCtrl.setLabelText = function(text) {
178         if (customLabel) return; // Assume that user is handling it on their own
179         mdSelectCtrl.setIsPlaceholder(!text);
180         text = text || attr.placeholder || '';
181         var target = customLabel ? labelEl : labelEl.children().eq(0);
182         target.text(text);
183       };
184
185       mdSelectCtrl.setIsPlaceholder = function(val) {
186         val ? labelEl.addClass('md-placeholder') : labelEl.removeClass('md-placeholder');
187       };
188
189       scope.$$postDigest(function() {
190         setAriaLabel();
191         syncLabelText();
192       });
193
194       function setAriaLabel() {
195         var labelText = element.attr('placeholder');
196         if (!labelText) {
197           labelText = element.find('md-select-label').text();
198         }
199         $mdAria.expect(element, 'aria-label', labelText);
200       }
201
202       function syncLabelText() {
203         if (selectContainer) {
204           selectMenuCtrl = selectMenuCtrl || selectContainer.find('md-select-menu').controller('mdSelectMenu');
205           mdSelectCtrl.setLabelText(selectMenuCtrl.selectedLabels());
206         }
207       }
208
209       var deregisterWatcher;
210       attr.$observe('ngMultiple', function(val) {
211         if (deregisterWatcher) deregisterWatcher();
212         var parser = $parse(val);
213         deregisterWatcher = scope.$watch(function() { return parser(scope); }, function(multiple, prevVal) {
214           if (multiple === undefined && prevVal === undefined) return; // assume compiler did a good job
215           if (multiple) {
216             element.attr('multiple', 'multiple');
217           } else {
218             element.removeAttr('multiple');
219           }
220           if (selectContainer) {
221             selectMenuCtrl.setMultiple(multiple);
222             originalRender = ngModel.$render;
223             ngModel.$render = function() {
224               originalRender();
225               syncLabelText();
226             };
227             selectMenuCtrl.refreshViewValue();
228             ngModel.$render();
229           }
230         });
231       });
232
233       attr.$observe('disabled', function(disabled) {
234         if (typeof disabled == "string") {
235           disabled = true;
236         }
237         // Prevent click event being registered twice
238         if (isDisabled !== undefined && isDisabled === disabled) {
239           return;
240         }
241         isDisabled = disabled;
242         if (disabled) {
243           element.attr({'tabindex': -1, 'aria-disabled': 'true'});
244           element.off('click', openSelect);
245           element.off('keydown', handleKeypress);
246         } else {
247           element.attr({'tabindex': attr.tabindex, 'aria-disabled': 'false'});
248           element.on('click', openSelect);
249           element.on('keydown', handleKeypress);
250         }
251       });
252       if (!attr.disabled && !attr.ngDisabled) {
253         element.attr({'tabindex': attr.tabindex, 'aria-disabled': 'false'});
254         element.on('click', openSelect);
255         element.on('keydown', handleKeypress);
256       }
257
258       var ariaAttrs = {
259         role: 'combobox',
260         'aria-expanded': 'false'
261       };
262       if (!element[0].hasAttribute('id')) {
263         ariaAttrs.id = 'select_' + $mdUtil.nextUid();
264       }
265       element.attr(ariaAttrs);
266
267       scope.$on('$destroy', function() {
268         if (isOpen) {
269           $mdSelect.cancel().then(function() {
270             selectContainer.remove();
271           });
272         } else {
273           selectContainer.remove();
274         }
275       });
276
277
278       // Create a fake select to find out the label value
279       function createSelect() {
280         selectContainer = angular.element(selectTemplate);
281         var selectEl = selectContainer.find('md-select-menu');
282         selectEl.data('$ngModelController', ngModel);
283         selectEl.data('$mdSelectController', mdSelectCtrl);
284         selectScope = scope.$new();
285         selectContainer = $compile(selectContainer)(selectScope);
286         selectMenuCtrl = selectContainer.find('md-select-menu').controller('mdSelectMenu');
287       }
288
289       function handleKeypress(e) {
290         var allowedCodes = [32, 13, 38, 40];
291         if (allowedCodes.indexOf(e.keyCode) != -1 ) {
292           // prevent page scrolling on interaction
293           e.preventDefault();
294           openSelect(e);
295         } else {
296           if (e.keyCode <= 90 && e.keyCode >= 31) {
297             e.preventDefault();
298             var node = selectMenuCtrl.optNodeForKeyboardSearch(e);
299             if (!node) return;
300             var optionCtrl = angular.element(node).controller('mdOption');
301             if (!selectMenuCtrl.isMultiple) {
302               selectMenuCtrl.deselect( Object.keys(selectMenuCtrl.selected)[0] );
303             }
304             selectMenuCtrl.select(optionCtrl.hashKey, optionCtrl.value);
305             selectMenuCtrl.refreshViewValue();
306             ngModel.$render();
307           }
308         }
309       }
310
311       function openSelect() {
312         scope.$evalAsync(function() {
313           isOpen = true;
314           $mdSelect.show({
315             scope: selectScope,
316             preserveScope: true,
317             skipCompile: true,
318             element: selectContainer,
319             target: element[0],
320             hasBackdrop: true,
321             loadingAsync: attr.mdOnOpen ? scope.$eval(attr.mdOnOpen) || true : false,
322           }).then(function(selectedText) {
323             isOpen = false;
324           });
325         });
326       }
327     };
328   }
329 }
330 SelectDirective.$inject = ["$mdSelect", "$mdUtil", "$mdTheming", "$mdAria", "$interpolate", "$compile", "$parse"];
331
332 function SelectMenuDirective($parse, $mdUtil, $mdTheming) {
333
334   SelectMenuController.$inject = ["$scope", "$attrs", "$element"];
335   return {
336     restrict: 'E',
337     require: ['mdSelectMenu', '?ngModel'],
338     controller: SelectMenuController,
339     link: { pre: preLink }
340   };
341
342   // We use preLink instead of postLink to ensure that the select is initialized before
343   // its child options run postLink.
344   function preLink(scope, element, attr, ctrls) {
345     var selectCtrl = ctrls[0];
346     var ngModel = ctrls[1];
347
348     $mdTheming(element);
349     element.on('click', clickListener);
350     element.on('keypress', keyListener);
351     if (ngModel) selectCtrl.init(ngModel);
352     configureAria();
353
354     function configureAria() {
355       element.attr({
356         'id': 'select_menu_' + $mdUtil.nextUid(),
357         'role': 'listbox',
358         'aria-multiselectable': (selectCtrl.isMultiple ? 'true' : 'false')
359       });
360     }
361
362     function keyListener(e) {
363       if (e.keyCode == 13 || e.keyCode == 32) {
364         clickListener(e);
365       }
366     }
367
368     function clickListener(ev) {
369       var option = $mdUtil.getClosest(ev.target, 'md-option');
370       var optionCtrl = option && angular.element(option).data('$mdOptionController');
371       if (!option || !optionCtrl) return;
372
373       var optionHashKey = selectCtrl.hashGetter(optionCtrl.value);
374       var isSelected = angular.isDefined(selectCtrl.selected[optionHashKey]);
375
376       scope.$apply(function() {
377         if (selectCtrl.isMultiple) {
378           if (isSelected) {
379             selectCtrl.deselect(optionHashKey);
380           } else {
381             selectCtrl.select(optionHashKey, optionCtrl.value);
382           }
383         } else {
384           if (!isSelected) {
385             selectCtrl.deselect( Object.keys(selectCtrl.selected)[0] );
386             selectCtrl.select( optionHashKey, optionCtrl.value );
387           }
388         }
389         selectCtrl.refreshViewValue();
390       });
391     }
392   }
393
394
395
396   function SelectMenuController($scope, $attrs, $element) {
397     var self = this;
398     self.isMultiple = angular.isDefined($attrs.multiple);
399     // selected is an object with keys matching all of the selected options' hashed values
400     self.selected = {};
401     // options is an object with keys matching every option's hash value,
402     // and values matching every option's controller.
403     self.options = {};
404
405     $scope.$watch(function() { return self.options; }, function() {
406       self.ngModel.$render();
407     }, true);
408
409     var deregisterCollectionWatch;
410     self.setMultiple = function(isMultiple) {
411       var ngModel = self.ngModel;
412       self.isMultiple = isMultiple;
413       if (deregisterCollectionWatch) deregisterCollectionWatch();
414
415       if (self.isMultiple) {
416         ngModel.$validators['md-multiple'] = validateArray;
417         ngModel.$render = renderMultiple;
418
419         // watchCollection on the model because by default ngModel only watches the model's
420         // reference. This allowed the developer to also push and pop from their array.
421         $scope.$watchCollection($attrs.ngModel, function(value) {
422           if (validateArray(value)) renderMultiple(value);
423         });
424       } else {
425         delete ngModel.$validators['md-multiple'];
426         ngModel.$render = renderSingular;
427       }
428
429       function validateArray(modelValue, viewValue) {
430         // If a value is truthy but not an array, reject it.
431         // If value is undefined/falsy, accept that it's an empty array.
432         return angular.isArray(modelValue || viewValue || []);
433       }
434     };
435
436     var searchStr = '';
437     var clearSearchTimeout, optNodes, optText;
438     var CLEAR_SEARCH_AFTER = 300;
439     self.optNodeForKeyboardSearch = function(e) {
440       clearSearchTimeout && clearTimeout(clearSearchTimeout);
441       clearSearchTimeout = setTimeout(function() {
442         clearSearchTimeout = undefined;
443         searchStr = '';
444         optText = undefined;
445         optNodes = undefined;
446       }, CLEAR_SEARCH_AFTER);
447       searchStr += String.fromCharCode(e.keyCode);
448       var search = new RegExp('^' + searchStr, 'i');
449       if (!optNodes) {
450         optNodes = $element.find('md-option');
451         optText = new Array(optNodes.length);
452         angular.forEach(optNodes, function(el, i) {
453           optText[i] = el.textContent.trim();
454         });
455       }
456       for (var i = 0; i < optText.length; ++i) {
457         if (search.test(optText[i])) {
458           return optNodes[i];
459         }
460       }
461     };
462
463
464     self.init = function(ngModel) {
465       self.ngModel = ngModel;
466
467       // Allow users to provide `ng-model="foo" ng-model-options="{trackBy: 'foo.id'}"` so
468       // that we can properly compare objects set on the model to the available options
469       if (ngModel.$options && ngModel.$options.trackBy) {
470         var trackByLocals = {};
471         var trackByParsed = $parse(ngModel.$options.trackBy);
472         self.hashGetter = function(value, valueScope) {
473           trackByLocals.$value = value;
474           return trackByParsed(valueScope || $scope, trackByLocals);
475         };
476       // If the user doesn't provide a trackBy, we automatically generate an id for every
477       // value passed in
478       } else {
479         self.hashGetter = function getHashValue(value) {
480           if (angular.isObject(value)) {
481             return 'object_' + (value.$$mdSelectId || (value.$$mdSelectId = ++selectNextId));
482           }
483           return value;
484         };
485       }
486       self.setMultiple(self.isMultiple);
487     };
488
489     self.selectedLabels = function() {
490       var selectedOptionEls = nodesToArray($element[0].querySelectorAll('md-option[selected]'));
491       if (selectedOptionEls.length) {
492         return selectedOptionEls.map(function(el) { return el.textContent; }).join(', ');
493       } else {
494         return '';
495       }
496     };
497
498     self.select = function(hashKey, hashedValue) {
499       var option = self.options[hashKey];
500       option && option.setSelected(true);
501       self.selected[hashKey] = hashedValue;
502     };
503     self.deselect = function(hashKey) {
504       var option = self.options[hashKey];
505       option && option.setSelected(false);
506       delete self.selected[hashKey];
507     };
508
509     self.addOption = function(hashKey, optionCtrl) {
510       if (angular.isDefined(self.options[hashKey])) {
511         throw new Error('Duplicate md-option values are not allowed in a select. ' +
512                         'Duplicate value "' + optionCtrl.value + '" found.');
513       }
514       self.options[hashKey] = optionCtrl;
515
516       // If this option's value was already in our ngModel, go ahead and select it.
517       if (angular.isDefined(self.selected[hashKey])) {
518         self.select(hashKey, optionCtrl.value);
519         self.refreshViewValue();
520       }
521     };
522     self.removeOption = function(hashKey) {
523       delete self.options[hashKey];
524       // Don't deselect an option when it's removed - the user's ngModel should be allowed
525       // to have values that do not match a currently available option.
526     };
527
528     self.refreshViewValue = function() {
529       var values = [];
530       var option;
531       for (var hashKey in self.selected) {
532          // If this hashKey has an associated option, push that option's value to the model.
533          if ((option = self.options[hashKey])) {
534            values.push(option.value);
535          } else {
536            // Otherwise, the given hashKey has no associated option, and we got it
537            // from an ngModel value at an earlier time. Push the unhashed value of
538            // this hashKey to the model.
539            // This allows the developer to put a value in the model that doesn't yet have
540            // an associated option.
541            values.push(self.selected[hashKey]);
542          }
543       }
544       self.ngModel.$setViewValue(self.isMultiple ? values : values[0]);
545     };
546
547     function renderMultiple() {
548       var newSelectedValues = self.ngModel.$modelValue || self.ngModel.$viewValue;
549       if (!angular.isArray(newSelectedValues)) return;
550
551       var oldSelected = Object.keys(self.selected);
552
553       var newSelectedHashes = newSelectedValues.map(self.hashGetter);
554       var deselected = oldSelected.filter(function(hash) {
555         return newSelectedHashes.indexOf(hash) === -1;
556       });
557
558       deselected.forEach(self.deselect);
559       newSelectedHashes.forEach(function(hashKey, i) {
560         self.select(hashKey, newSelectedValues[i]);
561       });
562     }
563     function renderSingular() {
564       var value = self.ngModel.$viewValue || self.ngModel.$modelValue;
565       Object.keys(self.selected).forEach(self.deselect);
566       self.select( self.hashGetter(value), value );
567     }
568   }
569
570 }
571 SelectMenuDirective.$inject = ["$parse", "$mdUtil", "$mdTheming"];
572
573 function OptionDirective($mdButtonInkRipple, $mdUtil) {
574
575   OptionController.$inject = ["$element"];
576   return {
577     restrict: 'E',
578     require: ['mdOption', '^^mdSelectMenu'],
579     controller: OptionController,
580     compile: compile
581   };
582
583   function compile(element, attr) {
584     // Manual transclusion to avoid the extra inner <span> that ng-transclude generates
585     element.append( angular.element('<div class="md-text">').append(element.contents()) );
586
587     element.attr('tabindex', attr.tabindex || '0');
588     return postLink;
589   }
590
591   function postLink(scope, element, attr, ctrls) {
592     var optionCtrl = ctrls[0];
593     var selectCtrl = ctrls[1];
594
595     if (angular.isDefined(attr.ngValue)) {
596       scope.$watch(attr.ngValue, setOptionValue);
597     } else if (angular.isDefined(attr.value)) {
598       setOptionValue(attr.value);
599     } else {
600       scope.$watch(function() { return element.text(); }, setOptionValue);
601     }
602
603     scope.$$postDigest(function() {
604       attr.$observe('selected', function(selected) {
605         if (!angular.isDefined(selected)) return;
606         if (selected) {
607           if (!selectCtrl.isMultiple) {
608             selectCtrl.deselect( Object.keys(selectCtrl.selected)[0] );
609           }
610           selectCtrl.select(optionCtrl.hashKey, optionCtrl.value);
611         } else {
612           selectCtrl.deselect(optionCtrl.hashKey);
613         }
614         selectCtrl.refreshViewValue();
615         selectCtrl.ngModel.$render();
616       });
617     });
618
619     $mdButtonInkRipple.attach(scope, element);
620     configureAria();
621
622     function setOptionValue(newValue, oldValue) {
623       var oldHashKey = selectCtrl.hashGetter(oldValue, scope);
624       var newHashKey = selectCtrl.hashGetter(newValue, scope);
625
626       optionCtrl.hashKey = newHashKey;
627       optionCtrl.value = newValue;
628
629       selectCtrl.removeOption(oldHashKey, optionCtrl);
630       selectCtrl.addOption(newHashKey, optionCtrl);
631     }
632
633     scope.$on('$destroy', function() {
634       selectCtrl.removeOption(optionCtrl.hashKey, optionCtrl);
635     });
636
637     function configureAria() {
638       var ariaAttrs = {
639         'role': 'option',
640         'aria-selected': 'false'
641       };
642
643       if (!element[0].hasAttribute('id')) {
644         ariaAttrs.id = 'select_option_' + $mdUtil.nextUid();
645       }
646       element.attr(ariaAttrs);
647     }
648   }
649
650   function OptionController($element) {
651     this.selected = false;
652     this.setSelected = function(isSelected) {
653       if (isSelected && !this.selected) {
654         $element.attr({
655           'selected': 'selected',
656           'aria-selected': 'true'
657         });
658       } else if (!isSelected && this.selected) {
659         $element.removeAttr('selected');
660         $element.attr('aria-selected', 'false');
661       }
662       this.selected = isSelected;
663     };
664   }
665
666 }
667 OptionDirective.$inject = ["$mdButtonInkRipple", "$mdUtil"];
668
669 function OptgroupDirective() {
670   return {
671     restrict: 'E',
672     compile: compile
673   };
674   function compile(el, attrs) {
675     var labelElement = el.find('label');
676     if (!labelElement.length) {
677       labelElement = angular.element('<label>');
678       el.prepend(labelElement);
679     }
680     if (attrs.label) labelElement.text(attrs.label);
681   }
682 }
683
684 function SelectProvider($$interimElementProvider) {
685   selectDefaultOptions.$inject = ["$mdSelect", "$mdConstant", "$$rAF", "$mdUtil", "$mdTheming", "$timeout", "$window"];
686   return $$interimElementProvider('$mdSelect')
687     .setDefaults({
688       methods: ['target'],
689       options: selectDefaultOptions
690     });
691
692   /* ngInject */
693   function selectDefaultOptions($mdSelect, $mdConstant, $$rAF, $mdUtil, $mdTheming, $timeout, $window ) {
694     return {
695       parent: 'body',
696       onShow: onShow,
697       onRemove: onRemove,
698       hasBackdrop: true,
699       disableParentScroll: true,
700       themable: true
701     };
702
703     function onShow(scope, element, opts) {
704       if (!opts.target) {
705         throw new Error('$mdSelect.show() expected a target element in options.target but got ' +
706                         '"' + opts.target + '"!');
707       }
708
709       angular.extend(opts, {
710         isRemoved: false,
711         target: angular.element(opts.target), //make sure it's not a naked dom node
712         parent: angular.element(opts.parent),
713         selectEl: element.find('md-select-menu'),
714         contentEl: element.find('md-content'),
715         backdrop: opts.hasBackdrop && angular.element('<md-backdrop class="md-select-backdrop md-click-catcher">')
716       });
717
718       opts.resizeFn = function() {
719         $$rAF(function() {
720           $$rAF(function() {
721             animateSelect(scope, element, opts);
722           });
723         });
724       };
725
726       angular.element($window).on('resize', opts.resizeFn);
727       angular.element($window).on('orientationchange', opts.resizeFn);
728
729
730       configureAria();
731
732       element.removeClass('md-leave');
733
734       var optionNodes = opts.selectEl[0].getElementsByTagName('md-option');
735
736       if (opts.loadingAsync && opts.loadingAsync.then) {
737         opts.loadingAsync.then(function() {
738           scope.$$loadingAsyncDone = true;
739           // Give ourselves two frames for the progress loader to clear out.
740           $$rAF(function() {
741             $$rAF(function() {
742               // Don't go forward if the select has been removed in this time...
743               if (opts.isRemoved) return;
744               animateSelect(scope, element, opts);
745             });
746           });
747         });
748       } else if (opts.loadingAsync) {
749         scope.$$loadingAsyncDone = true;
750       }
751
752       if (opts.disableParentScroll && !$mdUtil.getClosest(opts.target, 'MD-DIALOG')) {
753         opts.restoreScroll = $mdUtil.disableScrollAround(opts.target);
754       } else {
755         opts.disableParentScroll = false;
756       }
757       // Only activate click listeners after a short time to stop accidental double taps/clicks
758       // from clicking the wrong item
759       $timeout(activateInteraction, 75, false);
760
761       if (opts.backdrop) {
762         $mdTheming.inherit(opts.backdrop, opts.parent);
763         opts.parent.append(opts.backdrop);
764       }
765       opts.parent.append(element);
766
767       // Give the select a frame to 'initialize' in the DOM,
768       // so we can read its height/width/position
769       $$rAF(function() {
770         $$rAF(function() {
771           if (opts.isRemoved) return;
772           animateSelect(scope, element, opts);
773         });
774       });
775
776       return $mdUtil.transitionEndPromise(opts.selectEl, {timeout: 350});
777
778       function configureAria() {
779         opts.target.attr('aria-expanded', 'true');
780       }
781
782       function activateInteraction() {
783         if (opts.isRemoved) return;
784         var selectCtrl = opts.selectEl.controller('mdSelectMenu') || {};
785         element.addClass('md-clickable');
786
787         opts.backdrop && opts.backdrop.on('click', function(e) {
788           e.preventDefault();
789           e.stopPropagation();
790           opts.restoreFocus = false;
791           scope.$apply($mdSelect.cancel);
792         });
793
794         // Escape to close
795         opts.selectEl.on('keydown', function(ev) {
796           switch (ev.keyCode) {
797             case $mdConstant.KEY_CODE.SPACE:
798             case $mdConstant.KEY_CODE.ENTER:
799               var option = $mdUtil.getClosest(ev.target, 'md-option');
800               if (option) {
801                 opts.selectEl.triggerHandler({
802                   type: 'click',
803                   target: option
804                 });
805                 ev.preventDefault();
806               }
807               break;
808             case $mdConstant.KEY_CODE.TAB:
809             case $mdConstant.KEY_CODE.ESCAPE:
810               ev.preventDefault();
811               opts.restoreFocus = true;
812               scope.$apply($mdSelect.cancel);
813           }
814         });
815
816         // Cycling of options, and closing on enter
817         opts.selectEl.on('keydown', function(ev) {
818           switch (ev.keyCode) {
819             case $mdConstant.KEY_CODE.UP_ARROW: return focusPrevOption();
820             case $mdConstant.KEY_CODE.DOWN_ARROW: return focusNextOption();
821             default:
822               if (ev.keyCode >= 31 && ev.keyCode <= 90) {
823                 var optNode = opts.selectEl.controller('mdSelectMenu').optNodeForKeyboardSearch(ev);
824                 optNode && optNode.focus();
825               }
826           }
827         });
828
829
830         function focusOption(direction) {
831           var optionsArray = nodesToArray(optionNodes);
832           var index = optionsArray.indexOf(opts.focusedNode);
833           if (index === -1) {
834             // We lost the previously focused element, reset to first option
835             index = 0;
836           } else if (direction === 'next' && index < optionsArray.length - 1) {
837             index++;
838           } else if (direction === 'prev' && index > 0) {
839             index--;
840           }
841           var newOption = opts.focusedNode = optionsArray[index];
842           newOption && newOption.focus();
843         }
844         function focusNextOption() {
845           focusOption('next');
846         }
847         function focusPrevOption() {
848           focusOption('prev');
849         }
850
851         opts.selectEl.on('click', checkCloseMenu);
852         opts.selectEl.on('keydown', function(e) {
853           if (e.keyCode == 32 || e.keyCode == 13) {
854             checkCloseMenu();
855           }
856         });
857
858         function checkCloseMenu() {
859           if (!selectCtrl.isMultiple) {
860             opts.restoreFocus = true;
861             scope.$evalAsync(function() {
862               $mdSelect.hide(selectCtrl.ngModel.$viewValue);
863             });
864           }
865         }
866       }
867
868     }
869
870     function onRemove(scope, element, opts) {
871       opts.isRemoved = true;
872       element.addClass('md-leave')
873         .removeClass('md-clickable');
874       opts.target.attr('aria-expanded', 'false');
875
876
877       angular.element($window).off('resize', opts.resizeFn);
878       angular.element($window).off('orientationchange', opts.resizefn);
879       opts.resizeFn = undefined;
880
881       var mdSelect = opts.selectEl.controller('mdSelect');
882       if (mdSelect) {
883         mdSelect.setLabelText(opts.selectEl.controller('mdSelectMenu').selectedLabels());
884       }
885
886       return $mdUtil.transitionEndPromise(element, { timeout: 350 }).then(function() {
887         element.removeClass('md-active');
888         opts.backdrop && opts.backdrop.remove();
889         if (element[0].parentNode === opts.parent[0]) {
890           opts.parent[0].removeChild(element[0]); // use browser to avoid $destroy event
891         }
892         if (opts.disableParentScroll) {
893           opts.restoreScroll();
894         }
895         if (opts.restoreFocus) opts.target.focus();
896       });
897     }
898
899     function animateSelect(scope, element, opts) {
900       var containerNode = element[0],
901           targetNode = opts.target[0].firstElementChild.firstElementChild, // target the first span, functioning as the label
902           parentNode = opts.parent[0],
903           selectNode = opts.selectEl[0],
904           contentNode = opts.contentEl[0],
905           parentRect = parentNode.getBoundingClientRect(),
906           targetRect = targetNode.getBoundingClientRect(),
907           shouldOpenAroundTarget = false,
908           bounds = {
909             left: parentRect.left + SELECT_EDGE_MARGIN,
910             top: SELECT_EDGE_MARGIN,
911             bottom: parentRect.height - SELECT_EDGE_MARGIN,
912             right: parentRect.width - SELECT_EDGE_MARGIN - ($mdUtil.floatingScrollbars() ? 16 : 0)
913           },
914           spaceAvailable = {
915             top: targetRect.top - bounds.top,
916             left: targetRect.left - bounds.left,
917             right: bounds.right - (targetRect.left + targetRect.width),
918             bottom: bounds.bottom - (targetRect.top + targetRect.height)
919           },
920           maxWidth = parentRect.width - SELECT_EDGE_MARGIN * 2,
921           isScrollable = contentNode.scrollHeight > contentNode.offsetHeight,
922           selectedNode = selectNode.querySelector('md-option[selected]'),
923           optionNodes = selectNode.getElementsByTagName('md-option'),
924           optgroupNodes = selectNode.getElementsByTagName('md-optgroup');
925
926
927       var centeredNode;
928       // If a selected node, center around that
929       if (selectedNode) {
930         centeredNode = selectedNode;
931       // If there are option groups, center around the first option group
932       } else if (optgroupNodes.length) {
933         centeredNode = optgroupNodes[0];
934       // Otherwise, center around the first optionNode
935       } else if (optionNodes.length){
936         centeredNode = optionNodes[0];
937       // In case there are no options, center on whatever's in there... (eg progress indicator)
938       } else {
939         centeredNode = contentNode.firstElementChild || contentNode;
940       }
941
942       if (contentNode.offsetWidth > maxWidth) {
943         contentNode.style['max-width'] = maxWidth + 'px';
944       }
945       if (shouldOpenAroundTarget) {
946         contentNode.style['min-width'] = targetRect.width + 'px';
947       }
948
949       // Remove padding before we compute the position of the menu
950       if (isScrollable) {
951         selectNode.classList.add('md-overflow');
952       }
953
954       // Get the selectMenuRect *after* max-width is possibly set above
955       var selectMenuRect = selectNode.getBoundingClientRect();
956       var centeredRect = getOffsetRect(centeredNode);
957
958       if (centeredNode) {
959         var centeredStyle = $window.getComputedStyle(centeredNode);
960         centeredRect.paddingLeft = parseInt(centeredStyle.paddingLeft, 10) || 0;
961         centeredRect.paddingRight = parseInt(centeredStyle.paddingRight, 10) || 0;
962       }
963
964       var focusedNode = centeredNode;
965       if ((focusedNode.tagName || '').toUpperCase() === 'MD-OPTGROUP') {
966         focusedNode = optionNodes[0] || contentNode.firstElementChild || contentNode;
967       }
968
969       if (isScrollable) {
970         var scrollBuffer = contentNode.offsetHeight / 2;
971         contentNode.scrollTop = centeredRect.top + centeredRect.height / 2 - scrollBuffer;
972
973         if (spaceAvailable.top < scrollBuffer) {
974           contentNode.scrollTop = Math.min(
975             centeredRect.top,
976             contentNode.scrollTop + scrollBuffer - spaceAvailable.top
977           );
978         } else if (spaceAvailable.bottom < scrollBuffer) {
979           contentNode.scrollTop = Math.max(
980             centeredRect.top + centeredRect.height - selectMenuRect.height,
981             contentNode.scrollTop - scrollBuffer + spaceAvailable.bottom
982           );
983         }
984       }
985
986       var left, top, transformOrigin;
987       if (shouldOpenAroundTarget) {
988         left = targetRect.left;
989         top = targetRect.top + targetRect.height;
990         transformOrigin = '50% 0';
991         if (top + selectMenuRect.height > bounds.bottom) {
992           top = targetRect.top - selectMenuRect.height;
993           transformOrigin = '50% 100%';
994         }
995       } else {
996         left = targetRect.left + centeredRect.left - centeredRect.paddingLeft;
997         top = Math.floor(targetRect.top + targetRect.height / 2 - centeredRect.height / 2 -
998           centeredRect.top + contentNode.scrollTop);
999
1000
1001         transformOrigin = (centeredRect.left + targetRect.width / 2) + 'px ' +
1002         (centeredRect.top + centeredRect.height / 2 - contentNode.scrollTop) + 'px 0px';
1003
1004         containerNode.style.minWidth = targetRect.width + centeredRect.paddingLeft +
1005           centeredRect.paddingRight + 'px';
1006       }
1007
1008       // Keep left and top within the window
1009       var containerRect = containerNode.getBoundingClientRect();
1010       containerNode.style.left = clamp(bounds.left, left, bounds.right - containerRect.width) + 'px';
1011       containerNode.style.top = clamp(bounds.top, top, bounds.bottom - containerRect.height) + 'px';
1012       selectNode.style[$mdConstant.CSS.TRANSFORM_ORIGIN] = transformOrigin;
1013
1014       selectNode.style[$mdConstant.CSS.TRANSFORM] = 'scale(' +
1015         Math.min(targetRect.width / selectMenuRect.width, 1.0) + ',' +
1016         Math.min(targetRect.height / selectMenuRect.height, 1.0) +
1017       ')';
1018
1019
1020       $$rAF(function() {
1021         element.addClass('md-active');
1022         selectNode.style[$mdConstant.CSS.TRANSFORM] = '';
1023         if (focusedNode) {
1024           opts.focusedNode = focusedNode;
1025           focusedNode.focus();
1026         }
1027       });
1028     }
1029
1030   }
1031
1032   function clamp(min, n, max) {
1033     return Math.max(min, Math.min(n, max));
1034   }
1035
1036   function getOffsetRect(node) {
1037     return node ? {
1038       left: node.offsetLeft,
1039       top: node.offsetTop,
1040       width: node.offsetWidth,
1041       height: node.offsetHeight
1042     } : { left: 0, top: 0, width: 0, height: 0 };
1043   }
1044 }
1045 SelectProvider.$inject = ["$$interimElementProvider"];
1046
1047 // Annoying method to copy nodes to an array, thanks to IE
1048 function nodesToArray(nodes) {
1049   var results = [];
1050   for (var i = 0; i < nodes.length; ++i) {
1051     results.push(nodes.item(i));
1052   }
1053   return results;
1054 }
1055
1056 ng.material.components.select = angular.module("material.components.select");