select.js 45 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369
  1. /*!
  2. * Angular Material Design
  3. * https://github.com/angular/material
  4. * @license MIT
  5. * v0.11.4
  6. */
  7. goog.provide('ng.material.components.select');
  8. goog.require('ng.material.components.backdrop');
  9. goog.require('ng.material.core');
  10. /**
  11. * @ngdoc module
  12. * @name material.components.select
  13. */
  14. /***************************************************
  15. ### TODO ###
  16. **DOCUMENTATION AND DEMOS**
  17. - [ ] ng-model with child mdOptions (basic)
  18. - [ ] ng-model="foo" ng-model-options="{ trackBy: '$value.id' }" for objects
  19. - [ ] mdOption with value
  20. - [ ] Usage with input inside
  21. ### TODO - POST RC1 ###
  22. - [ ] Abstract placement logic in $mdSelect service to $mdMenu service
  23. ***************************************************/
  24. var SELECT_EDGE_MARGIN = 8;
  25. var selectNextId = 0;
  26. angular.module('material.components.select', [
  27. 'material.core',
  28. 'material.components.backdrop'
  29. ])
  30. .directive('mdSelect', SelectDirective)
  31. .directive('mdSelectMenu', SelectMenuDirective)
  32. .directive('mdOption', OptionDirective)
  33. .directive('mdOptgroup', OptgroupDirective)
  34. .provider('$mdSelect', SelectProvider);
  35. /**
  36. * @ngdoc directive
  37. * @name mdSelect
  38. * @restrict E
  39. * @module material.components.select
  40. *
  41. * @description Displays a select box, bound to an ng-model.
  42. *
  43. * @param {expression} ng-model The model!
  44. * @param {boolean=} multiple Whether it's multiple.
  45. * @param {expression=} md-on-close Expression to be evaluated when the select is closed.
  46. * @param {string=} placeholder Placeholder hint text.
  47. * @param {string=} aria-label Optional label for accessibility. Only necessary if no placeholder or
  48. * explicit label is present.
  49. * @param {string=} md-container-class Class list to get applied to the `.md-select-menu-container`
  50. * element (for custom styling).
  51. *
  52. * @usage
  53. * With a placeholder (label and aria-label are added dynamically)
  54. * <hljs lang="html">
  55. * <md-input-container>
  56. * <md-select
  57. * ng-model="someModel"
  58. * placeholder="Select a state">
  59. * <md-option ng-value="opt" ng-repeat="opt in neighborhoods2">{{ opt }}</md-option>
  60. * </md-select>
  61. * </md-input-container>
  62. * </hljs>
  63. *
  64. * With an explicit label
  65. * <hljs lang="html">
  66. * <md-input-container>
  67. * <label>State</label>
  68. * <md-select
  69. * ng-model="someModel">
  70. * <md-option ng-value="opt" ng-repeat="opt in neighborhoods2">{{ opt }}</md-option>
  71. * </md-select>
  72. * </md-input-container>
  73. * </hljs>
  74. */
  75. function SelectDirective($mdSelect, $mdUtil, $mdTheming, $mdAria, $compile, $parse) {
  76. return {
  77. restrict: 'E',
  78. require: ['^?mdInputContainer', 'mdSelect', 'ngModel', '?^form'],
  79. compile: compile,
  80. controller: function() {
  81. } // empty placeholder controller to be initialized in link
  82. };
  83. function compile(element, attr) {
  84. // add the select value that will hold our placeholder or selected option value
  85. var valueEl = angular.element('<md-select-value><span></span></md-select-value>');
  86. valueEl.append('<span class="md-select-icon" aria-hidden="true"></span>');
  87. valueEl.addClass('md-select-value');
  88. if (!valueEl[0].hasAttribute('id')) {
  89. valueEl.attr('id', 'select_value_label_' + $mdUtil.nextUid());
  90. }
  91. // There's got to be an md-content inside. If there's not one, let's add it.
  92. if (!element.find('md-content').length) {
  93. element.append(angular.element('<md-content>').append(element.contents()));
  94. }
  95. // Add progress spinner for md-options-loading
  96. if (attr.mdOnOpen) {
  97. // Show progress indicator while loading async
  98. // Use ng-hide for `display:none` so the indicator does not interfere with the options list
  99. element
  100. .find('md-content')
  101. .prepend(angular.element(
  102. '<div>' +
  103. ' <md-progress-circular md-mode="{{progressMode}}" ng-hide="$$loadingAsyncDone"></md-progress-circular>' +
  104. '</div>'
  105. ));
  106. // Hide list [of item options] while loading async
  107. element
  108. .find('md-option')
  109. .attr('ng-show', '$$loadingAsyncDone');
  110. }
  111. if (attr.name) {
  112. var autofillClone = angular.element('<select class="md-visually-hidden">');
  113. autofillClone.attr({
  114. 'name': '.' + attr.name,
  115. 'ng-model': attr.ngModel,
  116. 'aria-hidden': 'true',
  117. 'tabindex': '-1'
  118. });
  119. var opts = element.find('md-option');
  120. angular.forEach(opts, function(el) {
  121. var newEl = angular.element('<option>' + el.innerHTML + '</option>');
  122. if (el.hasAttribute('ng-value')) newEl.attr('ng-value', el.getAttribute('ng-value'));
  123. else if (el.hasAttribute('value')) newEl.attr('value', el.getAttribute('value'));
  124. autofillClone.append(newEl);
  125. });
  126. element.parent().append(autofillClone);
  127. }
  128. // Use everything that's left inside element.contents() as the contents of the menu
  129. var multiple = angular.isDefined(attr.multiple) ? 'multiple' : '';
  130. var selectTemplate = '' +
  131. '<div class="md-select-menu-container">' +
  132. '<md-select-menu {0}>{1}</md-select-menu>' +
  133. '</div>';
  134. selectTemplate = $mdUtil.supplant(selectTemplate, [multiple, element.html()]);
  135. element.empty().append(valueEl);
  136. attr.tabindex = attr.tabindex || '0';
  137. return function postLink(scope, element, attr, ctrls) {
  138. var isDisabled;
  139. var containerCtrl = ctrls[0];
  140. var mdSelectCtrl = ctrls[1];
  141. var ngModelCtrl = ctrls[2];
  142. var formCtrl = ctrls[3];
  143. // grab a reference to the select menu value label
  144. var valueEl = element.find('md-select-value');
  145. var isReadonly = angular.isDefined(attr.readonly);
  146. if (containerCtrl) {
  147. var isErrorGetter = containerCtrl.isErrorGetter || function() {
  148. return ngModelCtrl.$invalid && ngModelCtrl.$touched;
  149. };
  150. if (containerCtrl.input) {
  151. throw new Error("<md-input-container> can only have *one* child <input>, <textarea> or <select> element!");
  152. }
  153. containerCtrl.input = element;
  154. if (!containerCtrl.label) {
  155. $mdAria.expect(element, 'aria-label', element.attr('placeholder'));
  156. }
  157. scope.$watch(isErrorGetter, containerCtrl.setInvalid);
  158. }
  159. var selectContainer, selectScope, selectMenuCtrl;
  160. createSelect();
  161. $mdTheming(element);
  162. if (attr.name && formCtrl) {
  163. var selectEl = element.parent()[0].querySelector('select[name=".' + attr.name + '"]');
  164. var controller = angular.element(selectEl).controller();
  165. if (controller) {
  166. formCtrl.$removeControl(controller);
  167. }
  168. }
  169. if (formCtrl) {
  170. $mdUtil.nextTick(function() {
  171. formCtrl.$setPristine();
  172. });
  173. }
  174. var originalRender = ngModelCtrl.$render;
  175. ngModelCtrl.$render = function() {
  176. originalRender();
  177. syncLabelText();
  178. inputCheckValue();
  179. };
  180. attr.$observe('placeholder', ngModelCtrl.$render);
  181. mdSelectCtrl.setLabelText = function(text) {
  182. mdSelectCtrl.setIsPlaceholder(!text);
  183. // Use placeholder attribute, otherwise fallback to the md-input-container label
  184. var tmpPlaceholder = attr.placeholder || (containerCtrl && containerCtrl.label ? containerCtrl.label.text() : '');
  185. text = text || tmpPlaceholder || '';
  186. var target = valueEl.children().eq(0);
  187. target.text(text);
  188. };
  189. mdSelectCtrl.setIsPlaceholder = function(isPlaceholder) {
  190. if (isPlaceholder) {
  191. valueEl.addClass('md-select-placeholder');
  192. if (containerCtrl && containerCtrl.label) {
  193. containerCtrl.label.addClass('md-placeholder md-static');
  194. }
  195. } else {
  196. valueEl.removeClass('md-select-placeholder');
  197. if (containerCtrl && containerCtrl.label) {
  198. containerCtrl.label.removeClass('md-placeholder');
  199. }
  200. }
  201. };
  202. if (!isReadonly) {
  203. element
  204. .on('focus', function(ev) {
  205. // only set focus on if we don't currently have a selected value. This avoids the "bounce"
  206. // on the label transition because the focus will immediately switch to the open menu.
  207. if (containerCtrl && containerCtrl.element.hasClass('md-input-has-value')) {
  208. containerCtrl.setFocused(true);
  209. }
  210. })
  211. .on('blur', function(ev) {
  212. containerCtrl && containerCtrl.setFocused(false);
  213. inputCheckValue();
  214. });
  215. }
  216. mdSelectCtrl.triggerClose = function() {
  217. $parse(attr.mdOnClose)(scope);
  218. };
  219. scope.$$postDigest(function() {
  220. setAriaLabel();
  221. syncLabelText();
  222. });
  223. function setAriaLabel() {
  224. var labelText = element.attr('placeholder');
  225. if (!labelText && containerCtrl && containerCtrl.label) {
  226. labelText = containerCtrl.label.text();
  227. }
  228. $mdAria.expect(element, 'aria-label', labelText);
  229. }
  230. function syncLabelText() {
  231. if (selectContainer) {
  232. selectMenuCtrl = selectMenuCtrl || selectContainer.find('md-select-menu').controller('mdSelectMenu');
  233. mdSelectCtrl.setLabelText(selectMenuCtrl.selectedLabels());
  234. }
  235. }
  236. var deregisterWatcher;
  237. attr.$observe('ngMultiple', function(val) {
  238. if (deregisterWatcher) deregisterWatcher();
  239. var parser = $parse(val);
  240. deregisterWatcher = scope.$watch(function() {
  241. return parser(scope);
  242. }, function(multiple, prevVal) {
  243. if (multiple === undefined && prevVal === undefined) return; // assume compiler did a good job
  244. if (multiple) {
  245. element.attr('multiple', 'multiple');
  246. } else {
  247. element.removeAttr('multiple');
  248. }
  249. if (selectContainer) {
  250. selectMenuCtrl.setMultiple(multiple);
  251. originalRender = ngModelCtrl.$render;
  252. ngModelCtrl.$render = function() {
  253. originalRender();
  254. syncLabelText();
  255. };
  256. selectMenuCtrl.refreshViewValue();
  257. ngModelCtrl.$render();
  258. }
  259. });
  260. });
  261. attr.$observe('disabled', function(disabled) {
  262. if (angular.isString(disabled)) {
  263. disabled = true;
  264. }
  265. // Prevent click event being registered twice
  266. if (isDisabled !== undefined && isDisabled === disabled) {
  267. return;
  268. }
  269. isDisabled = disabled;
  270. if (disabled) {
  271. element.attr({'tabindex': -1, 'aria-disabled': 'true'});
  272. element.off('click', openSelect);
  273. element.off('keydown', handleKeypress);
  274. } else {
  275. element.attr({'tabindex': attr.tabindex, 'aria-disabled': 'false'});
  276. element.on('click', openSelect);
  277. element.on('keydown', handleKeypress);
  278. }
  279. });
  280. if (!attr.disabled && !attr.ngDisabled) {
  281. element.attr({'tabindex': attr.tabindex, 'aria-disabled': 'false'});
  282. element.on('click', openSelect);
  283. element.on('keydown', handleKeypress);
  284. }
  285. var ariaAttrs = {
  286. role: 'combobox',
  287. 'aria-expanded': 'false'
  288. };
  289. if (!element[0].hasAttribute('id')) {
  290. ariaAttrs.id = 'select_' + $mdUtil.nextUid();
  291. }
  292. element.attr(ariaAttrs);
  293. scope.$on('$destroy', function() {
  294. $mdSelect
  295. .destroy()
  296. .finally(function() {
  297. if ( selectContainer ) {
  298. selectContainer.remove();
  299. }
  300. if (containerCtrl) {
  301. containerCtrl.setFocused(false);
  302. containerCtrl.setHasValue(false);
  303. containerCtrl.input = null;
  304. }
  305. });
  306. });
  307. function inputCheckValue() {
  308. // The select counts as having a value if one or more options are selected,
  309. // or if the input's validity state says it has bad input (eg string in a number input)
  310. containerCtrl && containerCtrl.setHasValue(selectMenuCtrl.selectedLabels().length > 0 || (element[0].validity || {}).badInput);
  311. }
  312. // Create a fake select to find out the label value
  313. function createSelect() {
  314. selectContainer = angular.element(selectTemplate);
  315. var selectEl = selectContainer.find('md-select-menu');
  316. selectEl.data('$ngModelController', ngModelCtrl);
  317. selectEl.data('$mdSelectController', mdSelectCtrl);
  318. selectScope = scope.$new();
  319. $mdTheming.inherit(selectContainer, element);
  320. if (element.attr('md-container-class')) {
  321. var value = selectContainer[0].getAttribute('class') + ' ' + element.attr('md-container-class');
  322. selectContainer[0].setAttribute('class', value);
  323. }
  324. selectContainer = $compile(selectContainer)(selectScope);
  325. selectMenuCtrl = selectContainer.find('md-select-menu').controller('mdSelectMenu');
  326. }
  327. function handleKeypress(e) {
  328. var allowedCodes = [32, 13, 38, 40];
  329. if (allowedCodes.indexOf(e.keyCode) != -1) {
  330. // prevent page scrolling on interaction
  331. e.preventDefault();
  332. openSelect(e);
  333. } else {
  334. if (e.keyCode <= 90 && e.keyCode >= 31) {
  335. e.preventDefault();
  336. var node = selectMenuCtrl.optNodeForKeyboardSearch(e);
  337. if (!node) return;
  338. var optionCtrl = angular.element(node).controller('mdOption');
  339. if (!selectMenuCtrl.isMultiple) {
  340. selectMenuCtrl.deselect(Object.keys(selectMenuCtrl.selected)[0]);
  341. }
  342. selectMenuCtrl.select(optionCtrl.hashKey, optionCtrl.value);
  343. selectMenuCtrl.refreshViewValue();
  344. ngModelCtrl.$render();
  345. }
  346. }
  347. }
  348. function openSelect() {
  349. selectScope.isOpen = true;
  350. $mdSelect.show({
  351. scope: selectScope,
  352. preserveScope: true,
  353. skipCompile: true,
  354. element: selectContainer,
  355. target: element[0],
  356. hasBackdrop: true,
  357. loadingAsync: attr.mdOnOpen ? scope.$eval(attr.mdOnOpen) || true : false
  358. }).then(function() {
  359. selectScope.isOpen = false;
  360. });
  361. }
  362. };
  363. }
  364. }
  365. SelectDirective.$inject = ["$mdSelect", "$mdUtil", "$mdTheming", "$mdAria", "$compile", "$parse"];
  366. function SelectMenuDirective($parse, $mdUtil, $mdTheming) {
  367. SelectMenuController.$inject = ["$scope", "$attrs", "$element"];
  368. return {
  369. restrict: 'E',
  370. require: ['mdSelectMenu', '?ngModel'],
  371. controller: SelectMenuController,
  372. link: {pre: preLink}
  373. };
  374. // We use preLink instead of postLink to ensure that the select is initialized before
  375. // its child options run postLink.
  376. function preLink(scope, element, attr, ctrls) {
  377. var selectCtrl = ctrls[0];
  378. var ngModel = ctrls[1];
  379. $mdTheming(element);
  380. element.on('click', clickListener);
  381. element.on('keypress', keyListener);
  382. if (ngModel) selectCtrl.init(ngModel);
  383. configureAria();
  384. function configureAria() {
  385. element.attr({
  386. 'id': 'select_menu_' + $mdUtil.nextUid(),
  387. 'role': 'listbox',
  388. 'aria-multiselectable': (selectCtrl.isMultiple ? 'true' : 'false')
  389. });
  390. }
  391. function keyListener(e) {
  392. if (e.keyCode == 13 || e.keyCode == 32) {
  393. clickListener(e);
  394. }
  395. }
  396. function clickListener(ev) {
  397. var option = $mdUtil.getClosest(ev.target, 'md-option');
  398. var optionCtrl = option && angular.element(option).data('$mdOptionController');
  399. if (!option || !optionCtrl) return;
  400. if (option.hasAttribute('disabled')) {
  401. ev.stopImmediatePropagation();
  402. return false;
  403. }
  404. var optionHashKey = selectCtrl.hashGetter(optionCtrl.value);
  405. var isSelected = angular.isDefined(selectCtrl.selected[optionHashKey]);
  406. scope.$apply(function() {
  407. if (selectCtrl.isMultiple) {
  408. if (isSelected) {
  409. selectCtrl.deselect(optionHashKey);
  410. } else {
  411. selectCtrl.select(optionHashKey, optionCtrl.value);
  412. }
  413. } else {
  414. if (!isSelected) {
  415. selectCtrl.deselect(Object.keys(selectCtrl.selected)[0]);
  416. selectCtrl.select(optionHashKey, optionCtrl.value);
  417. }
  418. }
  419. selectCtrl.refreshViewValue();
  420. });
  421. }
  422. }
  423. function SelectMenuController($scope, $attrs, $element) {
  424. var self = this;
  425. self.isMultiple = angular.isDefined($attrs.multiple);
  426. // selected is an object with keys matching all of the selected options' hashed values
  427. self.selected = {};
  428. // options is an object with keys matching every option's hash value,
  429. // and values matching every option's controller.
  430. self.options = {};
  431. $scope.$watch(function() {
  432. return self.options;
  433. }, function() {
  434. self.ngModel.$render();
  435. }, true);
  436. var deregisterCollectionWatch;
  437. self.setMultiple = function(isMultiple) {
  438. var ngModel = self.ngModel;
  439. self.isMultiple = isMultiple;
  440. if (deregisterCollectionWatch) deregisterCollectionWatch();
  441. if (self.isMultiple) {
  442. ngModel.$validators['md-multiple'] = validateArray;
  443. ngModel.$render = renderMultiple;
  444. // watchCollection on the model because by default ngModel only watches the model's
  445. // reference. This allowed the developer to also push and pop from their array.
  446. $scope.$watchCollection($attrs.ngModel, function(value) {
  447. if (validateArray(value)) renderMultiple(value);
  448. self.ngModel.$setPristine();
  449. });
  450. } else {
  451. delete ngModel.$validators['md-multiple'];
  452. ngModel.$render = renderSingular;
  453. }
  454. function validateArray(modelValue, viewValue) {
  455. // If a value is truthy but not an array, reject it.
  456. // If value is undefined/falsy, accept that it's an empty array.
  457. return angular.isArray(modelValue || viewValue || []);
  458. }
  459. };
  460. var searchStr = '';
  461. var clearSearchTimeout, optNodes, optText;
  462. var CLEAR_SEARCH_AFTER = 300;
  463. self.optNodeForKeyboardSearch = function(e) {
  464. clearSearchTimeout && clearTimeout(clearSearchTimeout);
  465. clearSearchTimeout = setTimeout(function() {
  466. clearSearchTimeout = undefined;
  467. searchStr = '';
  468. optText = undefined;
  469. optNodes = undefined;
  470. }, CLEAR_SEARCH_AFTER);
  471. searchStr += String.fromCharCode(e.keyCode);
  472. var search = new RegExp('^' + searchStr, 'i');
  473. if (!optNodes) {
  474. optNodes = $element.find('md-option');
  475. optText = new Array(optNodes.length);
  476. angular.forEach(optNodes, function(el, i) {
  477. optText[i] = el.textContent.trim();
  478. });
  479. }
  480. for (var i = 0; i < optText.length; ++i) {
  481. if (search.test(optText[i])) {
  482. return optNodes[i];
  483. }
  484. }
  485. };
  486. self.init = function(ngModel) {
  487. self.ngModel = ngModel;
  488. // Allow users to provide `ng-model="foo" ng-model-options="{trackBy: 'foo.id'}"` so
  489. // that we can properly compare objects set on the model to the available options
  490. if (ngModel.$options && ngModel.$options.trackBy) {
  491. var trackByLocals = {};
  492. var trackByParsed = $parse(ngModel.$options.trackBy);
  493. self.hashGetter = function(value, valueScope) {
  494. trackByLocals.$value = value;
  495. return trackByParsed(valueScope || $scope, trackByLocals);
  496. };
  497. // If the user doesn't provide a trackBy, we automatically generate an id for every
  498. // value passed in
  499. } else {
  500. self.hashGetter = function getHashValue(value) {
  501. if (angular.isObject(value)) {
  502. return 'object_' + (value.$$mdSelectId || (value.$$mdSelectId = ++selectNextId));
  503. }
  504. return value;
  505. };
  506. }
  507. self.setMultiple(self.isMultiple);
  508. };
  509. self.selectedLabels = function() {
  510. var selectedOptionEls = $mdUtil.nodesToArray($element[0].querySelectorAll('md-option[selected]'));
  511. if (selectedOptionEls.length) {
  512. return selectedOptionEls.map(function(el) {
  513. return el.textContent;
  514. }).join(', ');
  515. } else {
  516. return '';
  517. }
  518. };
  519. self.select = function(hashKey, hashedValue) {
  520. var option = self.options[hashKey];
  521. option && option.setSelected(true);
  522. self.selected[hashKey] = hashedValue;
  523. };
  524. self.deselect = function(hashKey) {
  525. var option = self.options[hashKey];
  526. option && option.setSelected(false);
  527. delete self.selected[hashKey];
  528. };
  529. self.addOption = function(hashKey, optionCtrl) {
  530. if (angular.isDefined(self.options[hashKey])) {
  531. throw new Error('Duplicate md-option values are not allowed in a select. ' +
  532. 'Duplicate value "' + optionCtrl.value + '" found.');
  533. }
  534. self.options[hashKey] = optionCtrl;
  535. // If this option's value was already in our ngModel, go ahead and select it.
  536. if (angular.isDefined(self.selected[hashKey])) {
  537. self.select(hashKey, optionCtrl.value);
  538. self.refreshViewValue();
  539. }
  540. };
  541. self.removeOption = function(hashKey) {
  542. delete self.options[hashKey];
  543. // Don't deselect an option when it's removed - the user's ngModel should be allowed
  544. // to have values that do not match a currently available option.
  545. };
  546. self.refreshViewValue = function() {
  547. var values = [];
  548. var option;
  549. for (var hashKey in self.selected) {
  550. // If this hashKey has an associated option, push that option's value to the model.
  551. if ((option = self.options[hashKey])) {
  552. values.push(option.value);
  553. } else {
  554. // Otherwise, the given hashKey has no associated option, and we got it
  555. // from an ngModel value at an earlier time. Push the unhashed value of
  556. // this hashKey to the model.
  557. // This allows the developer to put a value in the model that doesn't yet have
  558. // an associated option.
  559. values.push(self.selected[hashKey]);
  560. }
  561. }
  562. self.ngModel.$setViewValue(self.isMultiple ? values : values[0]);
  563. };
  564. function renderMultiple() {
  565. var newSelectedValues = self.ngModel.$modelValue || self.ngModel.$viewValue || [];
  566. if (!angular.isArray(newSelectedValues)) return;
  567. var oldSelected = Object.keys(self.selected);
  568. var newSelectedHashes = newSelectedValues.map(self.hashGetter);
  569. var deselected = oldSelected.filter(function(hash) {
  570. return newSelectedHashes.indexOf(hash) === -1;
  571. });
  572. deselected.forEach(self.deselect);
  573. newSelectedHashes.forEach(function(hashKey, i) {
  574. self.select(hashKey, newSelectedValues[i]);
  575. });
  576. }
  577. function renderSingular() {
  578. var value = self.ngModel.$viewValue || self.ngModel.$modelValue;
  579. Object.keys(self.selected).forEach(self.deselect);
  580. self.select(self.hashGetter(value), value);
  581. }
  582. }
  583. }
  584. SelectMenuDirective.$inject = ["$parse", "$mdUtil", "$mdTheming"];
  585. function OptionDirective($mdButtonInkRipple, $mdUtil) {
  586. OptionController.$inject = ["$element"];
  587. return {
  588. restrict: 'E',
  589. require: ['mdOption', '^^mdSelectMenu'],
  590. controller: OptionController,
  591. compile: compile
  592. };
  593. function compile(element, attr) {
  594. // Manual transclusion to avoid the extra inner <span> that ng-transclude generates
  595. element.append(angular.element('<div class="md-text">').append(element.contents()));
  596. element.attr('tabindex', attr.tabindex || '0');
  597. return postLink;
  598. }
  599. function postLink(scope, element, attr, ctrls) {
  600. var optionCtrl = ctrls[0];
  601. var selectCtrl = ctrls[1];
  602. if (angular.isDefined(attr.ngValue)) {
  603. scope.$watch(attr.ngValue, setOptionValue);
  604. } else if (angular.isDefined(attr.value)) {
  605. setOptionValue(attr.value);
  606. } else {
  607. scope.$watch(function() {
  608. return element.text();
  609. }, setOptionValue);
  610. }
  611. attr.$observe('disabled', function(disabled) {
  612. if (disabled) {
  613. element.attr('tabindex', '-1');
  614. } else {
  615. element.attr('tabindex', '0');
  616. }
  617. });
  618. scope.$$postDigest(function() {
  619. attr.$observe('selected', function(selected) {
  620. if (!angular.isDefined(selected)) return;
  621. if (typeof selected == 'string') selected = true;
  622. if (selected) {
  623. if (!selectCtrl.isMultiple) {
  624. selectCtrl.deselect(Object.keys(selectCtrl.selected)[0]);
  625. }
  626. selectCtrl.select(optionCtrl.hashKey, optionCtrl.value);
  627. } else {
  628. selectCtrl.deselect(optionCtrl.hashKey);
  629. }
  630. selectCtrl.refreshViewValue();
  631. selectCtrl.ngModel.$render();
  632. });
  633. });
  634. $mdButtonInkRipple.attach(scope, element);
  635. configureAria();
  636. function setOptionValue(newValue, oldValue) {
  637. var oldHashKey = selectCtrl.hashGetter(oldValue, scope);
  638. var newHashKey = selectCtrl.hashGetter(newValue, scope);
  639. optionCtrl.hashKey = newHashKey;
  640. optionCtrl.value = newValue;
  641. selectCtrl.removeOption(oldHashKey, optionCtrl);
  642. selectCtrl.addOption(newHashKey, optionCtrl);
  643. }
  644. scope.$on('$destroy', function() {
  645. selectCtrl.removeOption(optionCtrl.hashKey, optionCtrl);
  646. });
  647. function configureAria() {
  648. var ariaAttrs = {
  649. 'role': 'option',
  650. 'aria-selected': 'false'
  651. };
  652. if (!element[0].hasAttribute('id')) {
  653. ariaAttrs.id = 'select_option_' + $mdUtil.nextUid();
  654. }
  655. element.attr(ariaAttrs);
  656. }
  657. }
  658. function OptionController($element) {
  659. this.selected = false;
  660. this.setSelected = function(isSelected) {
  661. if (isSelected && !this.selected) {
  662. $element.attr({
  663. 'selected': 'selected',
  664. 'aria-selected': 'true'
  665. });
  666. } else if (!isSelected && this.selected) {
  667. $element.removeAttr('selected');
  668. $element.attr('aria-selected', 'false');
  669. }
  670. this.selected = isSelected;
  671. };
  672. }
  673. }
  674. OptionDirective.$inject = ["$mdButtonInkRipple", "$mdUtil"];
  675. function OptgroupDirective() {
  676. return {
  677. restrict: 'E',
  678. compile: compile
  679. };
  680. function compile(el, attrs) {
  681. var labelElement = el.find('label');
  682. if (!labelElement.length) {
  683. labelElement = angular.element('<label>');
  684. el.prepend(labelElement);
  685. }
  686. if (attrs.label) labelElement.text(attrs.label);
  687. }
  688. }
  689. function SelectProvider($$interimElementProvider) {
  690. selectDefaultOptions.$inject = ["$mdSelect", "$mdConstant", "$mdUtil", "$window", "$q", "$$rAF", "$animateCss", "$animate"];
  691. return $$interimElementProvider('$mdSelect')
  692. .setDefaults({
  693. methods: ['target'],
  694. options: selectDefaultOptions
  695. });
  696. /* ngInject */
  697. function selectDefaultOptions($mdSelect, $mdConstant, $mdUtil, $window, $q, $$rAF, $animateCss, $animate) {
  698. var ERRROR_TARGET_EXPECTED = "$mdSelect.show() expected a target element in options.target but got '{0}'!";
  699. var animator = $mdUtil.dom.animator;
  700. return {
  701. parent: 'body',
  702. themable: true,
  703. onShow: onShow,
  704. onRemove: onRemove,
  705. hasBackdrop: true,
  706. disableParentScroll: true
  707. };
  708. /**
  709. * Interim-element onRemove logic....
  710. */
  711. function onRemove(scope, element, opts) {
  712. opts = opts || { };
  713. opts.cleanupInteraction();
  714. opts.cleanupResizing();
  715. opts.hideBackdrop();
  716. // For navigation $destroy events, do a quick, non-animated removal,
  717. // but for normal closes (from clicks, etc) animate the removal
  718. return (opts.$destroy === true) ? detachAndClean() : animateRemoval().then( detachAndClean );
  719. /**
  720. * For normal closes (eg clicks), animate the removal.
  721. * For forced closes (like $destroy events from navigation),
  722. * skip the animations
  723. */
  724. function animateRemoval() {
  725. return $animateCss(element, {addClass: 'md-leave'}).start();
  726. }
  727. /**
  728. * Detach the element and cleanup prior changes
  729. */
  730. function detachAndClean() {
  731. configureAria(opts.target, false);
  732. element.attr('opacity', 0);
  733. element.removeClass('md-active');
  734. detachElement(element, opts);
  735. announceClosed(opts);
  736. if (!opts.$destroy && opts.restoreFocus) {
  737. opts.target.focus();
  738. }
  739. }
  740. }
  741. /**
  742. * Interim-element onShow logic....
  743. */
  744. function onShow(scope, element, opts) {
  745. watchAsyncLoad();
  746. sanitizeAndConfigure(scope, opts);
  747. configureAria(opts.target);
  748. opts.hideBackdrop = showBackdrop(scope, element, opts);
  749. return showDropDown(scope, element, opts)
  750. .then(function(response) {
  751. opts.alreadyOpen = true;
  752. opts.cleanupInteraction = activateInteraction();
  753. opts.cleanupResizing = activateResizing();
  754. return response;
  755. }, opts.hideBackdrop);
  756. // ************************************
  757. // Closure Functions
  758. // ************************************
  759. /**
  760. * Attach the select DOM element(s) and animate to the correct positions
  761. * and scalings...
  762. */
  763. function showDropDown(scope, element, opts) {
  764. opts.parent.append(element);
  765. return $q(function(resolve, reject) {
  766. try {
  767. $animateCss(element, {removeClass: 'md-leave', duration: 0})
  768. .start()
  769. .then(positionAndFocusMenu)
  770. .then(resolve);
  771. } catch (e) {
  772. reject(e);
  773. }
  774. });
  775. }
  776. /**
  777. * Initialize container and dropDown menu positions/scale, then animate
  778. * to show... and autoFocus.
  779. */
  780. function positionAndFocusMenu() {
  781. return $q(function(resolve) {
  782. if (opts.isRemoved) return $q.reject(false);
  783. var info = calculateMenuPositions(scope, element, opts);
  784. info.container.element.css(animator.toCss(info.container.styles));
  785. info.dropDown.element.css(animator.toCss(info.dropDown.styles));
  786. $$rAF(function() {
  787. element.addClass('md-active');
  788. info.dropDown.element.css(animator.toCss({transform: ''}));
  789. autoFocus(opts.focusedNode);
  790. resolve();
  791. });
  792. });
  793. }
  794. /**
  795. * Show modal backdrop element...
  796. */
  797. function showBackdrop(scope, element, options) {
  798. // If we are not within a dialog...
  799. if (options.disableParentScroll && !$mdUtil.getClosest(options.target, 'MD-DIALOG')) {
  800. // !! DO this before creating the backdrop; since disableScrollAround()
  801. // configures the scroll offset; which is used by mdBackDrop postLink()
  802. options.restoreScroll = $mdUtil.disableScrollAround(options.element, options.parent);
  803. } else {
  804. options.disableParentScroll = false;
  805. }
  806. if (options.hasBackdrop) {
  807. // Override duration to immediately show invisible backdrop
  808. options.backdrop = $mdUtil.createBackdrop(scope, "md-select-backdrop md-click-catcher");
  809. $animate.enter(options.backdrop, options.parent, null, {duration: 0});
  810. }
  811. /**
  812. * Hide modal backdrop element...
  813. */
  814. return function hideBackdrop() {
  815. if (options.backdrop) options.backdrop.remove();
  816. if (options.disableParentScroll) options.restoreScroll();
  817. delete options.restoreScroll;
  818. }
  819. }
  820. /**
  821. *
  822. */
  823. function autoFocus(focusedNode) {
  824. if (focusedNode && !focusedNode.hasAttribute('disabled')) {
  825. focusedNode.focus();
  826. }
  827. }
  828. /**
  829. * Check for valid opts and set some sane defaults
  830. */
  831. function sanitizeAndConfigure(scope, options) {
  832. var selectEl = element.find('md-select-menu');
  833. if (!options.target) {
  834. throw new Error($mdUtil.supplant(ERRROR_TARGET_EXPECTED, [options.target]));
  835. }
  836. angular.extend(options, {
  837. isRemoved: false,
  838. target: angular.element(options.target), //make sure it's not a naked dom node
  839. parent: angular.element(options.parent),
  840. selectEl: selectEl,
  841. contentEl: element.find('md-content'),
  842. optionNodes: selectEl[0].getElementsByTagName('md-option')
  843. });
  844. }
  845. /**
  846. * Configure various resize listeners for screen changes
  847. */
  848. function activateResizing() {
  849. var debouncedOnResize = (function(scope, target, options) {
  850. return function() {
  851. if (options.isRemoved) return;
  852. var updates = calculateMenuPositions(scope, target, options);
  853. var container = updates.container;
  854. var dropDown = updates.dropDown;
  855. container.element.css(animator.toCss(container.styles));
  856. dropDown.element.css(animator.toCss(dropDown.styles));
  857. };
  858. })(scope, element, opts);
  859. var window = angular.element($window);
  860. window.on('resize', debouncedOnResize);
  861. window.on('orientationchange', debouncedOnResize);
  862. // Publish deactivation closure...
  863. return function deactivateResizing() {
  864. // Disable resizing handlers
  865. window.off('resize', debouncedOnResize);
  866. window.off('orientationchange', debouncedOnResize);
  867. }
  868. }
  869. /**
  870. * If asynchronously loading, watch and update internal
  871. * '$$loadingAsyncDone' flag
  872. */
  873. function watchAsyncLoad() {
  874. if (opts.loadingAsync && !opts.isRemoved) {
  875. scope.$$loadingAsyncDone = false;
  876. scope.progressMode = 'indeterminate';
  877. $q.when(opts.loadingAsync)
  878. .then(function() {
  879. scope.$$loadingAsyncDone = true;
  880. scope.progressMode = '';
  881. delete opts.loadingAsync;
  882. }).then(function() {
  883. $$rAF(positionAndFocusMenu);
  884. })
  885. }
  886. }
  887. /**
  888. *
  889. */
  890. function activateInteraction() {
  891. if (opts.isRemoved) return;
  892. var dropDown = opts.selectEl;
  893. var selectCtrl = dropDown.controller('mdSelectMenu') || {};
  894. element.addClass('md-clickable');
  895. // Close on backdrop click
  896. opts.backdrop && opts.backdrop.on('click', onBackdropClick);
  897. // Escape to close
  898. // Cycling of options, and closing on enter
  899. dropDown.on('keydown', onMenuKeyDown);
  900. dropDown.on('mouseup', checkCloseMenu);
  901. return function cleanupInteraction() {
  902. opts.backdrop && opts.backdrop.off('click', onBackdropClick);
  903. dropDown.off('keydown', onMenuKeyDown);
  904. dropDown.off('mouseup', checkCloseMenu);
  905. element.removeClass('md-clickable');
  906. opts.isRemoved = true;
  907. };
  908. // ************************************
  909. // Closure Functions
  910. // ************************************
  911. function onBackdropClick(e) {
  912. e.preventDefault();
  913. e.stopPropagation();
  914. opts.restoreFocus = false;
  915. $mdUtil.nextTick($mdSelect.hide, true);
  916. }
  917. function onMenuKeyDown(ev) {
  918. var keyCodes = $mdConstant.KEY_CODE;
  919. switch (ev.keyCode) {
  920. case keyCodes.UP_ARROW:
  921. return focusPrevOption();
  922. break;
  923. case keyCodes.DOWN_ARROW:
  924. return focusNextOption();
  925. break;
  926. case keyCodes.SPACE:
  927. case keyCodes.ENTER:
  928. var option = $mdUtil.getClosest(ev.target, 'md-option');
  929. if (option) {
  930. dropDown.triggerHandler({
  931. type: 'click',
  932. target: option
  933. });
  934. ev.preventDefault();
  935. }
  936. checkCloseMenu(ev);
  937. break;
  938. case keyCodes.TAB:
  939. case keyCodes.ESCAPE:
  940. ev.preventDefault();
  941. opts.restoreFocus = true;
  942. $mdUtil.nextTick($mdSelect.hide, true);
  943. break;
  944. default:
  945. if (ev.keyCode >= 31 && ev.keyCode <= 90) {
  946. var optNode = dropDown.controller('mdSelectMenu').optNodeForKeyboardSearch(ev);
  947. opts.focusedNode = optNode || opts.focusedNode;
  948. optNode && optNode.focus();
  949. }
  950. }
  951. }
  952. function focusOption(direction) {
  953. var optionsArray = $mdUtil.nodesToArray(opts.optionNodes);
  954. var index = optionsArray.indexOf(opts.focusedNode);
  955. var newOption;
  956. do {
  957. if (index === -1) {
  958. // We lost the previously focused element, reset to first option
  959. index = 0;
  960. } else if (direction === 'next' && index < optionsArray.length - 1) {
  961. index++;
  962. } else if (direction === 'prev' && index > 0) {
  963. index--;
  964. }
  965. newOption = optionsArray[index];
  966. if (newOption.hasAttribute('disabled')) newOption = undefined;
  967. } while (!newOption && index < optionsArray.length - 1 && index > 0)
  968. newOption && newOption.focus();
  969. opts.focusedNode = newOption;
  970. }
  971. function focusNextOption() {
  972. focusOption('next');
  973. }
  974. function focusPrevOption() {
  975. focusOption('prev');
  976. }
  977. function checkCloseMenu(ev) {
  978. if (ev && ( ev.type == 'mouseup') && (ev.currentTarget != dropDown[0])) return;
  979. if ( mouseOnScrollbar() ) return;
  980. if (!selectCtrl.isMultiple) {
  981. opts.restoreFocus = true;
  982. $mdUtil.nextTick(function() {
  983. $mdSelect.hide(selectCtrl.ngModel.$viewValue);
  984. }, true);
  985. }
  986. /**
  987. * check if the mouseup event was on a scrollbar
  988. */
  989. function mouseOnScrollbar() {
  990. var clickOnScrollbar = false;
  991. if (ev && (ev.currentTarget.children.length > 0)) {
  992. var child = ev.currentTarget.children[0];
  993. var hasScrollbar = child.scrollHeight > child.clientHeight;
  994. if (hasScrollbar && child.children.length > 0) {
  995. var relPosX = ev.pageX - ev.currentTarget.getBoundingClientRect().left;
  996. if (relPosX > child.querySelector('md-option').offsetWidth)
  997. clickOnScrollbar = true;
  998. }
  999. }
  1000. return clickOnScrollbar;
  1001. }
  1002. }
  1003. }
  1004. }
  1005. /**
  1006. *
  1007. */
  1008. function configureAria(element, isExpanded) {
  1009. isExpanded = angular.isUndefined(isExpanded) ? 'true' : 'false';
  1010. element && element.attr('aria-expanded', isExpanded);
  1011. }
  1012. /**
  1013. * To notify listeners that the Select menu has closed,
  1014. * trigger the [optional] user-defined expression
  1015. */
  1016. function announceClosed(opts) {
  1017. var mdSelect = opts.selectEl.controller('mdSelect');
  1018. if (mdSelect) {
  1019. var menuController = opts.selectEl.controller('mdSelectMenu');
  1020. mdSelect.setLabelText(menuController.selectedLabels());
  1021. mdSelect.triggerClose();
  1022. }
  1023. }
  1024. /**
  1025. * Use browser to remove this element without triggering a $destroy event
  1026. */
  1027. function detachElement(element, opts) {
  1028. if (element[0].parentNode === opts.parent[0]) {
  1029. opts.parent[0].removeChild(element[0]);
  1030. }
  1031. }
  1032. /**
  1033. * Calculate the
  1034. */
  1035. function calculateMenuPositions(scope, element, opts) {
  1036. var optionNodes,
  1037. containerNode = element[0],
  1038. targetNode = opts.target[0].firstElementChild, // target the label
  1039. parentNode = opts.parent[0],
  1040. selectNode = opts.selectEl[0],
  1041. contentNode = opts.contentEl[0],
  1042. parentRect = parentNode.getBoundingClientRect(),
  1043. targetRect = targetNode.getBoundingClientRect(),
  1044. shouldOpenAroundTarget = false,
  1045. bounds = {
  1046. left: parentRect.left + SELECT_EDGE_MARGIN,
  1047. top: SELECT_EDGE_MARGIN,
  1048. bottom: parentRect.height - SELECT_EDGE_MARGIN,
  1049. right: parentRect.width - SELECT_EDGE_MARGIN - ($mdUtil.floatingScrollbars() ? 16 : 0)
  1050. },
  1051. spaceAvailable = {
  1052. top: targetRect.top - bounds.top,
  1053. left: targetRect.left - bounds.left,
  1054. right: bounds.right - (targetRect.left + targetRect.width),
  1055. bottom: bounds.bottom - (targetRect.top + targetRect.height)
  1056. },
  1057. maxWidth = parentRect.width - SELECT_EDGE_MARGIN * 2,
  1058. isScrollable = contentNode.scrollHeight > contentNode.offsetHeight,
  1059. selectedNode = selectNode.querySelector('md-option[selected]'),
  1060. optionNodes = selectNode.getElementsByTagName('md-option'),
  1061. optgroupNodes = selectNode.getElementsByTagName('md-optgroup');
  1062. var loading = isPromiseLike(opts.loadingAsync);
  1063. var centeredNode;
  1064. if (!loading) {
  1065. // If a selected node, center around that
  1066. if (selectedNode) {
  1067. centeredNode = selectedNode;
  1068. // If there are option groups, center around the first option group
  1069. } else if (optgroupNodes.length) {
  1070. centeredNode = optgroupNodes[0];
  1071. // Otherwise - if we are not loading async - center around the first optionNode
  1072. } else if (optionNodes.length) {
  1073. centeredNode = optionNodes[0];
  1074. // In case there are no options, center on whatever's in there... (eg progress indicator)
  1075. } else {
  1076. centeredNode = contentNode.firstElementChild || contentNode;
  1077. }
  1078. } else {
  1079. // If loading, center on progress indicator
  1080. centeredNode = contentNode.firstElementChild || contentNode;
  1081. }
  1082. if (contentNode.offsetWidth > maxWidth) {
  1083. contentNode.style['max-width'] = maxWidth + 'px';
  1084. }
  1085. if (shouldOpenAroundTarget) {
  1086. contentNode.style['min-width'] = targetRect.width + 'px';
  1087. }
  1088. // Remove padding before we compute the position of the menu
  1089. if (isScrollable) {
  1090. selectNode.classList.add('md-overflow');
  1091. }
  1092. var focusedNode = centeredNode;
  1093. if ((focusedNode.tagName || '').toUpperCase() === 'MD-OPTGROUP') {
  1094. focusedNode = optionNodes[0] || contentNode.firstElementChild || contentNode;
  1095. centeredNode = focusedNode;
  1096. }
  1097. // Cache for autoFocus()
  1098. opts.focusedNode = focusedNode;
  1099. // Get the selectMenuRect *after* max-width is possibly set above
  1100. var selectMenuRect = selectNode.getBoundingClientRect();
  1101. var centeredRect = getOffsetRect(centeredNode);
  1102. if (centeredNode) {
  1103. var centeredStyle = $window.getComputedStyle(centeredNode);
  1104. centeredRect.paddingLeft = parseInt(centeredStyle.paddingLeft, 10) || 0;
  1105. centeredRect.paddingRight = parseInt(centeredStyle.paddingRight, 10) || 0;
  1106. }
  1107. if (isScrollable) {
  1108. var scrollBuffer = contentNode.offsetHeight / 2;
  1109. contentNode.scrollTop = centeredRect.top + centeredRect.height / 2 - scrollBuffer;
  1110. if (spaceAvailable.top < scrollBuffer) {
  1111. contentNode.scrollTop = Math.min(
  1112. centeredRect.top,
  1113. contentNode.scrollTop + scrollBuffer - spaceAvailable.top
  1114. );
  1115. } else if (spaceAvailable.bottom < scrollBuffer) {
  1116. contentNode.scrollTop = Math.max(
  1117. centeredRect.top + centeredRect.height - selectMenuRect.height,
  1118. contentNode.scrollTop - scrollBuffer + spaceAvailable.bottom
  1119. );
  1120. }
  1121. }
  1122. var left, top, transformOrigin, minWidth;
  1123. if (shouldOpenAroundTarget) {
  1124. left = targetRect.left;
  1125. top = targetRect.top + targetRect.height;
  1126. transformOrigin = '50% 0';
  1127. if (top + selectMenuRect.height > bounds.bottom) {
  1128. top = targetRect.top - selectMenuRect.height;
  1129. transformOrigin = '50% 100%';
  1130. }
  1131. } else {
  1132. left = (targetRect.left + centeredRect.left - centeredRect.paddingLeft) + 2;
  1133. top = Math.floor(targetRect.top + targetRect.height / 2 - centeredRect.height / 2 -
  1134. centeredRect.top + contentNode.scrollTop) + 2;
  1135. transformOrigin = (centeredRect.left + targetRect.width / 2) + 'px ' +
  1136. (centeredRect.top + centeredRect.height / 2 - contentNode.scrollTop) + 'px 0px';
  1137. minWidth = targetRect.width + centeredRect.paddingLeft + centeredRect.paddingRight;
  1138. }
  1139. // Keep left and top within the window
  1140. var containerRect = containerNode.getBoundingClientRect();
  1141. var scaleX = Math.round(100 * Math.min(targetRect.width / selectMenuRect.width, 1.0)) / 100;
  1142. var scaleY = Math.round(100 * Math.min(targetRect.height / selectMenuRect.height, 1.0)) / 100;
  1143. return {
  1144. container: {
  1145. element: angular.element(containerNode),
  1146. styles: {
  1147. left: Math.floor(clamp(bounds.left, left, bounds.right - containerRect.width)),
  1148. top: Math.floor(clamp(bounds.top, top, bounds.bottom - containerRect.height)),
  1149. 'min-width': minWidth
  1150. }
  1151. },
  1152. dropDown: {
  1153. element: angular.element(selectNode),
  1154. styles: {
  1155. transformOrigin: transformOrigin,
  1156. transform: !opts.alreadyOpen ? $mdUtil.supplant('scale({0},{1})', [scaleX, scaleY]) : ""
  1157. }
  1158. }
  1159. };
  1160. }
  1161. }
  1162. function isPromiseLike(obj) {
  1163. return obj && angular.isFunction(obj.then);
  1164. }
  1165. function clamp(min, n, max) {
  1166. return Math.max(min, Math.min(n, max));
  1167. }
  1168. function getOffsetRect(node) {
  1169. return node ? {
  1170. left: node.offsetLeft,
  1171. top: node.offsetTop,
  1172. width: node.offsetWidth,
  1173. height: node.offsetHeight
  1174. } : {left: 0, top: 0, width: 0, height: 0};
  1175. }
  1176. }
  1177. SelectProvider.$inject = ["$$interimElementProvider"];
  1178. ng.material.components.select = angular.module("material.components.select");