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