| /*! |
| * Angular Material Design |
| * https://github.com/angular/material |
| * @license MIT |
| * v1.0.1 |
| */ |
| goog.provide('ng.material.components.select'); |
| goog.require('ng.material.components.backdrop'); |
| goog.require('ng.material.core'); |
| /** |
| * @ngdoc module |
| * @name material.components.select |
| */ |
| |
| /*************************************************** |
| |
| ### TODO - POST RC1 ### |
| - [ ] Abstract placement logic in $mdSelect service to $mdMenu service |
| |
| ***************************************************/ |
| |
| var SELECT_EDGE_MARGIN = 8; |
| var selectNextId = 0; |
| |
| angular.module('material.components.select', [ |
| 'material.core', |
| 'material.components.backdrop' |
| ]) |
| .directive('mdSelect', SelectDirective) |
| .directive('mdSelectMenu', SelectMenuDirective) |
| .directive('mdOption', OptionDirective) |
| .directive('mdOptgroup', OptgroupDirective) |
| .provider('$mdSelect', SelectProvider); |
| |
| /** |
| * @ngdoc directive |
| * @name mdSelect |
| * @restrict E |
| * @module material.components.select |
| * |
| * @description Displays a select box, bound to an ng-model. |
| * |
| * @param {expression} ng-model The model! |
| * @param {boolean=} multiple Whether it's multiple. |
| * @param {expression=} md-on-close Expression to be evaluated when the select is closed. |
| * @param {string=} placeholder Placeholder hint text. |
| * @param {string=} aria-label Optional label for accessibility. Only necessary if no placeholder or |
| * explicit label is present. |
| * @param {string=} md-container-class Class list to get applied to the `.md-select-menu-container` |
| * element (for custom styling). |
| * |
| * @usage |
| * With a placeholder (label and aria-label are added dynamically) |
| * <hljs lang="html"> |
| * <md-input-container> |
| * <md-select |
| * ng-model="someModel" |
| * placeholder="Select a state"> |
| * <md-option ng-value="opt" ng-repeat="opt in neighborhoods2">{{ opt }}</md-option> |
| * </md-select> |
| * </md-input-container> |
| * </hljs> |
| * |
| * With an explicit label |
| * <hljs lang="html"> |
| * <md-input-container> |
| * <label>State</label> |
| * <md-select |
| * ng-model="someModel"> |
| * <md-option ng-value="opt" ng-repeat="opt in neighborhoods2">{{ opt }}</md-option> |
| * </md-select> |
| * </md-input-container> |
| * </hljs> |
| * |
| * ## Selects and object equality |
| * When using a `md-select` to pick from a list of objects, it is important to realize how javascript handles |
| * equality. Consider the following example: |
| * <hljs lang="js"> |
| * angular.controller('MyCtrl', function($scope) { |
| * $scope.users = [ |
| * { id: 1, name: 'Bob' }, |
| * { id: 2, name: 'Alice' }, |
| * { id: 3, name: 'Steve' } |
| * ]; |
| * $scope.selectedUser = { id: 1, name: 'Bob' }; |
| * }); |
| * </hljs> |
| * <hljs lang="html"> |
| * <div ng-controller="MyCtrl"> |
| * <md-select ng-model="selectedUser"> |
| * <md-option ng-value="user" ng-repeat="user in users">{{ user.name }}</md-option> |
| * </md-select> |
| * </div> |
| * </hljs> |
| * |
| * At first one might expect that the select should be populated with "Bob" as the selected user. However, |
| * this is not true. To determine whether something is selected, |
| * `ngModelController` is looking at whether `$scope.selectedUser == (any user in $scope.users);`; |
| * |
| * Javascript's `==` operator does not check for deep equality (ie. that all properties |
| * on the object are the same), but instead whether the objects are *the same object in memory*. |
| * In this case, we have two instances of identical objects, but they exist in memory as unique |
| * entities. Because of this, the select will have no value populated for a selected user. |
| * |
| * To get around this, `ngModelController` provides a `track by` option that allows us to specify a different |
| * expression which will be used for the equality operator. As such, we can update our `html` to |
| * make use of this by specifying the `ng-model-options="{trackBy: '$value.id'}"` on the `md-select` |
| * element. This converts our equality expression to be |
| * `$scope.selectedUser.id == (any id in $scope.users.map(function(u) { return u.id; }));` |
| * which results in Bob being selected as desired. |
| * |
| * Working HTML: |
| * <hljs lang="html"> |
| * <div ng-controller="MyCtrl"> |
| * <md-select ng-model="selectedUser" ng-model-options="{trackBy: '$value.id'}"> |
| * <md-option ng-value="user" ng-repeat="user in users">{{ user.name }}</md-option> |
| * </md-select> |
| * </div> |
| * </hljs> |
| */ |
| function SelectDirective($mdSelect, $mdUtil, $mdTheming, $mdAria, $compile, $parse) { |
| return { |
| restrict: 'E', |
| require: ['^?mdInputContainer', 'mdSelect', 'ngModel', '?^form'], |
| compile: compile, |
| controller: function() { |
| } // empty placeholder controller to be initialized in link |
| }; |
| |
| function compile(element, attr) { |
| // add the select value that will hold our placeholder or selected option value |
| var valueEl = angular.element('<md-select-value><span></span></md-select-value>'); |
| valueEl.append('<span class="md-select-icon" aria-hidden="true"></span>'); |
| valueEl.addClass('md-select-value'); |
| if (!valueEl[0].hasAttribute('id')) { |
| valueEl.attr('id', 'select_value_label_' + $mdUtil.nextUid()); |
| } |
| |
| // There's got to be an md-content inside. If there's not one, let's add it. |
| if (!element.find('md-content').length) { |
| element.append(angular.element('<md-content>').append(element.contents())); |
| } |
| |
| |
| // Add progress spinner for md-options-loading |
| if (attr.mdOnOpen) { |
| |
| // Show progress indicator while loading async |
| // Use ng-hide for `display:none` so the indicator does not interfere with the options list |
| element |
| .find('md-content') |
| .prepend(angular.element( |
| '<div>' + |
| ' <md-progress-circular md-mode="{{progressMode}}" ng-hide="$$loadingAsyncDone"></md-progress-circular>' + |
| '</div>' |
| )); |
| |
| // Hide list [of item options] while loading async |
| element |
| .find('md-option') |
| .attr('ng-show', '$$loadingAsyncDone'); |
| } |
| |
| if (attr.name) { |
| var autofillClone = angular.element('<select class="md-visually-hidden">'); |
| autofillClone.attr({ |
| 'name': '.' + attr.name, |
| 'ng-model': attr.ngModel, |
| 'aria-hidden': 'true', |
| 'tabindex': '-1' |
| }); |
| var opts = element.find('md-option'); |
| angular.forEach(opts, function(el) { |
| var newEl = angular.element('<option>' + el.innerHTML + '</option>'); |
| if (el.hasAttribute('ng-value')) newEl.attr('ng-value', el.getAttribute('ng-value')); |
| else if (el.hasAttribute('value')) newEl.attr('value', el.getAttribute('value')); |
| autofillClone.append(newEl); |
| }); |
| |
| element.parent().append(autofillClone); |
| } |
| |
| // Use everything that's left inside element.contents() as the contents of the menu |
| var multiple = angular.isDefined(attr.multiple) ? 'multiple' : ''; |
| var selectTemplate = '' + |
| '<div class="md-select-menu-container" aria-hidden="true">' + |
| '<md-select-menu {0}>{1}</md-select-menu>' + |
| '</div>'; |
| |
| selectTemplate = $mdUtil.supplant(selectTemplate, [multiple, element.html()]); |
| element.empty().append(valueEl); |
| element.append(selectTemplate); |
| |
| attr.tabindex = attr.tabindex || '0'; |
| |
| return function postLink(scope, element, attr, ctrls) { |
| var untouched = true; |
| var isDisabled, ariaLabelBase; |
| |
| var containerCtrl = ctrls[0]; |
| var mdSelectCtrl = ctrls[1]; |
| var ngModelCtrl = ctrls[2]; |
| var formCtrl = ctrls[3]; |
| // grab a reference to the select menu value label |
| var valueEl = element.find('md-select-value'); |
| var isReadonly = angular.isDefined(attr.readonly); |
| |
| if (containerCtrl) { |
| var isErrorGetter = containerCtrl.isErrorGetter || function() { |
| return ngModelCtrl.$invalid && ngModelCtrl.$touched; |
| }; |
| |
| if (containerCtrl.input) { |
| throw new Error("<md-input-container> can only have *one* child <input>, <textarea> or <select> element!"); |
| } |
| |
| containerCtrl.input = element; |
| if (!containerCtrl.label) { |
| $mdAria.expect(element, 'aria-label', element.attr('placeholder')); |
| } |
| |
| scope.$watch(isErrorGetter, containerCtrl.setInvalid); |
| } |
| |
| var selectContainer, selectScope, selectMenuCtrl; |
| |
| findSelectContainer(); |
| $mdTheming(element); |
| |
| if (attr.name && formCtrl) { |
| var selectEl = element.parent()[0].querySelector('select[name=".' + attr.name + '"]'); |
| $mdUtil.nextTick(function() { |
| var controller = angular.element(selectEl).controller('ngModel'); |
| if (controller) { |
| formCtrl.$removeControl(controller); |
| } |
| }); |
| } |
| |
| if (formCtrl) { |
| $mdUtil.nextTick(function() { |
| formCtrl.$setPristine(); |
| }); |
| } |
| |
| var originalRender = ngModelCtrl.$render; |
| ngModelCtrl.$render = function() { |
| originalRender(); |
| syncLabelText(); |
| syncAriaLabel(); |
| inputCheckValue(); |
| }; |
| |
| |
| attr.$observe('placeholder', ngModelCtrl.$render); |
| |
| |
| mdSelectCtrl.setLabelText = function(text) { |
| mdSelectCtrl.setIsPlaceholder(!text); |
| // Use placeholder attribute, otherwise fallback to the md-input-container label |
| var tmpPlaceholder = attr.placeholder || (containerCtrl && containerCtrl.label ? containerCtrl.label.text() : ''); |
| text = text || tmpPlaceholder || ''; |
| var target = valueEl.children().eq(0); |
| target.html(text); |
| }; |
| |
| mdSelectCtrl.setIsPlaceholder = function(isPlaceholder) { |
| if (isPlaceholder) { |
| valueEl.addClass('md-select-placeholder'); |
| if (containerCtrl && containerCtrl.label) { |
| containerCtrl.label.addClass('md-placeholder'); |
| } |
| } else { |
| valueEl.removeClass('md-select-placeholder'); |
| if (containerCtrl && containerCtrl.label) { |
| containerCtrl.label.removeClass('md-placeholder'); |
| } |
| } |
| }; |
| |
| if (!isReadonly) { |
| element |
| .on('focus', function(ev) { |
| // only set focus on if we don't currently have a selected value. This avoids the "bounce" |
| // on the label transition because the focus will immediately switch to the open menu. |
| if (containerCtrl && containerCtrl.element.hasClass('md-input-has-value')) { |
| containerCtrl.setFocused(true); |
| } |
| }); |
| |
| // Wait until postDigest so that we attach after ngModel's |
| // blur listener so we can set untouched. |
| $mdUtil.nextTick(function () { |
| element.on('blur', function() { |
| if (untouched) { |
| untouched = false; |
| ngModelCtrl.$setUntouched(); |
| } |
| |
| if (selectScope.isOpen) return; |
| containerCtrl && containerCtrl.setFocused(false); |
| inputCheckValue(); |
| }); |
| }); |
| } |
| |
| mdSelectCtrl.triggerClose = function() { |
| $parse(attr.mdOnClose)(scope); |
| }; |
| |
| scope.$$postDigest(function() { |
| initAriaLabel(); |
| syncLabelText(); |
| syncAriaLabel(); |
| }); |
| |
| function initAriaLabel() { |
| var labelText = element.attr('aria-label') || element.attr('placeholder'); |
| if (!labelText && containerCtrl && containerCtrl.label) { |
| labelText = containerCtrl.label.text(); |
| } |
| ariaLabelBase = labelText; |
| $mdAria.expect(element, 'aria-label', labelText); |
| } |
| |
| scope.$watch(selectMenuCtrl.selectedLabels, syncLabelText); |
| |
| function syncLabelText() { |
| if (selectContainer) { |
| selectMenuCtrl = selectMenuCtrl || selectContainer.find('md-select-menu').controller('mdSelectMenu'); |
| mdSelectCtrl.setLabelText(selectMenuCtrl.selectedLabels()); |
| } |
| } |
| |
| function syncAriaLabel() { |
| if (!ariaLabelBase) return; |
| var ariaLabels = selectMenuCtrl.selectedLabels({mode: 'aria'}); |
| element.attr('aria-label', ariaLabels.length ? ariaLabelBase + ': ' + ariaLabels : ariaLabelBase); |
| } |
| |
| var deregisterWatcher; |
| attr.$observe('ngMultiple', function(val) { |
| if (deregisterWatcher) deregisterWatcher(); |
| var parser = $parse(val); |
| deregisterWatcher = scope.$watch(function() { |
| return parser(scope); |
| }, function(multiple, prevVal) { |
| if (multiple === undefined && prevVal === undefined) return; // assume compiler did a good job |
| if (multiple) { |
| element.attr('multiple', 'multiple'); |
| } else { |
| element.removeAttr('multiple'); |
| } |
| element.attr('aria-multiselectable', multiple ? 'true' : 'false'); |
| if (selectContainer) { |
| selectMenuCtrl.setMultiple(multiple); |
| originalRender = ngModelCtrl.$render; |
| ngModelCtrl.$render = function() { |
| originalRender(); |
| syncLabelText(); |
| syncAriaLabel(); |
| inputCheckValue(); |
| }; |
| ngModelCtrl.$render(); |
| } |
| }); |
| }); |
| |
| attr.$observe('disabled', function(disabled) { |
| if (angular.isString(disabled)) { |
| disabled = true; |
| } |
| // Prevent click event being registered twice |
| if (isDisabled !== undefined && isDisabled === disabled) { |
| return; |
| } |
| isDisabled = disabled; |
| if (disabled) { |
| element.attr({'tabindex': -1, 'aria-disabled': 'true'}); |
| element.off('click', openSelect); |
| element.off('keydown', handleKeypress); |
| } else { |
| element.attr({'tabindex': attr.tabindex, 'aria-disabled': 'false'}); |
| element.on('click', openSelect); |
| element.on('keydown', handleKeypress); |
| } |
| }); |
| |
| if (!attr.disabled && !attr.ngDisabled) { |
| element.attr({'tabindex': attr.tabindex, 'aria-disabled': 'false'}); |
| element.on('click', openSelect); |
| element.on('keydown', handleKeypress); |
| } |
| |
| |
| var ariaAttrs = { |
| role: 'listbox', |
| 'aria-expanded': 'false', |
| 'aria-multiselectable': attr.multiple !== undefined && !attr.ngMultiple ? 'true' : 'false' |
| }; |
| |
| if (!element[0].hasAttribute('id')) { |
| ariaAttrs.id = 'select_' + $mdUtil.nextUid(); |
| } |
| |
| var containerId = 'select_container_' + $mdUtil.nextUid(); |
| selectContainer.attr('id', containerId); |
| ariaAttrs['aria-owns'] = containerId; |
| element.attr(ariaAttrs); |
| |
| scope.$on('$destroy', function() { |
| $mdSelect |
| .destroy() |
| .finally(function() { |
| if (containerCtrl) { |
| containerCtrl.setFocused(false); |
| containerCtrl.setHasValue(false); |
| containerCtrl.input = null; |
| } |
| ngModelCtrl.$setTouched(); |
| }); |
| }); |
| |
| |
| |
| function inputCheckValue() { |
| // The select counts as having a value if one or more options are selected, |
| // or if the input's validity state says it has bad input (eg string in a number input) |
| containerCtrl && containerCtrl.setHasValue(selectMenuCtrl.selectedLabels().length > 0 || (element[0].validity || {}).badInput); |
| } |
| |
| function findSelectContainer() { |
| selectContainer = angular.element( |
| element[0].querySelector('.md-select-menu-container') |
| ); |
| selectScope = scope; |
| if (element.attr('md-container-class')) { |
| var value = selectContainer[0].getAttribute('class') + ' ' + element.attr('md-container-class'); |
| selectContainer[0].setAttribute('class', value); |
| } |
| selectMenuCtrl = selectContainer.find('md-select-menu').controller('mdSelectMenu'); |
| selectMenuCtrl.init(ngModelCtrl, attr.ngModel); |
| element.on('$destroy', function() { |
| selectContainer.remove(); |
| }); |
| } |
| |
| function handleKeypress(e) { |
| var allowedCodes = [32, 13, 38, 40]; |
| if (allowedCodes.indexOf(e.keyCode) != -1) { |
| // prevent page scrolling on interaction |
| e.preventDefault(); |
| openSelect(e); |
| } else { |
| if (e.keyCode <= 90 && e.keyCode >= 31) { |
| e.preventDefault(); |
| var node = selectMenuCtrl.optNodeForKeyboardSearch(e); |
| if (!node) return; |
| var optionCtrl = angular.element(node).controller('mdOption'); |
| if (!selectMenuCtrl.isMultiple) { |
| selectMenuCtrl.deselect(Object.keys(selectMenuCtrl.selected)[0]); |
| } |
| selectMenuCtrl.select(optionCtrl.hashKey, optionCtrl.value); |
| selectMenuCtrl.refreshViewValue(); |
| } |
| } |
| } |
| |
| function openSelect() { |
| selectScope.isOpen = true; |
| element.attr('aria-expanded', 'true'); |
| |
| $mdSelect.show({ |
| scope: selectScope, |
| preserveScope: true, |
| skipCompile: true, |
| element: selectContainer, |
| target: element[0], |
| selectCtrl: mdSelectCtrl, |
| preserveElement: true, |
| hasBackdrop: true, |
| loadingAsync: attr.mdOnOpen ? scope.$eval(attr.mdOnOpen) || true : false |
| }).finally(function() { |
| selectScope.isOpen = false; |
| element.focus(); |
| element.attr('aria-expanded', 'false'); |
| ngModelCtrl.$setTouched(); |
| }); |
| } |
| }; |
| } |
| } |
| SelectDirective.$inject = ["$mdSelect", "$mdUtil", "$mdTheming", "$mdAria", "$compile", "$parse"]; |
| |
| function SelectMenuDirective($parse, $mdUtil, $mdTheming) { |
| |
| SelectMenuController.$inject = ["$scope", "$attrs", "$element"]; |
| return { |
| restrict: 'E', |
| require: ['mdSelectMenu'], |
| scope: true, |
| controller: SelectMenuController, |
| link: {pre: preLink} |
| }; |
| |
| // We use preLink instead of postLink to ensure that the select is initialized before |
| // its child options run postLink. |
| function preLink(scope, element, attr, ctrls) { |
| var selectCtrl = ctrls[0]; |
| |
| $mdTheming(element); |
| element.on('click', clickListener); |
| element.on('keypress', keyListener); |
| |
| function keyListener(e) { |
| if (e.keyCode == 13 || e.keyCode == 32) { |
| clickListener(e); |
| } |
| } |
| |
| function clickListener(ev) { |
| var option = $mdUtil.getClosest(ev.target, 'md-option'); |
| var optionCtrl = option && angular.element(option).data('$mdOptionController'); |
| if (!option || !optionCtrl) return; |
| if (option.hasAttribute('disabled')) { |
| ev.stopImmediatePropagation(); |
| return false; |
| } |
| |
| var optionHashKey = selectCtrl.hashGetter(optionCtrl.value); |
| var isSelected = angular.isDefined(selectCtrl.selected[optionHashKey]); |
| |
| scope.$apply(function() { |
| if (selectCtrl.isMultiple) { |
| if (isSelected) { |
| selectCtrl.deselect(optionHashKey); |
| } else { |
| selectCtrl.select(optionHashKey, optionCtrl.value); |
| } |
| } else { |
| if (!isSelected) { |
| selectCtrl.deselect(Object.keys(selectCtrl.selected)[0]); |
| selectCtrl.select(optionHashKey, optionCtrl.value); |
| } |
| } |
| selectCtrl.refreshViewValue(); |
| }); |
| } |
| } |
| |
| function SelectMenuController($scope, $attrs, $element) { |
| var self = this; |
| self.isMultiple = angular.isDefined($attrs.multiple); |
| // selected is an object with keys matching all of the selected options' hashed values |
| self.selected = {}; |
| // options is an object with keys matching every option's hash value, |
| // and values matching every option's controller. |
| self.options = {}; |
| |
| $scope.$watchCollection(function() { |
| return self.options; |
| }, function() { |
| self.ngModel.$render(); |
| }); |
| |
| var deregisterCollectionWatch; |
| var defaultIsEmpty; |
| self.setMultiple = function(isMultiple) { |
| var ngModel = self.ngModel; |
| defaultIsEmpty = defaultIsEmpty || ngModel.$isEmpty; |
| |
| self.isMultiple = isMultiple; |
| if (deregisterCollectionWatch) deregisterCollectionWatch(); |
| |
| if (self.isMultiple) { |
| ngModel.$validators['md-multiple'] = validateArray; |
| ngModel.$render = renderMultiple; |
| |
| // watchCollection on the model because by default ngModel only watches the model's |
| // reference. This allowed the developer to also push and pop from their array. |
| $scope.$watchCollection(self.modelBinding, function(value) { |
| if (validateArray(value)) renderMultiple(value); |
| self.ngModel.$setPristine(); |
| }); |
| |
| ngModel.$isEmpty = function(value) { |
| return !value || value.length === 0; |
| }; |
| } else { |
| delete ngModel.$validators['md-multiple']; |
| ngModel.$render = renderSingular; |
| } |
| |
| function validateArray(modelValue, viewValue) { |
| // If a value is truthy but not an array, reject it. |
| // If value is undefined/falsy, accept that it's an empty array. |
| return angular.isArray(modelValue || viewValue || []); |
| } |
| }; |
| |
| var searchStr = ''; |
| var clearSearchTimeout, optNodes, optText; |
| var CLEAR_SEARCH_AFTER = 300; |
| self.optNodeForKeyboardSearch = function(e) { |
| clearSearchTimeout && clearTimeout(clearSearchTimeout); |
| clearSearchTimeout = setTimeout(function() { |
| clearSearchTimeout = undefined; |
| searchStr = ''; |
| optText = undefined; |
| optNodes = undefined; |
| }, CLEAR_SEARCH_AFTER); |
| searchStr += String.fromCharCode(e.keyCode); |
| var search = new RegExp('^' + searchStr, 'i'); |
| if (!optNodes) { |
| optNodes = $element.find('md-option'); |
| optText = new Array(optNodes.length); |
| angular.forEach(optNodes, function(el, i) { |
| optText[i] = el.textContent.trim(); |
| }); |
| } |
| for (var i = 0; i < optText.length; ++i) { |
| if (search.test(optText[i])) { |
| return optNodes[i]; |
| } |
| } |
| }; |
| |
| self.init = function(ngModel, binding) { |
| self.ngModel = ngModel; |
| self.modelBinding = binding; |
| |
| // Allow users to provide `ng-model="foo" ng-model-options="{trackBy: 'foo.id'}"` so |
| // that we can properly compare objects set on the model to the available options |
| if (ngModel.$options && ngModel.$options.trackBy) { |
| var trackByLocals = {}; |
| var trackByParsed = $parse(ngModel.$options.trackBy); |
| self.hashGetter = function(value, valueScope) { |
| trackByLocals.$value = value; |
| return trackByParsed(valueScope || $scope, trackByLocals); |
| }; |
| // If the user doesn't provide a trackBy, we automatically generate an id for every |
| // value passed in |
| } else { |
| self.hashGetter = function getHashValue(value) { |
| if (angular.isObject(value)) { |
| return 'object_' + (value.$$mdSelectId || (value.$$mdSelectId = ++selectNextId)); |
| } |
| return value; |
| }; |
| } |
| self.setMultiple(self.isMultiple); |
| }; |
| |
| self.selectedLabels = function(opts) { |
| opts = opts || {}; |
| var mode = opts.mode || 'html'; |
| var selectedOptionEls = $mdUtil.nodesToArray($element[0].querySelectorAll('md-option[selected]')); |
| if (selectedOptionEls.length) { |
| var mapFn; |
| |
| if (mode == 'html') { |
| mapFn = function(el) { return el.innerHTML; }; |
| } else if (mode == 'aria') { |
| mapFn = function(el) { return el.hasAttribute('aria-label') ? el.getAttribute('aria-label') : el.textContent; }; |
| } |
| return selectedOptionEls.map(mapFn).join(', '); |
| } else { |
| return ''; |
| } |
| }; |
| |
| self.select = function(hashKey, hashedValue) { |
| var option = self.options[hashKey]; |
| option && option.setSelected(true); |
| self.selected[hashKey] = hashedValue; |
| }; |
| self.deselect = function(hashKey) { |
| var option = self.options[hashKey]; |
| option && option.setSelected(false); |
| delete self.selected[hashKey]; |
| }; |
| |
| self.addOption = function(hashKey, optionCtrl) { |
| if (angular.isDefined(self.options[hashKey])) { |
| throw new Error('Duplicate md-option values are not allowed in a select. ' + |
| 'Duplicate value "' + optionCtrl.value + '" found.'); |
| } |
| self.options[hashKey] = optionCtrl; |
| |
| // If this option's value was already in our ngModel, go ahead and select it. |
| if (angular.isDefined(self.selected[hashKey])) { |
| self.select(hashKey, optionCtrl.value); |
| self.refreshViewValue(); |
| } |
| }; |
| self.removeOption = function(hashKey) { |
| delete self.options[hashKey]; |
| // Don't deselect an option when it's removed - the user's ngModel should be allowed |
| // to have values that do not match a currently available option. |
| }; |
| |
| self.refreshViewValue = function() { |
| var values = []; |
| var option; |
| for (var hashKey in self.selected) { |
| // If this hashKey has an associated option, push that option's value to the model. |
| if ((option = self.options[hashKey])) { |
| values.push(option.value); |
| } else { |
| // Otherwise, the given hashKey has no associated option, and we got it |
| // from an ngModel value at an earlier time. Push the unhashed value of |
| // this hashKey to the model. |
| // This allows the developer to put a value in the model that doesn't yet have |
| // an associated option. |
| values.push(self.selected[hashKey]); |
| } |
| } |
| var usingTrackBy = self.ngModel.$options && self.ngModel.$options.trackBy; |
| |
| var newVal = self.isMultiple ? values : values[0]; |
| var prevVal = self.ngModel.$modelValue; |
| |
| if (usingTrackBy ? !angular.equals(prevVal, newVal) : prevVal != newVal) { |
| self.ngModel.$setViewValue(newVal); |
| self.ngModel.$render(); |
| } |
| }; |
| |
| function renderMultiple() { |
| var newSelectedValues = self.ngModel.$modelValue || self.ngModel.$viewValue || []; |
| if (!angular.isArray(newSelectedValues)) return; |
| |
| var oldSelected = Object.keys(self.selected); |
| |
| var newSelectedHashes = newSelectedValues.map(self.hashGetter); |
| var deselected = oldSelected.filter(function(hash) { |
| return newSelectedHashes.indexOf(hash) === -1; |
| }); |
| |
| deselected.forEach(self.deselect); |
| newSelectedHashes.forEach(function(hashKey, i) { |
| self.select(hashKey, newSelectedValues[i]); |
| }); |
| } |
| |
| function renderSingular() { |
| var value = self.ngModel.$viewValue || self.ngModel.$modelValue; |
| Object.keys(self.selected).forEach(self.deselect); |
| self.select(self.hashGetter(value), value); |
| } |
| } |
| |
| } |
| SelectMenuDirective.$inject = ["$parse", "$mdUtil", "$mdTheming"]; |
| |
| function OptionDirective($mdButtonInkRipple, $mdUtil) { |
| |
| OptionController.$inject = ["$element"]; |
| return { |
| restrict: 'E', |
| require: ['mdOption', '^^mdSelectMenu'], |
| controller: OptionController, |
| compile: compile |
| }; |
| |
| function compile(element, attr) { |
| // Manual transclusion to avoid the extra inner <span> that ng-transclude generates |
| element.append(angular.element('<div class="md-text">').append(element.contents())); |
| |
| element.attr('tabindex', attr.tabindex || '0'); |
| return postLink; |
| } |
| |
| function postLink(scope, element, attr, ctrls) { |
| var optionCtrl = ctrls[0]; |
| var selectCtrl = ctrls[1]; |
| |
| if (angular.isDefined(attr.ngValue)) { |
| scope.$watch(attr.ngValue, setOptionValue); |
| } else if (angular.isDefined(attr.value)) { |
| setOptionValue(attr.value); |
| } else { |
| scope.$watch(function() { |
| return element.text(); |
| }, setOptionValue); |
| } |
| |
| attr.$observe('disabled', function(disabled) { |
| if (disabled) { |
| element.attr('tabindex', '-1'); |
| } else { |
| element.attr('tabindex', '0'); |
| } |
| }); |
| |
| scope.$$postDigest(function() { |
| attr.$observe('selected', function(selected) { |
| if (!angular.isDefined(selected)) return; |
| if (typeof selected == 'string') selected = true; |
| if (selected) { |
| if (!selectCtrl.isMultiple) { |
| selectCtrl.deselect(Object.keys(selectCtrl.selected)[0]); |
| } |
| selectCtrl.select(optionCtrl.hashKey, optionCtrl.value); |
| } else { |
| selectCtrl.deselect(optionCtrl.hashKey); |
| } |
| selectCtrl.refreshViewValue(); |
| }); |
| }); |
| |
| $mdButtonInkRipple.attach(scope, element); |
| configureAria(); |
| |
| function setOptionValue(newValue, oldValue, prevAttempt) { |
| if (!selectCtrl.hashGetter) { |
| if (!prevAttempt) { |
| scope.$$postDigest(function() { |
| setOptionValue(newValue, oldValue, true); |
| }); |
| } |
| return; |
| } |
| var oldHashKey = selectCtrl.hashGetter(oldValue, scope); |
| var newHashKey = selectCtrl.hashGetter(newValue, scope); |
| |
| optionCtrl.hashKey = newHashKey; |
| optionCtrl.value = newValue; |
| |
| selectCtrl.removeOption(oldHashKey, optionCtrl); |
| selectCtrl.addOption(newHashKey, optionCtrl); |
| } |
| |
| scope.$on('$destroy', function() { |
| selectCtrl.removeOption(optionCtrl.hashKey, optionCtrl); |
| }); |
| |
| function configureAria() { |
| var ariaAttrs = { |
| 'role': 'option', |
| 'aria-selected': 'false' |
| }; |
| |
| if (!element[0].hasAttribute('id')) { |
| ariaAttrs.id = 'select_option_' + $mdUtil.nextUid(); |
| } |
| element.attr(ariaAttrs); |
| } |
| } |
| |
| function OptionController($element) { |
| this.selected = false; |
| this.setSelected = function(isSelected) { |
| if (isSelected && !this.selected) { |
| $element.attr({ |
| 'selected': 'selected', |
| 'aria-selected': 'true' |
| }); |
| } else if (!isSelected && this.selected) { |
| $element.removeAttr('selected'); |
| $element.attr('aria-selected', 'false'); |
| } |
| this.selected = isSelected; |
| }; |
| } |
| |
| } |
| OptionDirective.$inject = ["$mdButtonInkRipple", "$mdUtil"]; |
| |
| function OptgroupDirective() { |
| return { |
| restrict: 'E', |
| compile: compile |
| }; |
| function compile(el, attrs) { |
| var labelElement = el.find('label'); |
| if (!labelElement.length) { |
| labelElement = angular.element('<label>'); |
| el.prepend(labelElement); |
| } |
| labelElement.addClass('md-container-ignore'); |
| if (attrs.label) labelElement.text(attrs.label); |
| } |
| } |
| |
| function SelectProvider($$interimElementProvider) { |
| selectDefaultOptions.$inject = ["$mdSelect", "$mdConstant", "$mdUtil", "$window", "$q", "$$rAF", "$animateCss", "$animate", "$document"]; |
| return $$interimElementProvider('$mdSelect') |
| .setDefaults({ |
| methods: ['target'], |
| options: selectDefaultOptions |
| }); |
| |
| /* ngInject */ |
| function selectDefaultOptions($mdSelect, $mdConstant, $mdUtil, $window, $q, $$rAF, $animateCss, $animate, $document) { |
| var ERRROR_TARGET_EXPECTED = "$mdSelect.show() expected a target element in options.target but got '{0}'!"; |
| var animator = $mdUtil.dom.animator; |
| |
| return { |
| parent: 'body', |
| themable: true, |
| onShow: onShow, |
| onRemove: onRemove, |
| hasBackdrop: true, |
| disableParentScroll: true |
| }; |
| |
| /** |
| * Interim-element onRemove logic.... |
| */ |
| function onRemove(scope, element, opts) { |
| opts = opts || { }; |
| opts.cleanupInteraction(); |
| opts.cleanupResizing(); |
| opts.hideBackdrop(); |
| |
| // For navigation $destroy events, do a quick, non-animated removal, |
| // but for normal closes (from clicks, etc) animate the removal |
| |
| return (opts.$destroy === true) ? cleanElement() : animateRemoval().then( cleanElement ); |
| |
| /** |
| * For normal closes (eg clicks), animate the removal. |
| * For forced closes (like $destroy events from navigation), |
| * skip the animations |
| */ |
| function animateRemoval() { |
| return $animateCss(element, {addClass: 'md-leave'}).start(); |
| } |
| |
| /** |
| * Restore the element to a closed state |
| */ |
| function cleanElement() { |
| |
| element.removeClass('md-active'); |
| element.attr('aria-hidden', 'true'); |
| element[0].style.display = 'none'; |
| |
| announceClosed(opts); |
| |
| if (!opts.$destroy && opts.restoreFocus) { |
| opts.target.focus(); |
| } |
| } |
| |
| } |
| |
| /** |
| * Interim-element onShow logic.... |
| */ |
| function onShow(scope, element, opts) { |
| |
| watchAsyncLoad(); |
| sanitizeAndConfigure(scope, opts); |
| |
| opts.hideBackdrop = showBackdrop(scope, element, opts); |
| |
| return showDropDown(scope, element, opts) |
| .then(function(response) { |
| element.attr('aria-hidden', 'false'); |
| opts.alreadyOpen = true; |
| opts.cleanupInteraction = activateInteraction(); |
| opts.cleanupResizing = activateResizing(); |
| |
| return response; |
| }, opts.hideBackdrop); |
| |
| // ************************************ |
| // Closure Functions |
| // ************************************ |
| |
| /** |
| * Attach the select DOM element(s) and animate to the correct positions |
| * and scalings... |
| */ |
| function showDropDown(scope, element, opts) { |
| opts.parent.append(element); |
| |
| return $q(function(resolve, reject) { |
| |
| try { |
| |
| $animateCss(element, {removeClass: 'md-leave', duration: 0}) |
| .start() |
| .then(positionAndFocusMenu) |
| .then(resolve); |
| |
| } catch (e) { |
| reject(e); |
| } |
| |
| }); |
| } |
| |
| /** |
| * Initialize container and dropDown menu positions/scale, then animate |
| * to show... and autoFocus. |
| */ |
| function positionAndFocusMenu() { |
| return $q(function(resolve) { |
| if (opts.isRemoved) return $q.reject(false); |
| |
| var info = calculateMenuPositions(scope, element, opts); |
| |
| info.container.element.css(animator.toCss(info.container.styles)); |
| info.dropDown.element.css(animator.toCss(info.dropDown.styles)); |
| |
| $$rAF(function() { |
| element.addClass('md-active'); |
| info.dropDown.element.css(animator.toCss({transform: ''})); |
| |
| autoFocus(opts.focusedNode); |
| resolve(); |
| }); |
| |
| }); |
| } |
| |
| /** |
| * Show modal backdrop element... |
| */ |
| function showBackdrop(scope, element, options) { |
| |
| // If we are not within a dialog... |
| if (options.disableParentScroll && !$mdUtil.getClosest(options.target, 'MD-DIALOG')) { |
| // !! DO this before creating the backdrop; since disableScrollAround() |
| // configures the scroll offset; which is used by mdBackDrop postLink() |
| options.restoreScroll = $mdUtil.disableScrollAround(options.element, options.parent); |
| } else { |
| options.disableParentScroll = false; |
| } |
| |
| if (options.hasBackdrop) { |
| // Override duration to immediately show invisible backdrop |
| options.backdrop = $mdUtil.createBackdrop(scope, "md-select-backdrop md-click-catcher"); |
| $animate.enter(options.backdrop, $document[0].body, null, {duration: 0}); |
| } |
| |
| /** |
| * Hide modal backdrop element... |
| */ |
| return function hideBackdrop() { |
| if (options.backdrop) options.backdrop.remove(); |
| if (options.disableParentScroll) options.restoreScroll(); |
| |
| delete options.restoreScroll; |
| }; |
| } |
| |
| /** |
| * |
| */ |
| function autoFocus(focusedNode) { |
| if (focusedNode && !focusedNode.hasAttribute('disabled')) { |
| focusedNode.focus(); |
| } |
| } |
| |
| /** |
| * Check for valid opts and set some sane defaults |
| */ |
| function sanitizeAndConfigure(scope, options) { |
| var selectEl = element.find('md-select-menu'); |
| |
| if (!options.target) { |
| throw new Error($mdUtil.supplant(ERRROR_TARGET_EXPECTED, [options.target])); |
| } |
| |
| angular.extend(options, { |
| isRemoved: false, |
| target: angular.element(options.target), //make sure it's not a naked dom node |
| parent: angular.element(options.parent), |
| selectEl: selectEl, |
| contentEl: element.find('md-content'), |
| optionNodes: selectEl[0].getElementsByTagName('md-option') |
| }); |
| } |
| |
| /** |
| * Configure various resize listeners for screen changes |
| */ |
| function activateResizing() { |
| var debouncedOnResize = (function(scope, target, options) { |
| |
| return function() { |
| if (options.isRemoved) return; |
| |
| var updates = calculateMenuPositions(scope, target, options); |
| var container = updates.container; |
| var dropDown = updates.dropDown; |
| |
| container.element.css(animator.toCss(container.styles)); |
| dropDown.element.css(animator.toCss(dropDown.styles)); |
| }; |
| |
| })(scope, element, opts); |
| |
| var window = angular.element($window); |
| window.on('resize', debouncedOnResize); |
| window.on('orientationchange', debouncedOnResize); |
| |
| // Publish deactivation closure... |
| return function deactivateResizing() { |
| |
| // Disable resizing handlers |
| window.off('resize', debouncedOnResize); |
| window.off('orientationchange', debouncedOnResize); |
| }; |
| } |
| |
| /** |
| * If asynchronously loading, watch and update internal |
| * '$$loadingAsyncDone' flag |
| */ |
| function watchAsyncLoad() { |
| if (opts.loadingAsync && !opts.isRemoved) { |
| scope.$$loadingAsyncDone = false; |
| scope.progressMode = 'indeterminate'; |
| |
| $q.when(opts.loadingAsync) |
| .then(function() { |
| scope.$$loadingAsyncDone = true; |
| scope.progressMode = ''; |
| delete opts.loadingAsync; |
| }).then(function() { |
| $$rAF(positionAndFocusMenu); |
| }); |
| } |
| } |
| |
| /** |
| * |
| */ |
| function activateInteraction() { |
| if (opts.isRemoved) return; |
| |
| var dropDown = opts.selectEl; |
| var selectCtrl = dropDown.controller('mdSelectMenu') || {}; |
| |
| element.addClass('md-clickable'); |
| |
| // Close on backdrop click |
| opts.backdrop && opts.backdrop.on('click', onBackdropClick); |
| |
| // Escape to close |
| // Cycling of options, and closing on enter |
| dropDown.on('keydown', onMenuKeyDown); |
| dropDown.on('click', checkCloseMenu); |
| |
| return function cleanupInteraction() { |
| opts.backdrop && opts.backdrop.off('click', onBackdropClick); |
| dropDown.off('keydown', onMenuKeyDown); |
| dropDown.off('click', checkCloseMenu); |
| |
| element.removeClass('md-clickable'); |
| opts.isRemoved = true; |
| }; |
| |
| // ************************************ |
| // Closure Functions |
| // ************************************ |
| |
| function onBackdropClick(e) { |
| e.preventDefault(); |
| e.stopPropagation(); |
| opts.restoreFocus = false; |
| $mdUtil.nextTick($mdSelect.hide, true); |
| } |
| |
| function onMenuKeyDown(ev) { |
| var keyCodes = $mdConstant.KEY_CODE; |
| ev.preventDefault(); |
| ev.stopPropagation(); |
| |
| switch (ev.keyCode) { |
| case keyCodes.UP_ARROW: |
| return focusPrevOption(); |
| case keyCodes.DOWN_ARROW: |
| return focusNextOption(); |
| case keyCodes.SPACE: |
| case keyCodes.ENTER: |
| var option = $mdUtil.getClosest(ev.target, 'md-option'); |
| if (option) { |
| dropDown.triggerHandler({ |
| type: 'click', |
| target: option |
| }); |
| ev.preventDefault(); |
| } |
| checkCloseMenu(ev); |
| break; |
| case keyCodes.TAB: |
| case keyCodes.ESCAPE: |
| ev.stopPropagation(); |
| ev.preventDefault(); |
| opts.restoreFocus = true; |
| $mdUtil.nextTick($mdSelect.hide, true); |
| break; |
| default: |
| if (ev.keyCode >= 31 && ev.keyCode <= 90) { |
| var optNode = dropDown.controller('mdSelectMenu').optNodeForKeyboardSearch(ev); |
| opts.focusedNode = optNode || opts.focusedNode; |
| optNode && optNode.focus(); |
| } |
| } |
| } |
| |
| function focusOption(direction) { |
| var optionsArray = $mdUtil.nodesToArray(opts.optionNodes); |
| var index = optionsArray.indexOf(opts.focusedNode); |
| |
| var newOption; |
| |
| do { |
| if (index === -1) { |
| // We lost the previously focused element, reset to first option |
| index = 0; |
| } else if (direction === 'next' && index < optionsArray.length - 1) { |
| index++; |
| } else if (direction === 'prev' && index > 0) { |
| index--; |
| } |
| newOption = optionsArray[index]; |
| if (newOption.hasAttribute('disabled')) newOption = undefined; |
| } while (!newOption && index < optionsArray.length - 1 && index > 0) |
| newOption && newOption.focus(); |
| opts.focusedNode = newOption; |
| } |
| |
| function focusNextOption() { |
| focusOption('next'); |
| } |
| |
| function focusPrevOption() { |
| focusOption('prev'); |
| } |
| |
| function checkCloseMenu(ev) { |
| if (ev && ( ev.type == 'click') && (ev.currentTarget != dropDown[0])) return; |
| if ( mouseOnScrollbar() ) return; |
| |
| var option = $mdUtil.getClosest(ev.target, 'md-option'); |
| if (option && option.hasAttribute && !option.hasAttribute('disabled')) { |
| ev.preventDefault(); |
| ev.stopPropagation(); |
| if (!selectCtrl.isMultiple) { |
| opts.restoreFocus = true; |
| |
| $mdUtil.nextTick(function () { |
| $mdSelect.hide(selectCtrl.ngModel.$viewValue); |
| }, true); |
| } |
| } |
| /** |
| * check if the mouseup event was on a scrollbar |
| */ |
| function mouseOnScrollbar() { |
| var clickOnScrollbar = false; |
| if (ev && (ev.currentTarget.children.length > 0)) { |
| var child = ev.currentTarget.children[0]; |
| var hasScrollbar = child.scrollHeight > child.clientHeight; |
| if (hasScrollbar && child.children.length > 0) { |
| var relPosX = ev.pageX - ev.currentTarget.getBoundingClientRect().left; |
| if (relPosX > child.querySelector('md-option').offsetWidth) |
| clickOnScrollbar = true; |
| } |
| } |
| return clickOnScrollbar; |
| } |
| } |
| } |
| |
| } |
| |
| /** |
| * To notify listeners that the Select menu has closed, |
| * trigger the [optional] user-defined expression |
| */ |
| function announceClosed(opts) { |
| var mdSelect = opts.selectCtrl; |
| if (mdSelect) { |
| var menuController = opts.selectEl.controller('mdSelectMenu'); |
| mdSelect.setLabelText(menuController.selectedLabels()); |
| mdSelect.triggerClose(); |
| } |
| } |
| |
| |
| /** |
| * Calculate the |
| */ |
| function calculateMenuPositions(scope, element, opts) { |
| var |
| containerNode = element[0], |
| targetNode = opts.target[0].children[0], // target the label |
| parentNode = $document[0].body, |
| selectNode = opts.selectEl[0], |
| contentNode = opts.contentEl[0], |
| parentRect = parentNode.getBoundingClientRect(), |
| targetRect = targetNode.getBoundingClientRect(), |
| shouldOpenAroundTarget = false, |
| bounds = { |
| left: parentRect.left + SELECT_EDGE_MARGIN, |
| top: SELECT_EDGE_MARGIN, |
| bottom: parentRect.height - SELECT_EDGE_MARGIN, |
| right: parentRect.width - SELECT_EDGE_MARGIN - ($mdUtil.floatingScrollbars() ? 16 : 0) |
| }, |
| spaceAvailable = { |
| top: targetRect.top - bounds.top, |
| left: targetRect.left - bounds.left, |
| right: bounds.right - (targetRect.left + targetRect.width), |
| bottom: bounds.bottom - (targetRect.top + targetRect.height) |
| }, |
| maxWidth = parentRect.width - SELECT_EDGE_MARGIN * 2, |
| selectedNode = selectNode.querySelector('md-option[selected]'), |
| optionNodes = selectNode.getElementsByTagName('md-option'), |
| optgroupNodes = selectNode.getElementsByTagName('md-optgroup'), |
| isScrollable = calculateScrollable(element, contentNode), |
| centeredNode; |
| |
| var loading = isPromiseLike(opts.loadingAsync); |
| if (!loading) { |
| // If a selected node, center around that |
| if (selectedNode) { |
| centeredNode = selectedNode; |
| // If there are option groups, center around the first option group |
| } else if (optgroupNodes.length) { |
| centeredNode = optgroupNodes[0]; |
| // Otherwise - if we are not loading async - center around the first optionNode |
| } else if (optionNodes.length) { |
| centeredNode = optionNodes[0]; |
| // In case there are no options, center on whatever's in there... (eg progress indicator) |
| } else { |
| centeredNode = contentNode.firstElementChild || contentNode; |
| } |
| } else { |
| // If loading, center on progress indicator |
| centeredNode = contentNode.firstElementChild || contentNode; |
| } |
| |
| if (contentNode.offsetWidth > maxWidth) { |
| contentNode.style['max-width'] = maxWidth + 'px'; |
| } else { |
| contentNode.style.maxWidth = null; |
| } |
| if (shouldOpenAroundTarget) { |
| contentNode.style['min-width'] = targetRect.width + 'px'; |
| } |
| |
| // Remove padding before we compute the position of the menu |
| if (isScrollable) { |
| selectNode.classList.add('md-overflow'); |
| } |
| |
| var focusedNode = centeredNode; |
| if ((focusedNode.tagName || '').toUpperCase() === 'MD-OPTGROUP') { |
| focusedNode = optionNodes[0] || contentNode.firstElementChild || contentNode; |
| centeredNode = focusedNode; |
| } |
| // Cache for autoFocus() |
| opts.focusedNode = focusedNode; |
| |
| // Get the selectMenuRect *after* max-width is possibly set above |
| containerNode.style.display = 'block'; |
| var selectMenuRect = selectNode.getBoundingClientRect(); |
| var centeredRect = getOffsetRect(centeredNode); |
| |
| if (centeredNode) { |
| var centeredStyle = $window.getComputedStyle(centeredNode); |
| centeredRect.paddingLeft = parseInt(centeredStyle.paddingLeft, 10) || 0; |
| centeredRect.paddingRight = parseInt(centeredStyle.paddingRight, 10) || 0; |
| } |
| |
| if (isScrollable) { |
| var scrollBuffer = contentNode.offsetHeight / 2; |
| contentNode.scrollTop = centeredRect.top + centeredRect.height / 2 - scrollBuffer; |
| |
| if (spaceAvailable.top < scrollBuffer) { |
| contentNode.scrollTop = Math.min( |
| centeredRect.top, |
| contentNode.scrollTop + scrollBuffer - spaceAvailable.top |
| ); |
| } else if (spaceAvailable.bottom < scrollBuffer) { |
| contentNode.scrollTop = Math.max( |
| centeredRect.top + centeredRect.height - selectMenuRect.height, |
| contentNode.scrollTop - scrollBuffer + spaceAvailable.bottom |
| ); |
| } |
| } |
| |
| var left, top, transformOrigin, minWidth; |
| if (shouldOpenAroundTarget) { |
| left = targetRect.left; |
| top = targetRect.top + targetRect.height; |
| transformOrigin = '50% 0'; |
| if (top + selectMenuRect.height > bounds.bottom) { |
| top = targetRect.top - selectMenuRect.height; |
| transformOrigin = '50% 100%'; |
| } |
| } else { |
| left = (targetRect.left + centeredRect.left - centeredRect.paddingLeft) + 2; |
| top = Math.floor(targetRect.top + targetRect.height / 2 - centeredRect.height / 2 - |
| centeredRect.top + contentNode.scrollTop) + 2; |
| |
| transformOrigin = (centeredRect.left + targetRect.width / 2) + 'px ' + |
| (centeredRect.top + centeredRect.height / 2 - contentNode.scrollTop) + 'px 0px'; |
| |
| minWidth = Math.min(targetRect.width + centeredRect.paddingLeft + centeredRect.paddingRight, maxWidth); |
| } |
| |
| // Keep left and top within the window |
| var containerRect = containerNode.getBoundingClientRect(); |
| var scaleX = Math.round(100 * Math.min(targetRect.width / selectMenuRect.width, 1.0)) / 100; |
| var scaleY = Math.round(100 * Math.min(targetRect.height / selectMenuRect.height, 1.0)) / 100; |
| |
| return { |
| container: { |
| element: angular.element(containerNode), |
| styles: { |
| left: Math.floor(clamp(bounds.left, left, bounds.right - containerRect.width)), |
| top: Math.floor(clamp(bounds.top, top, bounds.bottom - containerRect.height)), |
| 'min-width': minWidth |
| } |
| }, |
| dropDown: { |
| element: angular.element(selectNode), |
| styles: { |
| transformOrigin: transformOrigin, |
| transform: !opts.alreadyOpen ? $mdUtil.supplant('scale({0},{1})', [scaleX, scaleY]) : "" |
| } |
| } |
| }; |
| |
| } |
| |
| } |
| |
| function isPromiseLike(obj) { |
| return obj && angular.isFunction(obj.then); |
| } |
| |
| function clamp(min, n, max) { |
| return Math.max(min, Math.min(n, max)); |
| } |
| |
| function getOffsetRect(node) { |
| return node ? { |
| left: node.offsetLeft, |
| top: node.offsetTop, |
| width: node.offsetWidth, |
| height: node.offsetHeight |
| } : {left: 0, top: 0, width: 0, height: 0}; |
| } |
| |
| function calculateScrollable(element, contentNode) { |
| var isScrollable = false; |
| |
| try { |
| var oldDisplay = element[0].style.display; |
| |
| // Set the element's display to block so that this calculation is correct |
| element[0].style.display = 'block'; |
| |
| isScrollable = contentNode.scrollHeight > contentNode.offsetHeight; |
| |
| // Reset it back afterwards |
| element[0].style.display = oldDisplay; |
| } finally { |
| // Nothing to do |
| } |
| return isScrollable; |
| } |
| } |
| SelectProvider.$inject = ["$$interimElementProvider"]; |
| |
| |
| ng.material.components.select = angular.module("material.components.select"); |