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