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