autocomplete.js 55 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707
  1. /*!
  2. * AngularJS Material Design
  3. * https://github.com/angular/material
  4. * @license MIT
  5. * v1.1.8-master-aba7b2b
  6. */
  7. goog.provide('ngmaterial.components.autocomplete');
  8. goog.require('ngmaterial.components.icon');
  9. goog.require('ngmaterial.components.virtualRepeat');
  10. goog.require('ngmaterial.core');
  11. /**
  12. * @ngdoc module
  13. * @name material.components.autocomplete
  14. */
  15. /*
  16. * @see js folder for autocomplete implementation
  17. */
  18. angular.module('material.components.autocomplete', [
  19. 'material.core',
  20. 'material.components.icon',
  21. 'material.components.virtualRepeat'
  22. ]);
  23. MdAutocompleteCtrl['$inject'] = ["$scope", "$element", "$mdUtil", "$mdConstant", "$mdTheming", "$window", "$animate", "$rootElement", "$attrs", "$q", "$log", "$mdLiveAnnouncer"];angular
  24. .module('material.components.autocomplete')
  25. .controller('MdAutocompleteCtrl', MdAutocompleteCtrl);
  26. var ITEM_HEIGHT = 48,
  27. MAX_ITEMS = 5,
  28. MENU_PADDING = 8,
  29. INPUT_PADDING = 2; // Padding provided by `md-input-container`
  30. function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming, $window,
  31. $animate, $rootElement, $attrs, $q, $log, $mdLiveAnnouncer) {
  32. // Internal Variables.
  33. var ctrl = this,
  34. itemParts = $scope.itemsExpr.split(/ in /i),
  35. itemExpr = itemParts[ 1 ],
  36. elements = null,
  37. cache = {},
  38. noBlur = false,
  39. selectedItemWatchers = [],
  40. hasFocus = false,
  41. fetchesInProgress = 0,
  42. enableWrapScroll = null,
  43. inputModelCtrl = null,
  44. debouncedOnResize = $mdUtil.debounce(onWindowResize);
  45. // Public Exported Variables with handlers
  46. defineProperty('hidden', handleHiddenChange, true);
  47. // Public Exported Variables
  48. ctrl.scope = $scope;
  49. ctrl.parent = $scope.$parent;
  50. ctrl.itemName = itemParts[ 0 ];
  51. ctrl.matches = [];
  52. ctrl.loading = false;
  53. ctrl.hidden = true;
  54. ctrl.index = null;
  55. ctrl.id = $mdUtil.nextUid();
  56. ctrl.isDisabled = null;
  57. ctrl.isRequired = null;
  58. ctrl.isReadonly = null;
  59. ctrl.hasNotFound = false;
  60. // Public Exported Methods
  61. ctrl.keydown = keydown;
  62. ctrl.blur = blur;
  63. ctrl.focus = focus;
  64. ctrl.clear = clearValue;
  65. ctrl.select = select;
  66. ctrl.listEnter = onListEnter;
  67. ctrl.listLeave = onListLeave;
  68. ctrl.mouseUp = onMouseup;
  69. ctrl.getCurrentDisplayValue = getCurrentDisplayValue;
  70. ctrl.registerSelectedItemWatcher = registerSelectedItemWatcher;
  71. ctrl.unregisterSelectedItemWatcher = unregisterSelectedItemWatcher;
  72. ctrl.notFoundVisible = notFoundVisible;
  73. ctrl.loadingIsVisible = loadingIsVisible;
  74. ctrl.positionDropdown = positionDropdown;
  75. /**
  76. * Report types to be used for the $mdLiveAnnouncer
  77. * @enum {number} Unique flag id.
  78. */
  79. var ReportType = {
  80. Count: 1,
  81. Selected: 2
  82. };
  83. return init();
  84. //-- initialization methods
  85. /**
  86. * Initialize the controller, setup watchers, gather elements
  87. */
  88. function init () {
  89. $mdUtil.initOptionalProperties($scope, $attrs, {
  90. searchText: '',
  91. selectedItem: null,
  92. clearButton: false
  93. });
  94. $mdTheming($element);
  95. configureWatchers();
  96. $mdUtil.nextTick(function () {
  97. gatherElements();
  98. moveDropdown();
  99. // Forward all focus events to the input element when autofocus is enabled
  100. if ($scope.autofocus) {
  101. $element.on('focus', focusInputElement);
  102. }
  103. });
  104. }
  105. function updateModelValidators() {
  106. if (!$scope.requireMatch || !inputModelCtrl) return;
  107. inputModelCtrl.$setValidity('md-require-match', !!$scope.selectedItem || !$scope.searchText);
  108. }
  109. /**
  110. * Calculates the dropdown's position and applies the new styles to the menu element
  111. * @returns {*}
  112. */
  113. function positionDropdown () {
  114. if (!elements) {
  115. return $mdUtil.nextTick(positionDropdown, false, $scope);
  116. }
  117. var dropdownHeight = ($scope.dropdownItems || MAX_ITEMS) * ITEM_HEIGHT;
  118. var hrect = elements.wrap.getBoundingClientRect(),
  119. vrect = elements.snap.getBoundingClientRect(),
  120. root = elements.root.getBoundingClientRect(),
  121. top = vrect.bottom - root.top,
  122. bot = root.bottom - vrect.top,
  123. left = hrect.left - root.left,
  124. width = hrect.width,
  125. offset = getVerticalOffset(),
  126. position = $scope.dropdownPosition,
  127. styles;
  128. // Automatically determine dropdown placement based on available space in viewport.
  129. if (!position) {
  130. position = (top > bot && root.height - top - MENU_PADDING < dropdownHeight) ? 'top' : 'bottom';
  131. }
  132. // Adjust the width to account for the padding provided by `md-input-container`
  133. if ($attrs.mdFloatingLabel) {
  134. left += INPUT_PADDING;
  135. width -= INPUT_PADDING * 2;
  136. }
  137. styles = {
  138. left: left + 'px',
  139. minWidth: width + 'px',
  140. maxWidth: Math.max(hrect.right - root.left, root.right - hrect.left) - MENU_PADDING + 'px'
  141. };
  142. if (position === 'top') {
  143. styles.top = 'auto';
  144. styles.bottom = bot + 'px';
  145. styles.maxHeight = Math.min(dropdownHeight, hrect.top - root.top - MENU_PADDING) + 'px';
  146. } else {
  147. var bottomSpace = root.bottom - hrect.bottom - MENU_PADDING + $mdUtil.getViewportTop();
  148. styles.top = (top - offset) + 'px';
  149. styles.bottom = 'auto';
  150. styles.maxHeight = Math.min(dropdownHeight, bottomSpace) + 'px';
  151. }
  152. elements.$.scrollContainer.css(styles);
  153. $mdUtil.nextTick(correctHorizontalAlignment, false);
  154. /**
  155. * Calculates the vertical offset for floating label examples to account for ngMessages
  156. * @returns {number}
  157. */
  158. function getVerticalOffset () {
  159. var offset = 0;
  160. var inputContainer = $element.find('md-input-container');
  161. if (inputContainer.length) {
  162. var input = inputContainer.find('input');
  163. offset = inputContainer.prop('offsetHeight');
  164. offset -= input.prop('offsetTop');
  165. offset -= input.prop('offsetHeight');
  166. // add in the height left up top for the floating label text
  167. offset += inputContainer.prop('offsetTop');
  168. }
  169. return offset;
  170. }
  171. /**
  172. * Makes sure that the menu doesn't go off of the screen on either side.
  173. */
  174. function correctHorizontalAlignment () {
  175. var dropdown = elements.scrollContainer.getBoundingClientRect(),
  176. styles = {};
  177. if (dropdown.right > root.right - MENU_PADDING) {
  178. styles.left = (hrect.right - dropdown.width) + 'px';
  179. }
  180. elements.$.scrollContainer.css(styles);
  181. }
  182. }
  183. /**
  184. * Moves the dropdown menu to the body tag in order to avoid z-index and overflow issues.
  185. */
  186. function moveDropdown () {
  187. if (!elements.$.root.length) return;
  188. $mdTheming(elements.$.scrollContainer);
  189. elements.$.scrollContainer.detach();
  190. elements.$.root.append(elements.$.scrollContainer);
  191. if ($animate.pin) $animate.pin(elements.$.scrollContainer, $rootElement);
  192. }
  193. /**
  194. * Sends focus to the input element.
  195. */
  196. function focusInputElement () {
  197. elements.input.focus();
  198. }
  199. /**
  200. * Sets up any watchers used by autocomplete
  201. */
  202. function configureWatchers () {
  203. var wait = parseInt($scope.delay, 10) || 0;
  204. $attrs.$observe('disabled', function (value) { ctrl.isDisabled = $mdUtil.parseAttributeBoolean(value, false); });
  205. $attrs.$observe('required', function (value) { ctrl.isRequired = $mdUtil.parseAttributeBoolean(value, false); });
  206. $attrs.$observe('readonly', function (value) { ctrl.isReadonly = $mdUtil.parseAttributeBoolean(value, false); });
  207. $scope.$watch('searchText', wait ? $mdUtil.debounce(handleSearchText, wait) : handleSearchText);
  208. $scope.$watch('selectedItem', selectedItemChange);
  209. angular.element($window).on('resize', debouncedOnResize);
  210. $scope.$on('$destroy', cleanup);
  211. }
  212. /**
  213. * Removes any events or leftover elements created by this controller
  214. */
  215. function cleanup () {
  216. if (!ctrl.hidden) {
  217. $mdUtil.enableScrolling();
  218. }
  219. angular.element($window).off('resize', debouncedOnResize);
  220. if ( elements ){
  221. var items = ['ul', 'scroller', 'scrollContainer', 'input'];
  222. angular.forEach(items, function(key){
  223. elements.$[key].remove();
  224. });
  225. }
  226. }
  227. /**
  228. * Event handler to be called whenever the window resizes.
  229. */
  230. function onWindowResize() {
  231. if (!ctrl.hidden) {
  232. positionDropdown();
  233. }
  234. }
  235. /**
  236. * Gathers all of the elements needed for this controller
  237. */
  238. function gatherElements () {
  239. var snapWrap = gatherSnapWrap();
  240. elements = {
  241. main: $element[0],
  242. scrollContainer: $element[0].querySelector('.md-virtual-repeat-container'),
  243. scroller: $element[0].querySelector('.md-virtual-repeat-scroller'),
  244. ul: $element.find('ul')[0],
  245. input: $element.find('input')[0],
  246. wrap: snapWrap.wrap,
  247. snap: snapWrap.snap,
  248. root: document.body
  249. };
  250. elements.li = elements.ul.getElementsByTagName('li');
  251. elements.$ = getAngularElements(elements);
  252. inputModelCtrl = elements.$.input.controller('ngModel');
  253. }
  254. /**
  255. * Gathers the snap and wrap elements
  256. *
  257. */
  258. function gatherSnapWrap() {
  259. var element;
  260. var value;
  261. for (element = $element; element.length; element = element.parent()) {
  262. value = element.attr('md-autocomplete-snap');
  263. if (angular.isDefined(value)) break;
  264. }
  265. if (element.length) {
  266. return {
  267. snap: element[0],
  268. wrap: (value.toLowerCase() === 'width') ? element[0] : $element.find('md-autocomplete-wrap')[0]
  269. };
  270. }
  271. var wrap = $element.find('md-autocomplete-wrap')[0];
  272. return {
  273. snap: wrap,
  274. wrap: wrap
  275. };
  276. }
  277. /**
  278. * Gathers angular-wrapped versions of each element
  279. * @param elements
  280. * @returns {{}}
  281. */
  282. function getAngularElements (elements) {
  283. var obj = {};
  284. for (var key in elements) {
  285. if (elements.hasOwnProperty(key)) obj[ key ] = angular.element(elements[ key ]);
  286. }
  287. return obj;
  288. }
  289. //-- event/change handlers
  290. /**
  291. * Handles changes to the `hidden` property.
  292. * @param hidden
  293. * @param oldHidden
  294. */
  295. function handleHiddenChange (hidden, oldHidden) {
  296. if (!hidden && oldHidden) {
  297. positionDropdown();
  298. // Report in polite mode, because the screenreader should finish the default description of
  299. // the input. element.
  300. reportMessages(true, ReportType.Count | ReportType.Selected);
  301. if (elements) {
  302. $mdUtil.disableScrollAround(elements.ul);
  303. enableWrapScroll = disableElementScrollEvents(angular.element(elements.wrap));
  304. }
  305. } else if (hidden && !oldHidden) {
  306. $mdUtil.enableScrolling();
  307. if (enableWrapScroll) {
  308. enableWrapScroll();
  309. enableWrapScroll = null;
  310. }
  311. }
  312. }
  313. /**
  314. * Disables scrolling for a specific element
  315. */
  316. function disableElementScrollEvents(element) {
  317. function preventDefault(e) {
  318. e.preventDefault();
  319. }
  320. element.on('wheel', preventDefault);
  321. element.on('touchmove', preventDefault);
  322. return function() {
  323. element.off('wheel', preventDefault);
  324. element.off('touchmove', preventDefault);
  325. };
  326. }
  327. /**
  328. * When the user mouses over the dropdown menu, ignore blur events.
  329. */
  330. function onListEnter () {
  331. noBlur = true;
  332. }
  333. /**
  334. * When the user's mouse leaves the menu, blur events may hide the menu again.
  335. */
  336. function onListLeave () {
  337. if (!hasFocus && !ctrl.hidden) elements.input.focus();
  338. noBlur = false;
  339. ctrl.hidden = shouldHide();
  340. }
  341. /**
  342. * When the mouse button is released, send focus back to the input field.
  343. */
  344. function onMouseup () {
  345. elements.input.focus();
  346. }
  347. /**
  348. * Handles changes to the selected item.
  349. * @param selectedItem
  350. * @param previousSelectedItem
  351. */
  352. function selectedItemChange (selectedItem, previousSelectedItem) {
  353. updateModelValidators();
  354. if (selectedItem) {
  355. getDisplayValue(selectedItem).then(function (val) {
  356. $scope.searchText = val;
  357. handleSelectedItemChange(selectedItem, previousSelectedItem);
  358. });
  359. } else if (previousSelectedItem && $scope.searchText) {
  360. getDisplayValue(previousSelectedItem).then(function(displayValue) {
  361. // Clear the searchText, when the selectedItem is set to null.
  362. // Do not clear the searchText, when the searchText isn't matching with the previous
  363. // selected item.
  364. if (angular.isString($scope.searchText)
  365. && displayValue.toString().toLowerCase() === $scope.searchText.toLowerCase()) {
  366. $scope.searchText = '';
  367. }
  368. });
  369. }
  370. if (selectedItem !== previousSelectedItem) announceItemChange();
  371. }
  372. /**
  373. * Use the user-defined expression to announce changes each time a new item is selected
  374. */
  375. function announceItemChange () {
  376. angular.isFunction($scope.itemChange) && $scope.itemChange(getItemAsNameVal($scope.selectedItem));
  377. }
  378. /**
  379. * Use the user-defined expression to announce changes each time the search text is changed
  380. */
  381. function announceTextChange () {
  382. angular.isFunction($scope.textChange) && $scope.textChange();
  383. }
  384. /**
  385. * Calls any external watchers listening for the selected item. Used in conjunction with
  386. * `registerSelectedItemWatcher`.
  387. * @param selectedItem
  388. * @param previousSelectedItem
  389. */
  390. function handleSelectedItemChange (selectedItem, previousSelectedItem) {
  391. selectedItemWatchers.forEach(function (watcher) { watcher(selectedItem, previousSelectedItem); });
  392. }
  393. /**
  394. * Register a function to be called when the selected item changes.
  395. * @param cb
  396. */
  397. function registerSelectedItemWatcher (cb) {
  398. if (selectedItemWatchers.indexOf(cb) == -1) {
  399. selectedItemWatchers.push(cb);
  400. }
  401. }
  402. /**
  403. * Unregister a function previously registered for selected item changes.
  404. * @param cb
  405. */
  406. function unregisterSelectedItemWatcher (cb) {
  407. var i = selectedItemWatchers.indexOf(cb);
  408. if (i != -1) {
  409. selectedItemWatchers.splice(i, 1);
  410. }
  411. }
  412. /**
  413. * Handles changes to the searchText property.
  414. * @param searchText
  415. * @param previousSearchText
  416. */
  417. function handleSearchText (searchText, previousSearchText) {
  418. ctrl.index = getDefaultIndex();
  419. // do nothing on init
  420. if (searchText === previousSearchText) return;
  421. updateModelValidators();
  422. getDisplayValue($scope.selectedItem).then(function (val) {
  423. // clear selected item if search text no longer matches it
  424. if (searchText !== val) {
  425. $scope.selectedItem = null;
  426. // trigger change event if available
  427. if (searchText !== previousSearchText) announceTextChange();
  428. // cancel results if search text is not long enough
  429. if (!isMinLengthMet()) {
  430. ctrl.matches = [];
  431. setLoading(false);
  432. reportMessages(false, ReportType.Count);
  433. } else {
  434. handleQuery();
  435. }
  436. }
  437. });
  438. }
  439. /**
  440. * Handles input blur event, determines if the dropdown should hide.
  441. */
  442. function blur($event) {
  443. hasFocus = false;
  444. if (!noBlur) {
  445. ctrl.hidden = shouldHide();
  446. evalAttr('ngBlur', { $event: $event });
  447. }
  448. }
  449. /**
  450. * Force blur on input element
  451. * @param forceBlur
  452. */
  453. function doBlur(forceBlur) {
  454. if (forceBlur) {
  455. noBlur = false;
  456. hasFocus = false;
  457. }
  458. elements.input.blur();
  459. }
  460. /**
  461. * Handles input focus event, determines if the dropdown should show.
  462. */
  463. function focus($event) {
  464. hasFocus = true;
  465. if (isSearchable() && isMinLengthMet()) {
  466. handleQuery();
  467. }
  468. ctrl.hidden = shouldHide();
  469. evalAttr('ngFocus', { $event: $event });
  470. }
  471. /**
  472. * Handles keyboard input.
  473. * @param event
  474. */
  475. function keydown (event) {
  476. switch (event.keyCode) {
  477. case $mdConstant.KEY_CODE.DOWN_ARROW:
  478. if (ctrl.loading) return;
  479. event.stopPropagation();
  480. event.preventDefault();
  481. ctrl.index = Math.min(ctrl.index + 1, ctrl.matches.length - 1);
  482. updateScroll();
  483. reportMessages(false, ReportType.Selected);
  484. break;
  485. case $mdConstant.KEY_CODE.UP_ARROW:
  486. if (ctrl.loading) return;
  487. event.stopPropagation();
  488. event.preventDefault();
  489. ctrl.index = ctrl.index < 0 ? ctrl.matches.length - 1 : Math.max(0, ctrl.index - 1);
  490. updateScroll();
  491. reportMessages(false, ReportType.Selected);
  492. break;
  493. case $mdConstant.KEY_CODE.TAB:
  494. // If we hit tab, assume that we've left the list so it will close
  495. onListLeave();
  496. if (ctrl.hidden || ctrl.loading || ctrl.index < 0 || ctrl.matches.length < 1) return;
  497. select(ctrl.index);
  498. break;
  499. case $mdConstant.KEY_CODE.ENTER:
  500. if (ctrl.hidden || ctrl.loading || ctrl.index < 0 || ctrl.matches.length < 1) return;
  501. if (hasSelection()) return;
  502. event.stopPropagation();
  503. event.preventDefault();
  504. select(ctrl.index);
  505. break;
  506. case $mdConstant.KEY_CODE.ESCAPE:
  507. event.preventDefault(); // Prevent browser from always clearing input
  508. if (!shouldProcessEscape()) return;
  509. event.stopPropagation();
  510. clearSelectedItem();
  511. if ($scope.searchText && hasEscapeOption('clear')) {
  512. clearSearchText();
  513. }
  514. // Manually hide (needed for mdNotFound support)
  515. ctrl.hidden = true;
  516. if (hasEscapeOption('blur')) {
  517. // Force the component to blur if they hit escape
  518. doBlur(true);
  519. }
  520. break;
  521. default:
  522. }
  523. }
  524. //-- getters
  525. /**
  526. * Returns the minimum length needed to display the dropdown.
  527. * @returns {*}
  528. */
  529. function getMinLength () {
  530. return angular.isNumber($scope.minLength) ? $scope.minLength : 1;
  531. }
  532. /**
  533. * Returns the display value for an item.
  534. * @param item
  535. * @returns {*}
  536. */
  537. function getDisplayValue (item) {
  538. return $q.when(getItemText(item) || item).then(function(itemText) {
  539. if (itemText && !angular.isString(itemText)) {
  540. $log.warn('md-autocomplete: Could not resolve display value to a string. ' +
  541. 'Please check the `md-item-text` attribute.');
  542. }
  543. return itemText;
  544. });
  545. /**
  546. * Getter function to invoke user-defined expression (in the directive)
  547. * to convert your object to a single string.
  548. */
  549. function getItemText (item) {
  550. return (item && $scope.itemText) ? $scope.itemText(getItemAsNameVal(item)) : null;
  551. }
  552. }
  553. /**
  554. * Returns the locals object for compiling item templates.
  555. * @param item
  556. * @returns {{}}
  557. */
  558. function getItemAsNameVal (item) {
  559. if (!item) return undefined;
  560. var locals = {};
  561. if (ctrl.itemName) locals[ ctrl.itemName ] = item;
  562. return locals;
  563. }
  564. /**
  565. * Returns the default index based on whether or not autoselect is enabled.
  566. * @returns {number}
  567. */
  568. function getDefaultIndex () {
  569. return $scope.autoselect ? 0 : -1;
  570. }
  571. /**
  572. * Sets the loading parameter and updates the hidden state.
  573. * @param value {boolean} Whether or not the component is currently loading.
  574. */
  575. function setLoading(value) {
  576. if (ctrl.loading != value) {
  577. ctrl.loading = value;
  578. }
  579. // Always refresh the hidden variable as something else might have changed
  580. ctrl.hidden = shouldHide();
  581. }
  582. /**
  583. * Determines if the menu should be hidden.
  584. * @returns {boolean}
  585. */
  586. function shouldHide () {
  587. if (!isSearchable()) return true; // Hide when not able to query
  588. else return !shouldShow(); // Hide when the dropdown is not able to show.
  589. }
  590. /**
  591. * Determines whether the autocomplete is able to query within the current state.
  592. * @returns {boolean}
  593. */
  594. function isSearchable() {
  595. if (ctrl.loading && !hasMatches()) return false; // No query when query is in progress.
  596. else if (hasSelection()) return false; // No query if there is already a selection
  597. else if (!hasFocus) return false; // No query if the input does not have focus
  598. return true;
  599. }
  600. /**
  601. * Determines if the escape keydown should be processed
  602. * @returns {boolean}
  603. */
  604. function shouldProcessEscape() {
  605. return hasEscapeOption('blur') || !ctrl.hidden || ctrl.loading || hasEscapeOption('clear') && $scope.searchText;
  606. }
  607. /**
  608. * Determines if an escape option is set
  609. * @returns {boolean}
  610. */
  611. function hasEscapeOption(option) {
  612. return !$scope.escapeOptions || $scope.escapeOptions.toLowerCase().indexOf(option) !== -1;
  613. }
  614. /**
  615. * Determines if the menu should be shown.
  616. * @returns {boolean}
  617. */
  618. function shouldShow() {
  619. return (isMinLengthMet() && hasMatches()) || notFoundVisible();
  620. }
  621. /**
  622. * Returns true if the search text has matches.
  623. * @returns {boolean}
  624. */
  625. function hasMatches() {
  626. return ctrl.matches.length ? true : false;
  627. }
  628. /**
  629. * Returns true if the autocomplete has a valid selection.
  630. * @returns {boolean}
  631. */
  632. function hasSelection() {
  633. return ctrl.scope.selectedItem ? true : false;
  634. }
  635. /**
  636. * Returns true if the loading indicator is, or should be, visible.
  637. * @returns {boolean}
  638. */
  639. function loadingIsVisible() {
  640. return ctrl.loading && !hasSelection();
  641. }
  642. /**
  643. * Returns the display value of the current item.
  644. * @returns {*}
  645. */
  646. function getCurrentDisplayValue () {
  647. return getDisplayValue(ctrl.matches[ ctrl.index ]);
  648. }
  649. /**
  650. * Determines if the minimum length is met by the search text.
  651. * @returns {*}
  652. */
  653. function isMinLengthMet () {
  654. return ($scope.searchText || '').length >= getMinLength();
  655. }
  656. //-- actions
  657. /**
  658. * Defines a public property with a handler and a default value.
  659. * @param key
  660. * @param handler
  661. * @param value
  662. */
  663. function defineProperty (key, handler, value) {
  664. Object.defineProperty(ctrl, key, {
  665. get: function () { return value; },
  666. set: function (newValue) {
  667. var oldValue = value;
  668. value = newValue;
  669. handler(newValue, oldValue);
  670. }
  671. });
  672. }
  673. /**
  674. * Selects the item at the given index.
  675. * @param index
  676. */
  677. function select (index) {
  678. //-- force form to update state for validation
  679. $mdUtil.nextTick(function () {
  680. getDisplayValue(ctrl.matches[ index ]).then(function (val) {
  681. var ngModel = elements.$.input.controller('ngModel');
  682. ngModel.$setViewValue(val);
  683. ngModel.$render();
  684. }).finally(function () {
  685. $scope.selectedItem = ctrl.matches[ index ];
  686. setLoading(false);
  687. });
  688. }, false);
  689. }
  690. /**
  691. * Clears the searchText value and selected item.
  692. */
  693. function clearValue () {
  694. clearSelectedItem();
  695. clearSearchText();
  696. }
  697. /**
  698. * Clears the selected item
  699. */
  700. function clearSelectedItem () {
  701. // Reset our variables
  702. ctrl.index = 0;
  703. ctrl.matches = [];
  704. }
  705. /**
  706. * Clears the searchText value
  707. */
  708. function clearSearchText () {
  709. // Set the loading to true so we don't see flashes of content.
  710. // The flashing will only occur when an async request is running.
  711. // So the loading process will stop when the results had been retrieved.
  712. setLoading(true);
  713. $scope.searchText = '';
  714. // Normally, triggering the change / input event is unnecessary, because the browser detects it properly.
  715. // But some browsers are not detecting it properly, which means that we have to trigger the event.
  716. // Using the `input` is not working properly, because for example IE11 is not supporting the `input` event.
  717. // The `change` event is a good alternative and is supported by all supported browsers.
  718. var eventObj = document.createEvent('CustomEvent');
  719. eventObj.initCustomEvent('change', true, true, { value: '' });
  720. elements.input.dispatchEvent(eventObj);
  721. // For some reason, firing the above event resets the value of $scope.searchText if
  722. // $scope.searchText has a space character at the end, so we blank it one more time and then
  723. // focus.
  724. elements.input.blur();
  725. $scope.searchText = '';
  726. elements.input.focus();
  727. }
  728. /**
  729. * Fetches the results for the provided search text.
  730. * @param searchText
  731. */
  732. function fetchResults (searchText) {
  733. var items = $scope.$parent.$eval(itemExpr),
  734. term = searchText.toLowerCase(),
  735. isList = angular.isArray(items),
  736. isPromise = !!items.then; // Every promise should contain a `then` property
  737. if (isList) onResultsRetrieved(items);
  738. else if (isPromise) handleAsyncResults(items);
  739. function handleAsyncResults(items) {
  740. if ( !items ) return;
  741. items = $q.when(items);
  742. fetchesInProgress++;
  743. setLoading(true);
  744. $mdUtil.nextTick(function () {
  745. items
  746. .then(onResultsRetrieved)
  747. .finally(function(){
  748. if (--fetchesInProgress === 0) {
  749. setLoading(false);
  750. }
  751. });
  752. },true, $scope);
  753. }
  754. function onResultsRetrieved(matches) {
  755. cache[term] = matches;
  756. // Just cache the results if the request is now outdated.
  757. // The request becomes outdated, when the new searchText has changed during the result fetching.
  758. if ((searchText || '') !== ($scope.searchText || '')) {
  759. return;
  760. }
  761. handleResults(matches);
  762. }
  763. }
  764. /**
  765. * Reports given message types to supported screenreaders.
  766. * @param {boolean} isPolite Whether the announcement should be polite.
  767. * @param {!number} types Message flags to be reported to the screenreader.
  768. */
  769. function reportMessages(isPolite, types) {
  770. var politeness = isPolite ? 'polite' : 'assertive';
  771. var messages = [];
  772. if (types & ReportType.Selected && ctrl.index !== -1) {
  773. messages.push(getCurrentDisplayValue());
  774. }
  775. if (types & ReportType.Count) {
  776. messages.push($q.resolve(getCountMessage()));
  777. }
  778. $q.all(messages).then(function(data) {
  779. $mdLiveAnnouncer.announce(data.join(' '), politeness);
  780. });
  781. }
  782. /**
  783. * Returns the ARIA message for how many results match the current query.
  784. * @returns {*}
  785. */
  786. function getCountMessage () {
  787. switch (ctrl.matches.length) {
  788. case 0:
  789. return 'There are no matches available.';
  790. case 1:
  791. return 'There is 1 match available.';
  792. default:
  793. return 'There are ' + ctrl.matches.length + ' matches available.';
  794. }
  795. }
  796. /**
  797. * Makes sure that the focused element is within view.
  798. */
  799. function updateScroll () {
  800. if (!elements.li[0]) return;
  801. var height = elements.li[0].offsetHeight,
  802. top = height * ctrl.index,
  803. bot = top + height,
  804. hgt = elements.scroller.clientHeight,
  805. scrollTop = elements.scroller.scrollTop;
  806. if (top < scrollTop) {
  807. scrollTo(top);
  808. } else if (bot > scrollTop + hgt) {
  809. scrollTo(bot - hgt);
  810. }
  811. }
  812. function isPromiseFetching() {
  813. return fetchesInProgress !== 0;
  814. }
  815. function scrollTo (offset) {
  816. elements.$.scrollContainer.controller('mdVirtualRepeatContainer').scrollTo(offset);
  817. }
  818. function notFoundVisible () {
  819. var textLength = (ctrl.scope.searchText || '').length;
  820. return ctrl.hasNotFound && !hasMatches() && (!ctrl.loading || isPromiseFetching()) && textLength >= getMinLength() && (hasFocus || noBlur) && !hasSelection();
  821. }
  822. /**
  823. * Starts the query to gather the results for the current searchText. Attempts to return cached
  824. * results first, then forwards the process to `fetchResults` if necessary.
  825. */
  826. function handleQuery () {
  827. var searchText = $scope.searchText || '';
  828. var term = searchText.toLowerCase();
  829. // If caching is enabled and the current searchText is stored in the cache
  830. if (!$scope.noCache && cache[term]) {
  831. // The results should be handled as same as a normal un-cached request does.
  832. handleResults(cache[term]);
  833. } else {
  834. fetchResults(searchText);
  835. }
  836. ctrl.hidden = shouldHide();
  837. }
  838. /**
  839. * Handles the retrieved results by showing them in the autocompletes dropdown.
  840. * @param results Retrieved results
  841. */
  842. function handleResults(results) {
  843. ctrl.matches = results;
  844. ctrl.hidden = shouldHide();
  845. // If loading is in progress, then we'll end the progress. This is needed for example,
  846. // when the `clear` button was clicked, because there we always show the loading process, to prevent flashing.
  847. if (ctrl.loading) setLoading(false);
  848. if ($scope.selectOnMatch) selectItemOnMatch();
  849. positionDropdown();
  850. reportMessages(true, ReportType.Count);
  851. }
  852. /**
  853. * If there is only one matching item and the search text matches its display value exactly,
  854. * automatically select that item. Note: This function is only called if the user uses the
  855. * `md-select-on-match` flag.
  856. */
  857. function selectItemOnMatch () {
  858. var searchText = $scope.searchText,
  859. matches = ctrl.matches,
  860. item = matches[ 0 ];
  861. if (matches.length === 1) getDisplayValue(item).then(function (displayValue) {
  862. var isMatching = searchText == displayValue;
  863. if ($scope.matchInsensitive && !isMatching) {
  864. isMatching = searchText.toLowerCase() == displayValue.toLowerCase();
  865. }
  866. if (isMatching) select(0);
  867. });
  868. }
  869. /**
  870. * Evaluates an attribute expression against the parent scope.
  871. * @param {String} attr Name of the attribute to be evaluated.
  872. * @param {Object?} locals Properties to be injected into the evaluation context.
  873. */
  874. function evalAttr(attr, locals) {
  875. if ($attrs[attr]) {
  876. $scope.$parent.$eval($attrs[attr], locals || {});
  877. }
  878. }
  879. }
  880. MdAutocomplete['$inject'] = ["$$mdSvgRegistry"];angular
  881. .module('material.components.autocomplete')
  882. .directive('mdAutocomplete', MdAutocomplete);
  883. /**
  884. * @ngdoc directive
  885. * @name mdAutocomplete
  886. * @module material.components.autocomplete
  887. *
  888. * @description
  889. * `<md-autocomplete>` is a special input component with a drop-down of all possible matches to a
  890. * custom query. This component allows you to provide real-time suggestions as the user types
  891. * in the input area.
  892. *
  893. * To start, you will need to specify the required parameters and provide a template for your
  894. * results. The content inside `md-autocomplete` will be treated as a template.
  895. *
  896. * In more complex cases, you may want to include other content such as a message to display when
  897. * no matches were found. You can do this by wrapping your template in `md-item-template` and
  898. * adding a tag for `md-not-found`. An example of this is shown below.
  899. *
  900. * To reset the displayed value you must clear both values for `md-search-text` and `md-selected-item`.
  901. *
  902. * ### Validation
  903. *
  904. * You can use `ng-messages` to include validation the same way that you would normally validate;
  905. * however, if you want to replicate a standard input with a floating label, you will have to
  906. * do the following:
  907. *
  908. * - Make sure that your template is wrapped in `md-item-template`
  909. * - Add your `ng-messages` code inside of `md-autocomplete`
  910. * - Add your validation properties to `md-autocomplete` (ie. `required`)
  911. * - Add a `name` to `md-autocomplete` (to be used on the generated `input`)
  912. *
  913. * There is an example below of how this should look.
  914. *
  915. * ### Snapping Drop-Down
  916. *
  917. * You can cause the autocomplete drop-down to snap to an ancestor element by applying the
  918. * `md-autocomplete-snap` attribute to that element. You can also snap to the width of
  919. * the `md-autocomplete-snap` element by setting the attribute's value to `width`
  920. * (ie. `md-autocomplete-snap="width"`).
  921. *
  922. * ### Notes
  923. *
  924. * **Autocomplete Dropdown Items Rendering**
  925. *
  926. * The `md-autocomplete` uses the the <a ng-href="api/directive/mdVirtualRepeatContainer">VirtualRepeat</a>
  927. * directive for displaying the results inside of the dropdown.<br/>
  928. *
  929. * > When encountering issues regarding the item template please take a look at the
  930. * <a ng-href="api/directive/mdVirtualRepeatContainer">VirtualRepeatContainer</a> documentation.
  931. *
  932. * **Autocomplete inside of a Virtual Repeat**
  933. *
  934. * When using the `md-autocomplete` directive inside of a
  935. * <a ng-href="api/directive/mdVirtualRepeatContainer">VirtualRepeatContainer</a> the dropdown items might
  936. * not update properly, because caching of the results is enabled by default.
  937. *
  938. * The autocomplete will then show invalid dropdown items, because the VirtualRepeat only updates the
  939. * scope bindings, rather than re-creating the `md-autocomplete` and the previous cached results will be used.
  940. *
  941. * > To avoid such problems ensure that the autocomplete does not cache any results.
  942. *
  943. * <hljs lang="html">
  944. * <md-autocomplete
  945. * md-no-cache="true"
  946. * md-selected-item="selectedItem"
  947. * md-items="item in items"
  948. * md-search-text="searchText"
  949. * md-item-text="item.display">
  950. * <span>{{ item.display }}</span>
  951. * </md-autocomplete>
  952. * </hljs>
  953. *
  954. *
  955. * @param {expression} md-items An expression in the format of `item in results` to iterate over
  956. * matches for your search.<br/><br/>
  957. * The `results` expression can be also a function, which returns the results synchronously
  958. * or asynchronously (per Promise).
  959. * @param {expression=} md-selected-item-change An expression to be run each time a new item is
  960. * selected.
  961. * @param {expression=} md-search-text-change An expression to be run each time the search text
  962. * updates.
  963. * @param {expression=} md-search-text A model to bind the search query text to.
  964. * @param {object=} md-selected-item A model to bind the selected item to.
  965. * @param {expression=} md-item-text An expression that will convert your object to a single string.
  966. * @param {string=} placeholder Placeholder text that will be forwarded to the input.
  967. * @param {boolean=} md-no-cache Disables the internal caching that happens in autocomplete.
  968. * @param {boolean=} ng-disabled Determines whether or not to disable the input field.
  969. * @param {boolean=} md-require-match When set to true, the autocomplete will add a validator,
  970. * which will evaluate to false, when no item is currently selected.
  971. * @param {number=} md-min-length Specifies the minimum length of text before autocomplete will
  972. * make suggestions.
  973. * @param {number=} md-delay Specifies the amount of time (in milliseconds) to wait before looking
  974. * for results.
  975. * @param {boolean=} md-clear-button Whether the clear button for the autocomplete input should show up or not.
  976. * @param {boolean=} md-autofocus If true, the autocomplete will be automatically focused when a `$mdDialog`,
  977. * `$mdBottomsheet` or `$mdSidenav`, which contains the autocomplete, is opening. <br/><br/>
  978. * Also the autocomplete will immediately focus the input element.
  979. * @param {boolean=} md-no-asterisk When present, asterisk will not be appended to the floating label.
  980. * @param {boolean=} md-autoselect If set to true, the first item will be automatically selected
  981. * in the dropdown upon open.
  982. * @param {string=} md-input-name The name attribute given to the input element to be used with
  983. * FormController.
  984. * @param {string=} md-menu-class This class will be applied to the dropdown menu for styling.
  985. * @param {string=} md-menu-container-class This class will be applied to the parent container
  986. * of the dropdown panel.
  987. * @param {string=} md-input-class This will be applied to the input for styling. This attribute is only valid when a `md-floating-label` is defined
  988. * @param {string=} md-floating-label This will add a floating label to autocomplete and wrap it in
  989. * `md-input-container`
  990. * @param {string=} md-input-name The name attribute given to the input element to be used with
  991. * FormController
  992. * @param {string=} md-select-on-focus When present the inputs text will be automatically selected
  993. * on focus.
  994. * @param {string=} md-input-id An ID to be added to the input element.
  995. * @param {number=} md-input-minlength The minimum length for the input's value for validation.
  996. * @param {number=} md-input-maxlength The maximum length for the input's value for validation.
  997. * @param {boolean=} md-select-on-match When set, autocomplete will automatically select
  998. * the item if the search text is an exact match. <br/><br/>
  999. * An exact match is when only one match is displayed.
  1000. * @param {boolean=} md-match-case-insensitive When set and using `md-select-on-match`, autocomplete
  1001. * will select on case-insensitive match.
  1002. * @param {string=} md-escape-options Override escape key logic. Default is `blur clear`.<br/>
  1003. * Options: `blur`, `clear`, `none`.
  1004. * @param {string=} md-dropdown-items Specifies the maximum amount of items to be shown in
  1005. * the dropdown.<br/><br/>
  1006. * When the dropdown doesn't fit into the viewport, the dropdown will shrink
  1007. * as much as possible.
  1008. * @param {string=} md-dropdown-position Overrides the default dropdown position. Options: `top`, `bottom`.
  1009. * @param {string=} ng-trim If set to false, the search text will be not trimmed automatically.
  1010. * Defaults to true.
  1011. * @param {string=} ng-pattern Adds the pattern validator to the ngModel of the search text.
  1012. * See the [ngPattern Directive](https://docs.angularjs.org/api/ng/directive/ngPattern)
  1013. * for more details.
  1014. *
  1015. * @usage
  1016. * ### Basic Example
  1017. * <hljs lang="html">
  1018. * <md-autocomplete
  1019. * md-selected-item="selectedItem"
  1020. * md-search-text="searchText"
  1021. * md-items="item in getMatches(searchText)"
  1022. * md-item-text="item.display">
  1023. * <span md-highlight-text="searchText">{{item.display}}</span>
  1024. * </md-autocomplete>
  1025. * </hljs>
  1026. *
  1027. * ### Example with "not found" message
  1028. * <hljs lang="html">
  1029. * <md-autocomplete
  1030. * md-selected-item="selectedItem"
  1031. * md-search-text="searchText"
  1032. * md-items="item in getMatches(searchText)"
  1033. * md-item-text="item.display">
  1034. * <md-item-template>
  1035. * <span md-highlight-text="searchText">{{item.display}}</span>
  1036. * </md-item-template>
  1037. * <md-not-found>
  1038. * No matches found.
  1039. * </md-not-found>
  1040. * </md-autocomplete>
  1041. * </hljs>
  1042. *
  1043. * In this example, our code utilizes `md-item-template` and `md-not-found` to specify the
  1044. * different parts that make up our component.
  1045. *
  1046. * ### Clear button for the input
  1047. * By default, for floating label autocomplete's the clear button is not showing up
  1048. * ([See specs](https://material.google.com/components/text-fields.html#text-fields-auto-complete-text-field))
  1049. *
  1050. * Nevertheless, developers are able to explicitly toggle the clear button for all types of autocomplete's.
  1051. *
  1052. * <hljs lang="html">
  1053. * <md-autocomplete ... md-clear-button="true"></md-autocomplete>
  1054. * <md-autocomplete ... md-clear-button="false"></md-autocomplete>
  1055. * </hljs>
  1056. *
  1057. * ### Example with validation
  1058. * <hljs lang="html">
  1059. * <form name="autocompleteForm">
  1060. * <md-autocomplete
  1061. * required
  1062. * md-input-name="autocomplete"
  1063. * md-selected-item="selectedItem"
  1064. * md-search-text="searchText"
  1065. * md-items="item in getMatches(searchText)"
  1066. * md-item-text="item.display">
  1067. * <md-item-template>
  1068. * <span md-highlight-text="searchText">{{item.display}}</span>
  1069. * </md-item-template>
  1070. * <div ng-messages="autocompleteForm.autocomplete.$error">
  1071. * <div ng-message="required">This field is required</div>
  1072. * </div>
  1073. * </md-autocomplete>
  1074. * </form>
  1075. * </hljs>
  1076. *
  1077. * In this example, our code utilizes `md-item-template` and `ng-messages` to specify
  1078. * input validation for the field.
  1079. *
  1080. * ### Asynchronous Results
  1081. * The autocomplete items expression also supports promises, which will resolve with the query results.
  1082. *
  1083. * <hljs lang="js">
  1084. * function AppController($scope, $http) {
  1085. * $scope.query = function(searchText) {
  1086. * return $http
  1087. * .get(BACKEND_URL + '/items/' + searchText)
  1088. * .then(function(data) {
  1089. * // Map the response object to the data object.
  1090. * return data;
  1091. * });
  1092. * };
  1093. * }
  1094. * </hljs>
  1095. *
  1096. * <hljs lang="html">
  1097. * <md-autocomplete
  1098. * md-selected-item="selectedItem"
  1099. * md-search-text="searchText"
  1100. * md-items="item in query(searchText)">
  1101. * <md-item-template>
  1102. * <span md-highlight-text="searchText">{{item}}</span>
  1103. * </md-item-template>
  1104. * </md-autocomplete>
  1105. * </hljs>
  1106. *
  1107. */
  1108. function MdAutocomplete ($$mdSvgRegistry) {
  1109. return {
  1110. controller: 'MdAutocompleteCtrl',
  1111. controllerAs: '$mdAutocompleteCtrl',
  1112. scope: {
  1113. inputName: '@mdInputName',
  1114. inputMinlength: '@mdInputMinlength',
  1115. inputMaxlength: '@mdInputMaxlength',
  1116. searchText: '=?mdSearchText',
  1117. selectedItem: '=?mdSelectedItem',
  1118. itemsExpr: '@mdItems',
  1119. itemText: '&mdItemText',
  1120. placeholder: '@placeholder',
  1121. noCache: '=?mdNoCache',
  1122. requireMatch: '=?mdRequireMatch',
  1123. selectOnMatch: '=?mdSelectOnMatch',
  1124. matchInsensitive: '=?mdMatchCaseInsensitive',
  1125. itemChange: '&?mdSelectedItemChange',
  1126. textChange: '&?mdSearchTextChange',
  1127. minLength: '=?mdMinLength',
  1128. delay: '=?mdDelay',
  1129. autofocus: '=?mdAutofocus',
  1130. floatingLabel: '@?mdFloatingLabel',
  1131. autoselect: '=?mdAutoselect',
  1132. menuClass: '@?mdMenuClass',
  1133. menuContainerClass: '@?mdMenuContainerClass',
  1134. inputClass: '@?mdInputClass',
  1135. inputId: '@?mdInputId',
  1136. escapeOptions: '@?mdEscapeOptions',
  1137. dropdownItems: '=?mdDropdownItems',
  1138. dropdownPosition: '@?mdDropdownPosition',
  1139. clearButton: '=?mdClearButton'
  1140. },
  1141. compile: function(tElement, tAttrs) {
  1142. var attributes = ['md-select-on-focus', 'md-no-asterisk', 'ng-trim', 'ng-pattern'];
  1143. var input = tElement.find('input');
  1144. attributes.forEach(function(attribute) {
  1145. var attrValue = tAttrs[tAttrs.$normalize(attribute)];
  1146. if (attrValue !== null) {
  1147. input.attr(attribute, attrValue);
  1148. }
  1149. });
  1150. return function(scope, element, attrs, ctrl) {
  1151. // Retrieve the state of using a md-not-found template by using our attribute, which will
  1152. // be added to the element in the template function.
  1153. ctrl.hasNotFound = !!element.attr('md-has-not-found');
  1154. // By default the inset autocomplete should show the clear button when not explicitly overwritten.
  1155. if (!angular.isDefined(attrs.mdClearButton) && !scope.floatingLabel) {
  1156. scope.clearButton = true;
  1157. }
  1158. };
  1159. },
  1160. template: function (element, attr) {
  1161. var noItemsTemplate = getNoItemsTemplate(),
  1162. itemTemplate = getItemTemplate(),
  1163. leftover = element.html(),
  1164. tabindex = attr.tabindex;
  1165. var menuContainerClass = attr.mdMenuContainerClass ? ' ' + attr.mdMenuContainerClass : '';
  1166. // Set our attribute for the link function above which runs later.
  1167. // We will set an attribute, because otherwise the stored variables will be trashed when
  1168. // removing the element is hidden while retrieving the template. For example when using ngIf.
  1169. if (noItemsTemplate) element.attr('md-has-not-found', true);
  1170. // Always set our tabindex of the autocomplete directive to -1, because our input
  1171. // will hold the actual tabindex.
  1172. element.attr('tabindex', '-1');
  1173. return '\
  1174. <md-autocomplete-wrap\
  1175. ng-class="{ \'md-whiteframe-z1\': !floatingLabel, \
  1176. \'md-menu-showing\': !$mdAutocompleteCtrl.hidden, \
  1177. \'md-show-clear-button\': !!clearButton }">\
  1178. ' + getInputElement() + '\
  1179. ' + getClearButton() + '\
  1180. <md-progress-linear\
  1181. class="' + (attr.mdFloatingLabel ? 'md-inline' : '') + '"\
  1182. ng-if="$mdAutocompleteCtrl.loadingIsVisible()"\
  1183. md-mode="indeterminate"></md-progress-linear>\
  1184. <md-virtual-repeat-container\
  1185. md-auto-shrink\
  1186. md-auto-shrink-min="1"\
  1187. ng-mouseenter="$mdAutocompleteCtrl.listEnter()"\
  1188. ng-mouseleave="$mdAutocompleteCtrl.listLeave()"\
  1189. ng-mouseup="$mdAutocompleteCtrl.mouseUp()"\
  1190. ng-hide="$mdAutocompleteCtrl.hidden"\
  1191. class="md-autocomplete-suggestions-container md-whiteframe-z1' + menuContainerClass + '"\
  1192. ng-class="{ \'md-not-found\': $mdAutocompleteCtrl.notFoundVisible() }"\
  1193. role="presentation">\
  1194. <ul class="md-autocomplete-suggestions"\
  1195. ng-class="::menuClass"\
  1196. id="ul-{{$mdAutocompleteCtrl.id}}">\
  1197. <li md-virtual-repeat="item in $mdAutocompleteCtrl.matches"\
  1198. ng-class="{ selected: $index === $mdAutocompleteCtrl.index }"\
  1199. ng-click="$mdAutocompleteCtrl.select($index)"\
  1200. md-extra-name="$mdAutocompleteCtrl.itemName">\
  1201. ' + itemTemplate + '\
  1202. </li>' + noItemsTemplate + '\
  1203. </ul>\
  1204. </md-virtual-repeat-container>\
  1205. </md-autocomplete-wrap>';
  1206. function getItemTemplate() {
  1207. var templateTag = element.find('md-item-template').detach(),
  1208. html = templateTag.length ? templateTag.html() : element.html();
  1209. if (!templateTag.length) element.empty();
  1210. return '<md-autocomplete-parent-scope md-autocomplete-replace>' + html + '</md-autocomplete-parent-scope>';
  1211. }
  1212. function getNoItemsTemplate() {
  1213. var templateTag = element.find('md-not-found').detach(),
  1214. template = templateTag.length ? templateTag.html() : '';
  1215. return template
  1216. ? '<li ng-if="$mdAutocompleteCtrl.notFoundVisible()"\
  1217. md-autocomplete-parent-scope>' + template + '</li>'
  1218. : '';
  1219. }
  1220. function getInputElement () {
  1221. if (attr.mdFloatingLabel) {
  1222. return '\
  1223. <md-input-container ng-if="floatingLabel">\
  1224. <label>{{floatingLabel}}</label>\
  1225. <input type="search"\
  1226. ' + (tabindex != null ? 'tabindex="' + tabindex + '"' : '') + '\
  1227. id="{{ inputId || \'fl-input-\' + $mdAutocompleteCtrl.id }}"\
  1228. name="{{inputName}}"\
  1229. ng-class="::inputClass"\
  1230. autocomplete="off"\
  1231. ng-required="$mdAutocompleteCtrl.isRequired"\
  1232. ng-readonly="$mdAutocompleteCtrl.isReadonly"\
  1233. ng-minlength="inputMinlength"\
  1234. ng-maxlength="inputMaxlength"\
  1235. ng-disabled="$mdAutocompleteCtrl.isDisabled"\
  1236. ng-model="$mdAutocompleteCtrl.scope.searchText"\
  1237. ng-model-options="{ allowInvalid: true }"\
  1238. ng-keydown="$mdAutocompleteCtrl.keydown($event)"\
  1239. ng-blur="$mdAutocompleteCtrl.blur($event)"\
  1240. ng-focus="$mdAutocompleteCtrl.focus($event)"\
  1241. aria-owns="ul-{{$mdAutocompleteCtrl.id}}"\
  1242. aria-label="{{floatingLabel}}"\
  1243. aria-autocomplete="list"\
  1244. role="combobox"\
  1245. aria-haspopup="true"\
  1246. aria-activedescendant=""\
  1247. aria-expanded="{{!$mdAutocompleteCtrl.hidden}}"/>\
  1248. <div md-autocomplete-parent-scope md-autocomplete-replace>' + leftover + '</div>\
  1249. </md-input-container>';
  1250. } else {
  1251. return '\
  1252. <input type="search"\
  1253. ' + (tabindex != null ? 'tabindex="' + tabindex + '"' : '') + '\
  1254. id="{{ inputId || \'input-\' + $mdAutocompleteCtrl.id }}"\
  1255. name="{{inputName}}"\
  1256. ng-class="::inputClass"\
  1257. ng-if="!floatingLabel"\
  1258. autocomplete="off"\
  1259. ng-required="$mdAutocompleteCtrl.isRequired"\
  1260. ng-disabled="$mdAutocompleteCtrl.isDisabled"\
  1261. ng-readonly="$mdAutocompleteCtrl.isReadonly"\
  1262. ng-minlength="inputMinlength"\
  1263. ng-maxlength="inputMaxlength"\
  1264. ng-model="$mdAutocompleteCtrl.scope.searchText"\
  1265. ng-keydown="$mdAutocompleteCtrl.keydown($event)"\
  1266. ng-blur="$mdAutocompleteCtrl.blur($event)"\
  1267. ng-focus="$mdAutocompleteCtrl.focus($event)"\
  1268. placeholder="{{placeholder}}"\
  1269. aria-owns="ul-{{$mdAutocompleteCtrl.id}}"\
  1270. aria-label="{{placeholder}}"\
  1271. aria-autocomplete="list"\
  1272. role="combobox"\
  1273. aria-haspopup="true"\
  1274. aria-activedescendant=""\
  1275. aria-expanded="{{!$mdAutocompleteCtrl.hidden}}"/>';
  1276. }
  1277. }
  1278. function getClearButton() {
  1279. return '' +
  1280. '<button ' +
  1281. 'type="button" ' +
  1282. 'aria-label="Clear Input" ' +
  1283. 'tabindex="-1" ' +
  1284. 'ng-if="clearButton && $mdAutocompleteCtrl.scope.searchText" ' +
  1285. 'ng-click="$mdAutocompleteCtrl.clear($event)">' +
  1286. '<md-icon md-svg-src="' + $$mdSvgRegistry.mdClose + '"></md-icon>' +
  1287. '</button>';
  1288. }
  1289. }
  1290. };
  1291. }
  1292. MdAutocompleteItemScopeDirective['$inject'] = ["$compile", "$mdUtil"];angular
  1293. .module('material.components.autocomplete')
  1294. .directive('mdAutocompleteParentScope', MdAutocompleteItemScopeDirective);
  1295. function MdAutocompleteItemScopeDirective($compile, $mdUtil) {
  1296. return {
  1297. restrict: 'AE',
  1298. compile: compile,
  1299. terminal: true,
  1300. transclude: 'element'
  1301. };
  1302. function compile(tElement, tAttr, transclude) {
  1303. return function postLink(scope, element, attr) {
  1304. var ctrl = scope.$mdAutocompleteCtrl;
  1305. var newScope = ctrl.parent.$new();
  1306. var itemName = ctrl.itemName;
  1307. // Watch for changes to our scope's variables and copy them to the new scope
  1308. watchVariable('$index', '$index');
  1309. watchVariable('item', itemName);
  1310. // Ensure that $digest calls on our scope trigger $digest on newScope.
  1311. connectScopes();
  1312. // Link the element against newScope.
  1313. transclude(newScope, function(clone) {
  1314. element.after(clone);
  1315. });
  1316. /**
  1317. * Creates a watcher for variables that are copied from the parent scope
  1318. * @param variable
  1319. * @param alias
  1320. */
  1321. function watchVariable(variable, alias) {
  1322. newScope[alias] = scope[variable];
  1323. scope.$watch(variable, function(value) {
  1324. $mdUtil.nextTick(function() {
  1325. newScope[alias] = value;
  1326. });
  1327. });
  1328. }
  1329. /**
  1330. * Creates watchers on scope and newScope that ensure that for any
  1331. * $digest of scope, newScope is also $digested.
  1332. */
  1333. function connectScopes() {
  1334. var scopeDigesting = false;
  1335. var newScopeDigesting = false;
  1336. scope.$watch(function() {
  1337. if (newScopeDigesting || scopeDigesting) {
  1338. return;
  1339. }
  1340. scopeDigesting = true;
  1341. scope.$$postDigest(function() {
  1342. if (!newScopeDigesting) {
  1343. newScope.$digest();
  1344. }
  1345. scopeDigesting = newScopeDigesting = false;
  1346. });
  1347. });
  1348. newScope.$watch(function() {
  1349. newScopeDigesting = true;
  1350. });
  1351. }
  1352. };
  1353. }
  1354. }
  1355. MdHighlightCtrl['$inject'] = ["$scope", "$element", "$attrs"];angular
  1356. .module('material.components.autocomplete')
  1357. .controller('MdHighlightCtrl', MdHighlightCtrl);
  1358. function MdHighlightCtrl ($scope, $element, $attrs) {
  1359. this.$scope = $scope;
  1360. this.$element = $element;
  1361. this.$attrs = $attrs;
  1362. // Cache the Regex to avoid rebuilding each time.
  1363. this.regex = null;
  1364. }
  1365. MdHighlightCtrl.prototype.init = function(unsafeTermFn, unsafeContentFn) {
  1366. this.flags = this.$attrs.mdHighlightFlags || '';
  1367. this.unregisterFn = this.$scope.$watch(function($scope) {
  1368. return {
  1369. term: unsafeTermFn($scope),
  1370. contentText: unsafeContentFn($scope)
  1371. };
  1372. }.bind(this), this.onRender.bind(this), true);
  1373. this.$element.on('$destroy', this.unregisterFn);
  1374. };
  1375. /**
  1376. * Triggered once a new change has been recognized and the highlighted
  1377. * text needs to be updated.
  1378. */
  1379. MdHighlightCtrl.prototype.onRender = function(state, prevState) {
  1380. var contentText = state.contentText;
  1381. /* Update the regex if it's outdated, because we don't want to rebuilt it constantly. */
  1382. if (this.regex === null || state.term !== prevState.term) {
  1383. this.regex = this.createRegex(state.term, this.flags);
  1384. }
  1385. /* If a term is available apply the regex to the content */
  1386. if (state.term) {
  1387. this.applyRegex(contentText);
  1388. } else {
  1389. this.$element.text(contentText);
  1390. }
  1391. };
  1392. /**
  1393. * Decomposes the specified text into different tokens (whether match or not).
  1394. * Breaking down the string guarantees proper XSS protection due to the native browser
  1395. * escaping of unsafe text.
  1396. */
  1397. MdHighlightCtrl.prototype.applyRegex = function(text) {
  1398. var tokens = this.resolveTokens(text);
  1399. this.$element.empty();
  1400. tokens.forEach(function (token) {
  1401. if (token.isMatch) {
  1402. var tokenEl = angular.element('<span class="highlight">').text(token.text);
  1403. this.$element.append(tokenEl);
  1404. } else {
  1405. this.$element.append(document.createTextNode(token));
  1406. }
  1407. }.bind(this));
  1408. };
  1409. /**
  1410. * Decomposes the specified text into different tokens by running the regex against the text.
  1411. */
  1412. MdHighlightCtrl.prototype.resolveTokens = function(string) {
  1413. var tokens = [];
  1414. var lastIndex = 0;
  1415. // Use replace here, because it supports global and single regular expressions at same time.
  1416. string.replace(this.regex, function(match, index) {
  1417. appendToken(lastIndex, index);
  1418. tokens.push({
  1419. text: match,
  1420. isMatch: true
  1421. });
  1422. lastIndex = index + match.length;
  1423. });
  1424. // Append the missing text as a token.
  1425. appendToken(lastIndex);
  1426. return tokens;
  1427. function appendToken(from, to) {
  1428. var targetText = string.slice(from, to);
  1429. targetText && tokens.push(targetText);
  1430. }
  1431. };
  1432. /** Creates a regex for the specified text with the given flags. */
  1433. MdHighlightCtrl.prototype.createRegex = function(term, flags) {
  1434. var startFlag = '', endFlag = '';
  1435. var regexTerm = this.sanitizeRegex(term);
  1436. if (flags.indexOf('^') >= 0) startFlag = '^';
  1437. if (flags.indexOf('$') >= 0) endFlag = '$';
  1438. return new RegExp(startFlag + regexTerm + endFlag, flags.replace(/[$^]/g, ''));
  1439. };
  1440. /** Sanitizes a regex by removing all common RegExp identifiers */
  1441. MdHighlightCtrl.prototype.sanitizeRegex = function(term) {
  1442. return term && term.toString().replace(/[\\^$*+?.()|{}[\]]/g, '\\$&');
  1443. };
  1444. MdHighlight['$inject'] = ["$interpolate", "$parse"];angular
  1445. .module('material.components.autocomplete')
  1446. .directive('mdHighlightText', MdHighlight);
  1447. /**
  1448. * @ngdoc directive
  1449. * @name mdHighlightText
  1450. * @module material.components.autocomplete
  1451. *
  1452. * @description
  1453. * The `md-highlight-text` directive allows you to specify text that should be highlighted within
  1454. * an element. Highlighted text will be wrapped in `<span class="highlight"></span>` which can
  1455. * be styled through CSS. Please note that child elements may not be used with this directive.
  1456. *
  1457. * @param {string} md-highlight-text A model to be searched for
  1458. * @param {string=} md-highlight-flags A list of flags (loosely based on JavaScript RexExp flags).
  1459. * #### **Supported flags**:
  1460. * - `g`: Find all matches within the provided text
  1461. * - `i`: Ignore case when searching for matches
  1462. * - `$`: Only match if the text ends with the search term
  1463. * - `^`: Only match if the text begins with the search term
  1464. *
  1465. * @usage
  1466. * <hljs lang="html">
  1467. * <input placeholder="Enter a search term..." ng-model="searchTerm" type="text" />
  1468. * <ul>
  1469. * <li ng-repeat="result in results" md-highlight-text="searchTerm">
  1470. * {{result.text}}
  1471. * </li>
  1472. * </ul>
  1473. * </hljs>
  1474. */
  1475. function MdHighlight ($interpolate, $parse) {
  1476. return {
  1477. terminal: true,
  1478. controller: 'MdHighlightCtrl',
  1479. compile: function mdHighlightCompile(tElement, tAttr) {
  1480. var termExpr = $parse(tAttr.mdHighlightText);
  1481. var unsafeContentExpr = $interpolate(tElement.html());
  1482. return function mdHighlightLink(scope, element, attr, ctrl) {
  1483. ctrl.init(termExpr, unsafeContentExpr);
  1484. };
  1485. }
  1486. };
  1487. }
  1488. ngmaterial.components.autocomplete = angular.module("material.components.autocomplete");