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