2 * Angular Material Design
3 * https://github.com/angular/material
7 goog.provide('ng.material.components.select');
8 goog.require('ng.material.components.backdrop');
9 goog.require('ng.material.core');
12 * @name material.components.select
15 /***************************************************
18 **DOCUMENTATION AND DEMOS**
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
25 ### TODO - POST RC1 ###
26 - [ ] Abstract placement logic in $mdSelect service to $mdMenu service
28 ***************************************************/
30 var SELECT_EDGE_MARGIN = 8;
33 angular.module('material.components.select', [
35 'material.components.backdrop'
37 .directive('mdSelect', SelectDirective)
38 .directive('mdSelectMenu', SelectMenuDirective)
39 .directive('mdOption', OptionDirective)
40 .directive('mdOptgroup', OptgroupDirective)
41 .provider('$mdSelect', SelectProvider);
48 * @module material.components.select
50 * @description Displays a select box, bound to an ng-model.
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.
59 * With a placeholder (label and aria-label are added dynamically)
62 * ng-model="someModel"
63 * placeholder="Select a state">
64 * <md-option ng-value="opt" ng-repeat="opt in neighborhoods2">{{ opt }}</md-option>
68 * With an explicit label
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>
77 function SelectDirective($mdSelect, $mdUtil, $mdTheming, $mdAria, $interpolate, $compile, $parse) {
80 require: ['mdSelect', 'ngModel', '?^form'],
82 controller: function() { } // empty placeholder controller to be initialized in link
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();
89 // If not provided, we automatically make one
90 if (!labelEl.length) {
91 labelEl = angular.element('<md-select-label><span></span></md-select-label>');
93 if (!labelEl[0].firstElementChild) {
94 var spanWrapper = angular.element('<span>');
95 spanWrapper.append(labelEl.contents());
96 labelEl.append(spanWrapper);
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());
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()) );
110 // Add progress spinner for md-options-loading
112 element.find('md-content').prepend(
113 angular.element('<md-progress-circular>')
114 .attr('md-mode', 'indeterminate')
115 .attr('ng-hide', '$$loadingAsyncDone')
122 var autofillClone = angular.element('<select class="md-visually-hidden">');
124 'name': '.' + attr.name,
125 'ng-model': attr.ngModel,
126 'aria-hidden': 'true',
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);
137 element.parent().append(autofillClone);
140 // Use everything that's left inside element.contents() as the contents of the menu
141 var selectTemplate = '<div class="md-select-menu-container">' +
143 (angular.isDefined(attr.multiple) ? 'multiple' : '') + '>' +
145 '</md-select-menu></div>';
147 element.empty().append(labelEl);
149 attr.tabindex = attr.tabindex || '0';
151 return function postLink(scope, element, attr, ctrls) {
155 var mdSelectCtrl = ctrls[0];
156 var ngModel = ctrls[1];
157 var formCtrl = ctrls[2];
159 var labelEl = element.find('md-select-label');
160 var customLabel = labelEl.text().length !== 0;
161 var selectContainer, selectScope, selectMenuCtrl;
166 if (attr.name && formCtrl) {
167 var selectEl = element.parent()[0].querySelector('select[name=".' + attr.name + '"]')
168 formCtrl.$removeControl(angular.element(selectEl).controller());
171 var originalRender = ngModel.$render;
172 ngModel.$render = function() {
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);
185 mdSelectCtrl.setIsPlaceholder = function(val) {
186 val ? labelEl.addClass('md-placeholder') : labelEl.removeClass('md-placeholder');
189 scope.$$postDigest(function() {
194 function setAriaLabel() {
195 var labelText = element.attr('placeholder');
197 labelText = element.find('md-select-label').text();
199 $mdAria.expect(element, 'aria-label', labelText);
202 function syncLabelText() {
203 if (selectContainer) {
204 selectMenuCtrl = selectMenuCtrl || selectContainer.find('md-select-menu').controller('mdSelectMenu');
205 mdSelectCtrl.setLabelText(selectMenuCtrl.selectedLabels());
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
216 element.attr('multiple', 'multiple');
218 element.removeAttr('multiple');
220 if (selectContainer) {
221 selectMenuCtrl.setMultiple(multiple);
222 originalRender = ngModel.$render;
223 ngModel.$render = function() {
227 selectMenuCtrl.refreshViewValue();
233 attr.$observe('disabled', function(disabled) {
234 if (typeof disabled == "string") {
237 // Prevent click event being registered twice
238 if (isDisabled !== undefined && isDisabled === disabled) {
241 isDisabled = disabled;
243 element.attr({'tabindex': -1, 'aria-disabled': 'true'});
244 element.off('click', openSelect);
245 element.off('keydown', handleKeypress);
247 element.attr({'tabindex': attr.tabindex, 'aria-disabled': 'false'});
248 element.on('click', openSelect);
249 element.on('keydown', handleKeypress);
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);
260 'aria-expanded': 'false'
262 if (!element[0].hasAttribute('id')) {
263 ariaAttrs.id = 'select_' + $mdUtil.nextUid();
265 element.attr(ariaAttrs);
267 scope.$on('$destroy', function() {
269 $mdSelect.cancel().then(function() {
270 selectContainer.remove();
273 selectContainer.remove();
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');
289 function handleKeypress(e) {
290 var allowedCodes = [32, 13, 38, 40];
291 if (allowedCodes.indexOf(e.keyCode) != -1 ) {
292 // prevent page scrolling on interaction
296 if (e.keyCode <= 90 && e.keyCode >= 31) {
298 var node = selectMenuCtrl.optNodeForKeyboardSearch(e);
300 var optionCtrl = angular.element(node).controller('mdOption');
301 if (!selectMenuCtrl.isMultiple) {
302 selectMenuCtrl.deselect( Object.keys(selectMenuCtrl.selected)[0] );
304 selectMenuCtrl.select(optionCtrl.hashKey, optionCtrl.value);
305 selectMenuCtrl.refreshViewValue();
311 function openSelect() {
312 scope.$evalAsync(function() {
318 element: selectContainer,
321 loadingAsync: attr.mdOnOpen ? scope.$eval(attr.mdOnOpen) || true : false,
322 }).then(function(selectedText) {
330 SelectDirective.$inject = ["$mdSelect", "$mdUtil", "$mdTheming", "$mdAria", "$interpolate", "$compile", "$parse"];
332 function SelectMenuDirective($parse, $mdUtil, $mdTheming) {
334 SelectMenuController.$inject = ["$scope", "$attrs", "$element"];
337 require: ['mdSelectMenu', '?ngModel'],
338 controller: SelectMenuController,
339 link: { pre: preLink }
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];
349 element.on('click', clickListener);
350 element.on('keypress', keyListener);
351 if (ngModel) selectCtrl.init(ngModel);
354 function configureAria() {
356 'id': 'select_menu_' + $mdUtil.nextUid(),
358 'aria-multiselectable': (selectCtrl.isMultiple ? 'true' : 'false')
362 function keyListener(e) {
363 if (e.keyCode == 13 || e.keyCode == 32) {
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;
373 var optionHashKey = selectCtrl.hashGetter(optionCtrl.value);
374 var isSelected = angular.isDefined(selectCtrl.selected[optionHashKey]);
376 scope.$apply(function() {
377 if (selectCtrl.isMultiple) {
379 selectCtrl.deselect(optionHashKey);
381 selectCtrl.select(optionHashKey, optionCtrl.value);
385 selectCtrl.deselect( Object.keys(selectCtrl.selected)[0] );
386 selectCtrl.select( optionHashKey, optionCtrl.value );
389 selectCtrl.refreshViewValue();
396 function SelectMenuController($scope, $attrs, $element) {
398 self.isMultiple = angular.isDefined($attrs.multiple);
399 // selected is an object with keys matching all of the selected options' hashed values
401 // options is an object with keys matching every option's hash value,
402 // and values matching every option's controller.
405 $scope.$watch(function() { return self.options; }, function() {
406 self.ngModel.$render();
409 var deregisterCollectionWatch;
410 self.setMultiple = function(isMultiple) {
411 var ngModel = self.ngModel;
412 self.isMultiple = isMultiple;
413 if (deregisterCollectionWatch) deregisterCollectionWatch();
415 if (self.isMultiple) {
416 ngModel.$validators['md-multiple'] = validateArray;
417 ngModel.$render = renderMultiple;
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);
425 delete ngModel.$validators['md-multiple'];
426 ngModel.$render = renderSingular;
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 || []);
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;
445 optNodes = undefined;
446 }, CLEAR_SEARCH_AFTER);
447 searchStr += String.fromCharCode(e.keyCode);
448 var search = new RegExp('^' + searchStr, 'i');
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();
456 for (var i = 0; i < optText.length; ++i) {
457 if (search.test(optText[i])) {
464 self.init = function(ngModel) {
465 self.ngModel = ngModel;
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);
476 // If the user doesn't provide a trackBy, we automatically generate an id for every
479 self.hashGetter = function getHashValue(value) {
480 if (angular.isObject(value)) {
481 return 'object_' + (value.$$mdSelectId || (value.$$mdSelectId = ++selectNextId));
486 self.setMultiple(self.isMultiple);
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(', ');
498 self.select = function(hashKey, hashedValue) {
499 var option = self.options[hashKey];
500 option && option.setSelected(true);
501 self.selected[hashKey] = hashedValue;
503 self.deselect = function(hashKey) {
504 var option = self.options[hashKey];
505 option && option.setSelected(false);
506 delete self.selected[hashKey];
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.');
514 self.options[hashKey] = optionCtrl;
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();
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.
528 self.refreshViewValue = function() {
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);
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]);
544 self.ngModel.$setViewValue(self.isMultiple ? values : values[0]);
547 function renderMultiple() {
548 var newSelectedValues = self.ngModel.$modelValue || self.ngModel.$viewValue;
549 if (!angular.isArray(newSelectedValues)) return;
551 var oldSelected = Object.keys(self.selected);
553 var newSelectedHashes = newSelectedValues.map(self.hashGetter);
554 var deselected = oldSelected.filter(function(hash) {
555 return newSelectedHashes.indexOf(hash) === -1;
558 deselected.forEach(self.deselect);
559 newSelectedHashes.forEach(function(hashKey, i) {
560 self.select(hashKey, newSelectedValues[i]);
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 );
571 SelectMenuDirective.$inject = ["$parse", "$mdUtil", "$mdTheming"];
573 function OptionDirective($mdButtonInkRipple, $mdUtil) {
575 OptionController.$inject = ["$element"];
578 require: ['mdOption', '^^mdSelectMenu'],
579 controller: OptionController,
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()) );
587 element.attr('tabindex', attr.tabindex || '0');
591 function postLink(scope, element, attr, ctrls) {
592 var optionCtrl = ctrls[0];
593 var selectCtrl = ctrls[1];
595 if (angular.isDefined(attr.ngValue)) {
596 scope.$watch(attr.ngValue, setOptionValue);
597 } else if (angular.isDefined(attr.value)) {
598 setOptionValue(attr.value);
600 scope.$watch(function() { return element.text(); }, setOptionValue);
603 scope.$$postDigest(function() {
604 attr.$observe('selected', function(selected) {
605 if (!angular.isDefined(selected)) return;
607 if (!selectCtrl.isMultiple) {
608 selectCtrl.deselect( Object.keys(selectCtrl.selected)[0] );
610 selectCtrl.select(optionCtrl.hashKey, optionCtrl.value);
612 selectCtrl.deselect(optionCtrl.hashKey);
614 selectCtrl.refreshViewValue();
615 selectCtrl.ngModel.$render();
619 $mdButtonInkRipple.attach(scope, element);
622 function setOptionValue(newValue, oldValue) {
623 var oldHashKey = selectCtrl.hashGetter(oldValue, scope);
624 var newHashKey = selectCtrl.hashGetter(newValue, scope);
626 optionCtrl.hashKey = newHashKey;
627 optionCtrl.value = newValue;
629 selectCtrl.removeOption(oldHashKey, optionCtrl);
630 selectCtrl.addOption(newHashKey, optionCtrl);
633 scope.$on('$destroy', function() {
634 selectCtrl.removeOption(optionCtrl.hashKey, optionCtrl);
637 function configureAria() {
640 'aria-selected': 'false'
643 if (!element[0].hasAttribute('id')) {
644 ariaAttrs.id = 'select_option_' + $mdUtil.nextUid();
646 element.attr(ariaAttrs);
650 function OptionController($element) {
651 this.selected = false;
652 this.setSelected = function(isSelected) {
653 if (isSelected && !this.selected) {
655 'selected': 'selected',
656 'aria-selected': 'true'
658 } else if (!isSelected && this.selected) {
659 $element.removeAttr('selected');
660 $element.attr('aria-selected', 'false');
662 this.selected = isSelected;
667 OptionDirective.$inject = ["$mdButtonInkRipple", "$mdUtil"];
669 function OptgroupDirective() {
674 function compile(el, attrs) {
675 var labelElement = el.find('label');
676 if (!labelElement.length) {
677 labelElement = angular.element('<label>');
678 el.prepend(labelElement);
680 if (attrs.label) labelElement.text(attrs.label);
684 function SelectProvider($$interimElementProvider) {
685 selectDefaultOptions.$inject = ["$mdSelect", "$mdConstant", "$$rAF", "$mdUtil", "$mdTheming", "$timeout", "$window"];
686 return $$interimElementProvider('$mdSelect')
689 options: selectDefaultOptions
693 function selectDefaultOptions($mdSelect, $mdConstant, $$rAF, $mdUtil, $mdTheming, $timeout, $window ) {
699 disableParentScroll: true,
703 function onShow(scope, element, opts) {
705 throw new Error('$mdSelect.show() expected a target element in options.target but got ' +
706 '"' + opts.target + '"!');
709 angular.extend(opts, {
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">')
718 opts.resizeFn = function() {
721 animateSelect(scope, element, opts);
726 angular.element($window).on('resize', opts.resizeFn);
727 angular.element($window).on('orientationchange', opts.resizeFn);
732 element.removeClass('md-leave');
734 var optionNodes = opts.selectEl[0].getElementsByTagName('md-option');
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.
742 // Don't go forward if the select has been removed in this time...
743 if (opts.isRemoved) return;
744 animateSelect(scope, element, opts);
748 } else if (opts.loadingAsync) {
749 scope.$$loadingAsyncDone = true;
752 if (opts.disableParentScroll && !$mdUtil.getClosest(opts.target, 'MD-DIALOG')) {
753 opts.restoreScroll = $mdUtil.disableScrollAround(opts.target);
755 opts.disableParentScroll = false;
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);
762 $mdTheming.inherit(opts.backdrop, opts.parent);
763 opts.parent.append(opts.backdrop);
765 opts.parent.append(element);
767 // Give the select a frame to 'initialize' in the DOM,
768 // so we can read its height/width/position
771 if (opts.isRemoved) return;
772 animateSelect(scope, element, opts);
776 return $mdUtil.transitionEndPromise(opts.selectEl, {timeout: 350});
778 function configureAria() {
779 opts.target.attr('aria-expanded', 'true');
782 function activateInteraction() {
783 if (opts.isRemoved) return;
784 var selectCtrl = opts.selectEl.controller('mdSelectMenu') || {};
785 element.addClass('md-clickable');
787 opts.backdrop && opts.backdrop.on('click', function(e) {
790 opts.restoreFocus = false;
791 scope.$apply($mdSelect.cancel);
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');
801 opts.selectEl.triggerHandler({
808 case $mdConstant.KEY_CODE.TAB:
809 case $mdConstant.KEY_CODE.ESCAPE:
811 opts.restoreFocus = true;
812 scope.$apply($mdSelect.cancel);
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();
822 if (ev.keyCode >= 31 && ev.keyCode <= 90) {
823 var optNode = opts.selectEl.controller('mdSelectMenu').optNodeForKeyboardSearch(ev);
824 optNode && optNode.focus();
830 function focusOption(direction) {
831 var optionsArray = nodesToArray(optionNodes);
832 var index = optionsArray.indexOf(opts.focusedNode);
834 // We lost the previously focused element, reset to first option
836 } else if (direction === 'next' && index < optionsArray.length - 1) {
838 } else if (direction === 'prev' && index > 0) {
841 var newOption = opts.focusedNode = optionsArray[index];
842 newOption && newOption.focus();
844 function focusNextOption() {
847 function focusPrevOption() {
851 opts.selectEl.on('click', checkCloseMenu);
852 opts.selectEl.on('keydown', function(e) {
853 if (e.keyCode == 32 || e.keyCode == 13) {
858 function checkCloseMenu() {
859 if (!selectCtrl.isMultiple) {
860 opts.restoreFocus = true;
861 scope.$evalAsync(function() {
862 $mdSelect.hide(selectCtrl.ngModel.$viewValue);
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');
877 angular.element($window).off('resize', opts.resizeFn);
878 angular.element($window).off('orientationchange', opts.resizefn);
879 opts.resizeFn = undefined;
881 var mdSelect = opts.selectEl.controller('mdSelect');
883 mdSelect.setLabelText(opts.selectEl.controller('mdSelectMenu').selectedLabels());
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
892 if (opts.disableParentScroll) {
893 opts.restoreScroll();
895 if (opts.restoreFocus) opts.target.focus();
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,
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)
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)
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');
928 // If a selected node, center around that
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)
939 centeredNode = contentNode.firstElementChild || contentNode;
942 if (contentNode.offsetWidth > maxWidth) {
943 contentNode.style['max-width'] = maxWidth + 'px';
945 if (shouldOpenAroundTarget) {
946 contentNode.style['min-width'] = targetRect.width + 'px';
949 // Remove padding before we compute the position of the menu
951 selectNode.classList.add('md-overflow');
954 // Get the selectMenuRect *after* max-width is possibly set above
955 var selectMenuRect = selectNode.getBoundingClientRect();
956 var centeredRect = getOffsetRect(centeredNode);
959 var centeredStyle = $window.getComputedStyle(centeredNode);
960 centeredRect.paddingLeft = parseInt(centeredStyle.paddingLeft, 10) || 0;
961 centeredRect.paddingRight = parseInt(centeredStyle.paddingRight, 10) || 0;
964 var focusedNode = centeredNode;
965 if ((focusedNode.tagName || '').toUpperCase() === 'MD-OPTGROUP') {
966 focusedNode = optionNodes[0] || contentNode.firstElementChild || contentNode;
970 var scrollBuffer = contentNode.offsetHeight / 2;
971 contentNode.scrollTop = centeredRect.top + centeredRect.height / 2 - scrollBuffer;
973 if (spaceAvailable.top < scrollBuffer) {
974 contentNode.scrollTop = Math.min(
976 contentNode.scrollTop + scrollBuffer - spaceAvailable.top
978 } else if (spaceAvailable.bottom < scrollBuffer) {
979 contentNode.scrollTop = Math.max(
980 centeredRect.top + centeredRect.height - selectMenuRect.height,
981 contentNode.scrollTop - scrollBuffer + spaceAvailable.bottom
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%';
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);
1001 transformOrigin = (centeredRect.left + targetRect.width / 2) + 'px ' +
1002 (centeredRect.top + centeredRect.height / 2 - contentNode.scrollTop) + 'px 0px';
1004 containerNode.style.minWidth = targetRect.width + centeredRect.paddingLeft +
1005 centeredRect.paddingRight + 'px';
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;
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) +
1021 element.addClass('md-active');
1022 selectNode.style[$mdConstant.CSS.TRANSFORM] = '';
1024 opts.focusedNode = focusedNode;
1025 focusedNode.focus();
1032 function clamp(min, n, max) {
1033 return Math.max(min, Math.min(n, max));
1036 function getOffsetRect(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 };
1045 SelectProvider.$inject = ["$$interimElementProvider"];
1047 // Annoying method to copy nodes to an array, thanks to IE
1048 function nodesToArray(nodes) {
1050 for (var i = 0; i < nodes.length; ++i) {
1051 results.push(nodes.item(i));
1056 ng.material.components.select = angular.module("material.components.select");