1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369 |
- /*!
- * Angular Material Design
- * https://github.com/angular/material
- * @license MIT
- * v0.11.4
- */
- goog.provide('ng.material.components.select');
- goog.require('ng.material.components.backdrop');
- goog.require('ng.material.core');
- /**
- * @ngdoc module
- * @name material.components.select
- */
- /***************************************************
- ### TODO ###
- **DOCUMENTATION AND DEMOS**
- - [ ] ng-model with child mdOptions (basic)
- - [ ] ng-model="foo" ng-model-options="{ trackBy: '$value.id' }" for objects
- - [ ] mdOption with value
- - [ ] Usage with input inside
- ### 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>
- */
- 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">' +
- '<md-select-menu {0}>{1}</md-select-menu>' +
- '</div>';
- selectTemplate = $mdUtil.supplant(selectTemplate, [multiple, element.html()]);
- element.empty().append(valueEl);
- attr.tabindex = attr.tabindex || '0';
- return function postLink(scope, element, attr, ctrls) {
- var isDisabled;
- 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;
- createSelect();
- $mdTheming(element);
- if (attr.name && formCtrl) {
- var selectEl = element.parent()[0].querySelector('select[name=".' + attr.name + '"]');
- var controller = angular.element(selectEl).controller();
- if (controller) {
- formCtrl.$removeControl(controller);
- }
- }
- if (formCtrl) {
- $mdUtil.nextTick(function() {
- formCtrl.$setPristine();
- });
- }
- var originalRender = ngModelCtrl.$render;
- ngModelCtrl.$render = function() {
- originalRender();
- syncLabelText();
- 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.text(text);
- };
- mdSelectCtrl.setIsPlaceholder = function(isPlaceholder) {
- if (isPlaceholder) {
- valueEl.addClass('md-select-placeholder');
- if (containerCtrl && containerCtrl.label) {
- containerCtrl.label.addClass('md-placeholder md-static');
- }
- } 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);
- }
- })
- .on('blur', function(ev) {
- containerCtrl && containerCtrl.setFocused(false);
- inputCheckValue();
- });
- }
- mdSelectCtrl.triggerClose = function() {
- $parse(attr.mdOnClose)(scope);
- };
- scope.$$postDigest(function() {
- setAriaLabel();
- syncLabelText();
- });
- function setAriaLabel() {
- var labelText = element.attr('placeholder');
- if (!labelText && containerCtrl && containerCtrl.label) {
- labelText = containerCtrl.label.text();
- }
- $mdAria.expect(element, 'aria-label', labelText);
- }
- function syncLabelText() {
- if (selectContainer) {
- selectMenuCtrl = selectMenuCtrl || selectContainer.find('md-select-menu').controller('mdSelectMenu');
- mdSelectCtrl.setLabelText(selectMenuCtrl.selectedLabels());
- }
- }
- 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');
- }
- if (selectContainer) {
- selectMenuCtrl.setMultiple(multiple);
- originalRender = ngModelCtrl.$render;
- ngModelCtrl.$render = function() {
- originalRender();
- syncLabelText();
- };
- selectMenuCtrl.refreshViewValue();
- 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: 'combobox',
- 'aria-expanded': 'false'
- };
- if (!element[0].hasAttribute('id')) {
- ariaAttrs.id = 'select_' + $mdUtil.nextUid();
- }
- element.attr(ariaAttrs);
- scope.$on('$destroy', function() {
- $mdSelect
- .destroy()
- .finally(function() {
- if ( selectContainer ) {
- selectContainer.remove();
- }
- if (containerCtrl) {
- containerCtrl.setFocused(false);
- containerCtrl.setHasValue(false);
- containerCtrl.input = null;
- }
- });
- });
- 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);
- }
- // Create a fake select to find out the label value
- function createSelect() {
- selectContainer = angular.element(selectTemplate);
- var selectEl = selectContainer.find('md-select-menu');
- selectEl.data('$ngModelController', ngModelCtrl);
- selectEl.data('$mdSelectController', mdSelectCtrl);
- selectScope = scope.$new();
- $mdTheming.inherit(selectContainer, element);
- if (element.attr('md-container-class')) {
- var value = selectContainer[0].getAttribute('class') + ' ' + element.attr('md-container-class');
- selectContainer[0].setAttribute('class', value);
- }
- selectContainer = $compile(selectContainer)(selectScope);
- selectMenuCtrl = selectContainer.find('md-select-menu').controller('mdSelectMenu');
- }
- 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();
- ngModelCtrl.$render();
- }
- }
- }
- function openSelect() {
- selectScope.isOpen = true;
- $mdSelect.show({
- scope: selectScope,
- preserveScope: true,
- skipCompile: true,
- element: selectContainer,
- target: element[0],
- hasBackdrop: true,
- loadingAsync: attr.mdOnOpen ? scope.$eval(attr.mdOnOpen) || true : false
- }).then(function() {
- selectScope.isOpen = false;
- });
- }
- };
- }
- }
- SelectDirective.$inject = ["$mdSelect", "$mdUtil", "$mdTheming", "$mdAria", "$compile", "$parse"];
- function SelectMenuDirective($parse, $mdUtil, $mdTheming) {
- SelectMenuController.$inject = ["$scope", "$attrs", "$element"];
- return {
- restrict: 'E',
- require: ['mdSelectMenu', '?ngModel'],
- 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];
- var ngModel = ctrls[1];
- $mdTheming(element);
- element.on('click', clickListener);
- element.on('keypress', keyListener);
- if (ngModel) selectCtrl.init(ngModel);
- configureAria();
- function configureAria() {
- element.attr({
- 'id': 'select_menu_' + $mdUtil.nextUid(),
- 'role': 'listbox',
- 'aria-multiselectable': (selectCtrl.isMultiple ? 'true' : 'false')
- });
- }
- 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.$watch(function() {
- return self.options;
- }, function() {
- self.ngModel.$render();
- }, true);
- var deregisterCollectionWatch;
- self.setMultiple = function(isMultiple) {
- var ngModel = self.ngModel;
- 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($attrs.ngModel, function(value) {
- if (validateArray(value)) renderMultiple(value);
- self.ngModel.$setPristine();
- });
- } 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) {
- self.ngModel = ngModel;
- // 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() {
- var selectedOptionEls = $mdUtil.nodesToArray($element[0].querySelectorAll('md-option[selected]'));
- if (selectedOptionEls.length) {
- return selectedOptionEls.map(function(el) {
- return el.textContent;
- }).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]);
- }
- }
- self.ngModel.$setViewValue(self.isMultiple ? values : values[0]);
- };
- 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();
- selectCtrl.ngModel.$render();
- });
- });
- $mdButtonInkRipple.attach(scope, element);
- configureAria();
- function setOptionValue(newValue, oldValue) {
- 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);
- }
- if (attrs.label) labelElement.text(attrs.label);
- }
- }
- function SelectProvider($$interimElementProvider) {
- selectDefaultOptions.$inject = ["$mdSelect", "$mdConstant", "$mdUtil", "$window", "$q", "$$rAF", "$animateCss", "$animate"];
- return $$interimElementProvider('$mdSelect')
- .setDefaults({
- methods: ['target'],
- options: selectDefaultOptions
- });
- /* ngInject */
- function selectDefaultOptions($mdSelect, $mdConstant, $mdUtil, $window, $q, $$rAF, $animateCss, $animate) {
- 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) ? detachAndClean() : animateRemoval().then( detachAndClean );
- /**
- * 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();
- }
- /**
- * Detach the element and cleanup prior changes
- */
- function detachAndClean() {
- configureAria(opts.target, false);
- element.attr('opacity', 0);
- element.removeClass('md-active');
- detachElement(element, opts);
- announceClosed(opts);
- if (!opts.$destroy && opts.restoreFocus) {
- opts.target.focus();
- }
- }
- }
- /**
- * Interim-element onShow logic....
- */
- function onShow(scope, element, opts) {
- watchAsyncLoad();
- sanitizeAndConfigure(scope, opts);
- configureAria(opts.target);
- opts.hideBackdrop = showBackdrop(scope, element, opts);
- return showDropDown(scope, element, opts)
- .then(function(response) {
- 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, options.parent, 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('mouseup', checkCloseMenu);
- return function cleanupInteraction() {
- opts.backdrop && opts.backdrop.off('click', onBackdropClick);
- dropDown.off('keydown', onMenuKeyDown);
- dropDown.off('mouseup', 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;
- switch (ev.keyCode) {
- case keyCodes.UP_ARROW:
- return focusPrevOption();
- break;
- case keyCodes.DOWN_ARROW:
- return focusNextOption();
- break;
- 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.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 == 'mouseup') && (ev.currentTarget != dropDown[0])) return;
- if ( mouseOnScrollbar() ) return;
- 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;
- }
- }
- }
- }
- /**
- *
- */
- function configureAria(element, isExpanded) {
- isExpanded = angular.isUndefined(isExpanded) ? 'true' : 'false';
- element && element.attr('aria-expanded', isExpanded);
- }
- /**
- * To notify listeners that the Select menu has closed,
- * trigger the [optional] user-defined expression
- */
- function announceClosed(opts) {
- var mdSelect = opts.selectEl.controller('mdSelect');
- if (mdSelect) {
- var menuController = opts.selectEl.controller('mdSelectMenu');
- mdSelect.setLabelText(menuController.selectedLabels());
- mdSelect.triggerClose();
- }
- }
- /**
- * Use browser to remove this element without triggering a $destroy event
- */
- function detachElement(element, opts) {
- if (element[0].parentNode === opts.parent[0]) {
- opts.parent[0].removeChild(element[0]);
- }
- }
- /**
- * Calculate the
- */
- function calculateMenuPositions(scope, element, opts) {
- var optionNodes,
- containerNode = element[0],
- targetNode = opts.target[0].firstElementChild, // target the label
- parentNode = opts.parent[0],
- 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,
- isScrollable = contentNode.scrollHeight > contentNode.offsetHeight,
- selectedNode = selectNode.querySelector('md-option[selected]'),
- optionNodes = selectNode.getElementsByTagName('md-option'),
- optgroupNodes = selectNode.getElementsByTagName('md-optgroup');
- var loading = isPromiseLike(opts.loadingAsync);
- var centeredNode;
- 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';
- }
- 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
- 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 = targetRect.width + centeredRect.paddingLeft + centeredRect.paddingRight;
- }
- // 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};
- }
- }
- SelectProvider.$inject = ["$$interimElementProvider"];
- ng.material.components.select = angular.module("material.components.select");
|