/*!
* AngularJS Material Design
* https://github.com/angular/material
* @license MIT
* v1.1.19
*/
goog.provide('ngmaterial.components.select');
goog.require('ngmaterial.components.backdrop');
goog.require('ngmaterial.core');
/**
* @ngdoc module
* @name material.components.select
*/
/***************************************************
### TODO ###
- [ ] Abstract placement logic in $mdSelect service to $mdMenu service
***************************************************/
SelectDirective['$inject'] = ["$mdSelect", "$mdUtil", "$mdConstant", "$mdTheming", "$mdAria", "$parse", "$sce", "$injector"];
SelectMenuDirective['$inject'] = ["$parse", "$mdUtil", "$mdConstant", "$mdTheming"];
OptionDirective['$inject'] = ["$mdButtonInkRipple", "$mdUtil", "$mdTheming"];
SelectProvider['$inject'] = ["$$interimElementProvider"];
var SELECT_EDGE_MARGIN = 8;
var selectNextId = 0;
var CHECKBOX_SELECTION_INDICATOR =
angular.element('
');
angular.module('material.components.select', [
'material.core',
'material.components.backdrop'
])
.directive('mdSelect', SelectDirective)
.directive('mdSelectMenu', SelectMenuDirective)
.directive('mdOption', OptionDirective)
.directive('mdOptgroup', OptgroupDirective)
.directive('mdSelectHeader', SelectHeaderDirective)
.provider('$mdSelect', SelectProvider);
/**
* @ngdoc directive
* @name mdSelect
* @restrict E
* @module material.components.select
*
* @description Displays a select box, bound to an `ng-model`. Selectable options are defined using
* the md-option element directive. Options can be grouped
* using the md-optgroup element directive.
*
* When the select is required and uses a floating label, then the label will automatically contain
* an asterisk (`*`). This behavior can be disabled by using the `md-no-asterisk` attribute.
*
* By default, the select will display with an underline to match other form elements. This can be
* disabled by applying the `md-no-underline` CSS class.
*
* @param {expression} ng-model Assignable angular expression to data-bind to.
* @param {expression=} ng-change Expression to be executed when the model value changes.
* @param {boolean=} multiple When present, allows for more than one option to be selected.
* The model is an array with the selected choices. **Note:** This attribute is only evaluated
* once; it is not watched.
* @param {expression=} md-on-close Expression to be evaluated when the select is closed.
* @param {expression=} md-on-open Expression to be evaluated when opening the select.
* Will hide the select options and show a spinner until the evaluated promise resolves.
* @param {expression=} md-selected-text Expression to be evaluated that will return a string
* to be displayed as a placeholder in the select input box when it is closed. The value
* will be treated as *text* (not html).
* @param {expression=} md-selected-html Expression to be evaluated that will return a string
* to be displayed as a placeholder in the select input box when it is closed. The value
* will be treated as *html*. The value must either be explicitly marked as trustedHtml or
* the ngSanitize module must be loaded.
* @param {string=} placeholder Placeholder hint text.
* @param {boolean=} md-no-asterisk When set to true, an asterisk will not be appended to the
* floating label. **Note:** This attribute is only evaluated once; it is not watched.
* @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)
*
*
*
* {{ opt }}
*
*
*
*
* With an explicit label
*
*
*
*
* {{ opt }}
*
*
*
*
* Using the `md-select-header` element directive
*
* When a developer needs to put more than just a text label in the `md-select-menu`, they should
* use one or more `md-select-header`s. These elements can contain custom HTML which can be styled
* as desired. Use cases for this element include a sticky search bar and custom option group
* labels.
*
*
*
*
*
* Neighborhoods -
*
* {{ opt }}
*
*
*
*
* ## 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:
*
* 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' };
* });
*
*
*
*
* {{ user.name }}
*
*
*
*
* 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.
*
* **Note:** We do not support AngularJS's `track by` syntax. For instance
* `ng-options="user in users track by user.id"` will not work with `md-select`.
*
* Working HTML:
*
*
*
* {{ user.name }}
*
*
*
*/
function SelectDirective($mdSelect, $mdUtil, $mdConstant, $mdTheming, $mdAria, $parse, $sce,
$injector) {
var keyCodes = $mdConstant.KEY_CODE;
var NAVIGATION_KEYS = [keyCodes.SPACE, keyCodes.ENTER, keyCodes.UP_ARROW, keyCodes.DOWN_ARROW];
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('');
valueEl.append('');
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.
var mdContentEl = element.find('md-content');
if (!mdContentEl.length) {
element.append(angular.element('').append(element.contents()));
}
mdContentEl.attr('role', 'presentation');
// 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(
'
' +
' ' +
'
'
));
// Hide list [of item options] while loading async
element
.find('md-option')
.attr('ng-show', '$$loadingAsyncDone');
}
if (attr.name) {
var autofillClone = angular.element('');
autofillClone.attr({
'name': attr.name,
'aria-hidden': 'true',
'tabindex': '-1'
});
var opts = element.find('md-option');
angular.forEach(opts, function(el) {
var newEl = angular.element('');
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);
});
// Adds an extra option that will hold the selected value for the
// cases where the select is a part of a non-angular form. This can be done with a ng-model,
// however if the `md-option` is being `ng-repeat`-ed, AngularJS seems to insert a similar
// `option` node, but with a value of `? string: ?` which would then get submitted.
// This also goes around having to prepend a dot to the name attribute.
autofillClone.append(
''
);
element.parent().append(autofillClone);
}
var isMultiple = $mdUtil.parseAttributeBoolean(attr.multiple);
// Use everything that's left inside element.contents() as the contents of the menu
var multipleContent = isMultiple ? 'multiple' : '';
var selectTemplate = '' +
'
' +
'{1}' +
'
';
selectTemplate = $mdUtil.supplant(selectTemplate, [multipleContent, element.html()]);
element.empty().append(valueEl);
element.append(selectTemplate);
if (!attr.tabindex){
attr.$set('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);
var disableAsterisk = $mdUtil.parseAttributeBoolean(attr.mdNoAsterisk);
if (disableAsterisk) {
element.addClass('md-no-asterisk');
}
if (containerCtrl) {
var isErrorGetter = containerCtrl.isErrorGetter || function() {
return ngModelCtrl.$invalid && (ngModelCtrl.$touched || (formCtrl && formCtrl.$submitted));
};
if (containerCtrl.input) {
// We ignore inputs that are in the md-select-header (one
// case where this might be useful would be adding as searchbox)
if (element.find('md-select-header').find('input')[0] !== containerCtrl.input[0]) {
throw new Error(" can only have *one* child ,