text: angular
      .module('material.components.autocomplete')
      .controller('MdAutocompleteCtrl', MdAutocompleteCtrl);
  
  var ITEM_HEIGHT   = 41,
      MAX_HEIGHT    = 5.5 * ITEM_HEIGHT,
      MENU_PADDING  = 8,
      INPUT_PADDING = 2; // Padding provided by `md-input-container`
  
  function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming, $window,
                               $animate, $rootElement, $attrs, $q) {
  
    // Internal Variables.
    var ctrl                 = this,
        itemParts            = $scope.itemsExpr.split(/ in /i),
        itemExpr             = itemParts[ 1 ],
        elements             = null,
        cache                = {},
        noBlur               = false,
        selectedItemWatchers = [],
        hasFocus             = false,
        lastCount            = 0,
        fetchesInProgress    = 0,
        enableWrapScroll     = null,
        inputModelCtrl       = null;
  
    // Public Exported Variables with handlers
    defineProperty('hidden', handleHiddenChange, true);
  
    // Public Exported Variables
    ctrl.scope      = $scope;
    ctrl.parent     = $scope.$parent;
    ctrl.itemName   = itemParts[ 0 ];
    ctrl.matches    = [];
    ctrl.loading    = false;
    ctrl.hidden     = true;
    ctrl.index      = null;
    ctrl.messages   = [];
    ctrl.id         = $mdUtil.nextUid();
    ctrl.isDisabled = null;
    ctrl.isRequired = null;
    ctrl.isReadonly = null;
    ctrl.hasNotFound = false;
  
    // Public Exported Methods
    ctrl.keydown                       = keydown;
    ctrl.blur                          = blur;
    ctrl.focus                         = focus;
    ctrl.clear                         = clearValue;
    ctrl.select                        = select;
    ctrl.listEnter                     = onListEnter;
    ctrl.listLeave                     = onListLeave;
    ctrl.mouseUp                       = onMouseup;
    ctrl.getCurrentDisplayValue        = getCurrentDisplayValue;
    ctrl.registerSelectedItemWatcher   = registerSelectedItemWatcher;
    ctrl.unregisterSelectedItemWatcher = unregisterSelectedItemWatcher;
    ctrl.notFoundVisible               = notFoundVisible;
    ctrl.loadingIsVisible              = loadingIsVisible;
    ctrl.positionDropdown              = positionDropdown;
  
    return init();
  
    //-- initialization methods
  
    /**
     * Initialize the controller, setup watchers, gather elements
     */
    function init () {
      $mdUtil.initOptionalProperties($scope, $attrs, { searchText: '', selectedItem: null });
      $mdTheming($element);
      configureWatchers();
      $mdUtil.nextTick(function () {
        gatherElements();
        moveDropdown();
        focusElement();
        $element.on('focus', focusElement);
      });
    }
  
    function updateModelValidators() {
      if (!$scope.requireMatch || !inputModelCtrl) return;
  
      inputModelCtrl.$setValidity('md-require-match', !!$scope.selectedItem);
    }
  
    /**
     * Calculates the dropdown's position and applies the new styles to the menu element
     * @returns {*}
     */
    function positionDropdown () {
      if (!elements) return $mdUtil.nextTick(positionDropdown, false, $scope);
      var hrect  = elements.wrap.getBoundingClientRect(),
          vrect  = elements.snap.getBoundingClientRect(),
          root   = elements.root.getBoundingClientRect(),
          top    = vrect.bottom - root.top,
          bot    = root.bottom - vrect.top,
          left   = hrect.left - root.left,
          width  = hrect.width,
          offset = getVerticalOffset(),
          styles;
      // Adjust the width to account for the padding provided by `md-input-container`
      if ($attrs.mdFloatingLabel) {
        left += INPUT_PADDING;
        width -= INPUT_PADDING * 2;
      }
      styles = {
        left:     left + 'px',
        minWidth: width + 'px',
        maxWidth: Math.max(hrect.right - root.left, root.right - hrect.left) - MENU_PADDING + 'px'
      };
      if (top > bot && root.height - hrect.bottom - MENU_PADDING < MAX_HEIGHT) {
        styles.top       = 'auto';
        styles.bottom    = bot + 'px';
        styles.maxHeight = Math.min(MAX_HEIGHT, hrect.top - root.top - MENU_PADDING) + 'px';
      } else {
        styles.top       = (top - offset) + 'px';
        styles.bottom    = 'auto';
        styles.maxHeight = Math.min(MAX_HEIGHT, root.bottom + $mdUtil.scrollTop() - hrect.bottom - MENU_PADDING) + 'px';
      }
  
      elements.$.scrollContainer.css(styles);
      $mdUtil.nextTick(correctHorizontalAlignment, false);
  
      /**
       * Calculates the vertical offset for floating label examples to account for ngMessages
       * @returns {number}
       */
      function getVerticalOffset () {
        var offset = 0;
        var inputContainer = $element.find('md-input-container');
        if (inputContainer.length) {
          var input = inputContainer.find('input');
          offset = inputContainer.prop('offsetHeight');
          offset -= input.prop('offsetTop');
          offset -= input.prop('offsetHeight');
          // add in the height left up top for the floating label text
          offset += inputContainer.prop('offsetTop');
        }
        return offset;
      }
  
      /**
       * Makes sure that the menu doesn't go off of the screen on either side.
       */
      function correctHorizontalAlignment () {
        var dropdown = elements.scrollContainer.getBoundingClientRect(),
            styles   = {};
        if (dropdown.right > root.right - MENU_PADDING) {
          styles.left = (hrect.right - dropdown.width) + 'px';
        }
        elements.$.scrollContainer.css(styles);
      }
    }
  
    /**
     * Moves the dropdown menu to the body tag in order to avoid z-index and overflow issues.
     */
    function moveDropdown () {
      if (!elements.$.root.length) return;
      $mdTheming(elements.$.scrollContainer);
      elements.$.scrollContainer.detach();
      elements.$.root.append(elements.$.scrollContainer);
      if ($animate.pin) $animate.pin(elements.$.scrollContainer, $rootElement);
    }
  
    /**
     * Sends focus to the input element.
     */
    function focusElement () {
      if ($scope.autofocus) elements.input.focus();
    }
  
    /**
     * Sets up any watchers used by autocomplete
     */
    function configureWatchers () {
      var wait = parseInt($scope.delay, 10) || 0;
      $attrs.$observe('disabled', function (value) { ctrl.isDisabled = $mdUtil.parseAttributeBoolean(value, false); });
      $attrs.$observe('required', function (value) { ctrl.isRequired = $mdUtil.parseAttributeBoolean(value, false); });
      $attrs.$observe('readonly', function (value) { ctrl.isReadonly = $mdUtil.parseAttributeBoolean(value, false); });
      $scope.$watch('searchText', wait ? $mdUtil.debounce(handleSearchText, wait) : handleSearchText);
      $scope.$watch('selectedItem', selectedItemChange);
      angular.element($window).on('resize', positionDropdown);
      $scope.$on('$destroy', cleanup);
    }
  
    /**
     * Removes any events or leftover elements created by this controller
     */
    function cleanup () {
      if (!ctrl.hidden) {
        $mdUtil.enableScrolling();
      }
  
      angular.element($window).off('resize', positionDropdown);
      if ( elements ){
        var items = 'ul scroller scrollContainer input'.split(' ');
        angular.forEach(items, function(key){
          elements.$[key].remove();
        });
      }
    }
  
    /**
     * Gathers all of the elements needed for this controller
     */
    function gatherElements () {
      elements = {
        main:  $element[0],
        scrollContainer: $element[0].getElementsByClassName('md-virtual-repeat-container')[0],
        scroller: $element[0].getElementsByClassName('md-virtual-repeat-scroller')[0],
        ul:    $element.find('ul')[0],
        input: $element.find('input')[0],
        wrap:  $element.find('md-autocomplete-wrap')[0],
        root:  document.body
      };
  
      elements.li   = elements.ul.getElementsByTagName('li');
      elements.snap = getSnapTarget();
      elements.$    = getAngularElements(elements);
  
      inputModelCtrl = elements.$.input.controller('ngModel');
    }
  
    /**
     * Finds the element that the menu will base its position on
     * @returns {*}
     */
    function getSnapTarget () {
      for (var element = $element; element.length; element = element.parent()) {
        if (angular.isDefined(element.attr('md-autocomplete-snap'))) return element[ 0 ];
      }
      return elements.wrap;
    }
  
    /**
     * Gathers angular-wrapped versions of each element
     * @param elements
     * @returns {{}}
     */
    function getAngularElements (elements) {
      var obj = {};
      for (var key in elements) {
        if (elements.hasOwnProperty(key)) obj[ key ] = angular.element(elements[ key ]);
      }
      return obj;
    }
  
    //-- event/change handlers
  
    /**
     * Handles changes to the `hidden` property.
     * @param hidden
     * @param oldHidden
     */
    function handleHiddenChange (hidden, oldHidden) {
      if (!hidden && oldHidden) {
        positionDropdown();
  
        if (elements) {
          $mdUtil.nextTick(function () {
            $mdUtil.disableScrollAround(elements.ul);
            enableWrapScroll = disableElementScrollEvents(angular.element(elements.wrap));
          }, false, $scope);
        }
      } else if (hidden && !oldHidden) {
        $mdUtil.nextTick(function () {
          $mdUtil.enableScrolling();
  
          if (enableWrapScroll) {
            enableWrapScroll();
            enableWrapScroll = null;
          }
        }, false, $scope);
      }
    }
  
    /**
     * Disables scrolling for a specific element
     */
    function disableElementScrollEvents(element) {
  
      function preventDefault(e) {
        e.preventDefault();
      }
  
      element.on('wheel', preventDefault);
      element.on('touchmove', preventDefault);
  
      return function() {
        element.off('wheel', preventDefault);
        element.off('touchmove', preventDefault);
      }
    }
  
    /**
     * When the user mouses over the dropdown menu, ignore blur events.
     */
    function onListEnter () {
      noBlur = true;
    }
  
    /**
     * When the user's mouse leaves the menu, blur events may hide the menu again.
     */
    function onListLeave () {
      if (!hasFocus && !ctrl.hidden) elements.input.focus();
      noBlur = false;
      ctrl.hidden = shouldHide();
    }
  
    /**
     * When the mouse button is released, send focus back to the input field.
     */
    function onMouseup () {
      elements.input.focus();
    }
  
    /**
     * Handles changes to the selected item.
     * @param selectedItem
     * @param previousSelectedItem
     */
    function selectedItemChange (selectedItem, previousSelectedItem) {
  
      updateModelValidators();
  
      if (selectedItem) {
        getDisplayValue(selectedItem).then(function (val) {
          $scope.searchText = val;
          handleSelectedItemChange(selectedItem, previousSelectedItem);
        });
      } else if (previousSelectedItem) {
        getDisplayValue(previousSelectedItem).then(function(displayValue) {
          // Clear the searchText, when the selectedItem is set to null.
          // Do not clear the searchText, when the searchText isn't matching with the previous
          // selected item.
          if (displayValue.toLowerCase() === $scope.searchText.toLowerCase()) {
            $scope.searchText = '';
          }
        });
      }
  
      if (selectedItem !== previousSelectedItem) announceItemChange();
    }
  
    /**
     * Use the user-defined expression to announce changes each time a new item is selected
     */
    function announceItemChange () {
      angular.isFunction($scope.itemChange) && $scope.itemChange(getItemAsNameVal($scope.selectedItem));
    }
  
    /**
     * Use the user-defined expression to announce changes each time the search text is changed
     */
    function announceTextChange () {
      angular.isFunction($scope.textChange) && $scope.textChange();
    }
  
    /**
     * Calls any external watchers listening for the selected item.  Used in conjunction with
     * `registerSelectedItemWatcher`.
     * @param selectedItem
     * @param previousSelectedItem
     */
    function handleSelectedItemChange (selectedItem, previousSelectedItem) {
      selectedItemWatchers.forEach(function (watcher) { watcher(selectedItem, previousSelectedItem); });
    }
  
    /**
     * Register a function to be called when the selected item changes.
     * @param cb
     */
    function registerSelectedItemWatcher (cb) {
      if (selectedItemWatchers.indexOf(cb) == -1) {
        selectedItemWatchers.push(cb);
      }
    }
  
    /**
     * Unregister a function previously registered for selected item changes.
     * @param cb
     */
    function unregisterSelectedItemWatcher (cb) {
      var i = selectedItemWatchers.indexOf(cb);
      if (i != -1) {
        selectedItemWatchers.splice(i, 1);
      }
    }
  
    /**
     * Handles changes to the searchText property.
     * @param searchText
     * @param previousSearchText
     */
    function handleSearchText (searchText, previousSearchText) {
      ctrl.index = getDefaultIndex();
  
      // do nothing on init
      if (searchText === previousSearchText) return;
  
      updateModelValidators();
  
      getDisplayValue($scope.selectedItem).then(function (val) {
        // clear selected item if search text no longer matches it
        if (searchText !== val) {
          $scope.selectedItem = null;
  
          // trigger change event if available
          if (searchText !== previousSearchText) announceTextChange();
  
          // cancel results if search text is not long enough
          if (!isMinLengthMet()) {
            ctrl.matches = [];
            setLoading(false);
            updateMessages();
          } else {
            handleQuery();
          }
        }
      });
  
    }
  
    /**
     * Handles input blur event, determines if the dropdown should hide.
     */
    function blur () {
      hasFocus = false;
      if (!noBlur) {
        ctrl.hidden = shouldHide();
      }
    }
  
    /**
     * Force blur on input element
     * @param forceBlur
     */
    function doBlur(forceBlur) {
      if (forceBlur) {
        noBlur = false;
        hasFocus = false;
      }
      elements.input.blur();
    }
  
    /**
     * Handles input focus event, determines if the dropdown should show.
     */
    function focus($event) {
      hasFocus = true;
      //-- if searchText is null, let's force it to be a string
      if (!angular.isString($scope.searchText)) $scope.searchText = '';
      ctrl.hidden = shouldHide();
      if (!ctrl.hidden) handleQuery();
    }
  
    /**
     * Handles keyboard input.
     * @param event
     */
    function keydown (event) {
      switch (event.keyCode) {
        case $mdConstant.KEY_CODE.DOWN_ARROW:
          if (ctrl.loading) return;
          event.stopPropagation();
          event.preventDefault();
          ctrl.index   = Math.min(ctrl.index + 1, ctrl.matches.length - 1);
          updateScroll();
          updateMessages();
          break;
        case $mdConstant.KEY_CODE.UP_ARROW:
          if (ctrl.loading) return;
          event.stopPropagation();
          event.preventDefault();
          ctrl.index   = ctrl.index < 0 ? ctrl.matches.length - 1 : Math.max(0, ctrl.index - 1);
          updateScroll();
          updateMessages();
          break;
        case $mdConstant.KEY_CODE.TAB:
          // If we hit tab, assume that we've left the list so it will close
          onListLeave();
  
          if (ctrl.hidden || ctrl.loading || ctrl.index < 0 || ctrl.matches.length < 1) return;
          select(ctrl.index);
          break;
        case $mdConstant.KEY_CODE.ENTER:
          if (ctrl.hidden || ctrl.loading || ctrl.index < 0 || ctrl.matches.length < 1) return;
          if (hasSelection()) return;
          event.stopPropagation();
          event.preventDefault();
          select(ctrl.index);
          break;
        case $mdConstant.KEY_CODE.ESCAPE:
          event.preventDefault(); // Prevent browser from always clearing input
          if (!shouldProcessEscape()) return;
          event.stopPropagation();
  
          clearSelectedItem();
          if ($scope.searchText && hasEscapeOption('clear')) {
            clearSearchText();
          }
  
          // Manually hide (needed for mdNotFound support)
          ctrl.hidden = true;
  
          if (hasEscapeOption('blur')) {
            // Force the component to blur if they hit escape
            doBlur(true);
          }
  
          break;
        default:
      }
    }
  
    //-- getters
  
    /**
     * Returns the minimum length needed to display the dropdown.
     * @returns {*}
     */
    function getMinLength () {
      return angular.isNumber($scope.minLength) ? $scope.minLength : 1;
    }
  
    /**
     * Returns the display value for an item.
     * @param item
     * @returns {*}
     */
    function getDisplayValue (item) {
      return $q.when(getItemText(item) || item);
  
      /**
       * Getter function to invoke user-defined expression (in the directive)
       * to convert your object to a single string.
       */
      function getItemText (item) {
        return (item && $scope.itemText) ? $scope.itemText(getItemAsNameVal(item)) : null;
      }
    }
  
    /**
     * Returns the locals object for compiling item templates.
     * @param item
     * @returns {{}}
     */
    function getItemAsNameVal (item) {
      if (!item) return undefined;
  
      var locals = {};
      if (ctrl.itemName) locals[ ctrl.itemName ] = item;
  
      return locals;
    }
  
    /**
     * Returns the default index based on whether or not autoselect is enabled.
     * @returns {number}
     */
    function getDefaultIndex () {
      return $scope.autoselect ? 0 : -1;
    }
  
    /**
     * Sets the loading parameter and updates the hidden state.
     * @param value {boolean} Whether or not the component is currently loading.
     */
    function setLoading(value) {
      if (ctrl.loading != value) {
        ctrl.loading = value;
      }
  
      // Always refresh the hidden variable as something else might have changed
      ctrl.hidden = shouldHide();
    }
  
    /**
     * Determines if the menu should be hidden.
     * @returns {boolean}
     */
    function shouldHide () {
      if (ctrl.loading && !hasMatches()) return true; // Hide while loading initial matches
      else if (hasSelection()) return true;           // Hide if there is already a selection
      else if (!hasFocus) return true;                // Hide if the input does not have focus
      else return !shouldShow();                      // Defer to standard show logic
    }
  
    /**
     * Determines if the escape keydown should be processed
     * @returns {boolean}
     */
    function shouldProcessEscape() {
      return hasEscapeOption('blur') || !ctrl.hidden || ctrl.loading || hasEscapeOption('clear') && $scope.searchText;
    }
  
    /**
     * Determines if an escape option is set
     * @returns {boolean}
     */
    function hasEscapeOption(option) {
      return !$scope.escapeOptions || $scope.escapeOptions.toLowerCase().indexOf(option) !== -1;
    }
  
    /**
     * Determines if the menu should be shown.
     * @returns {boolean}
     */
    function shouldShow() {
      return (isMinLengthMet() && hasMatches()) || notFoundVisible();
    }
  
    /**
     * Returns true if the search text has matches.
     * @returns {boolean}
     */
    function hasMatches() {
      return ctrl.matches.length ? true : false;
    }
  
    /**
     * Returns true if the autocomplete has a valid selection.
     * @returns {boolean}
     */
    function hasSelection() {
      return ctrl.scope.selectedItem ? true : false;
    }
  
    /**
     * Returns true if the loading indicator is, or should be, visible.
     * @returns {boolean}
     */
    function loadingIsVisible() {
      return ctrl.loading && !hasSelection();
    }
  
    /**
     * Returns the display value of the current item.
     * @returns {*}
     */
    function getCurrentDisplayValue () {
      return getDisplayValue(ctrl.matches[ ctrl.index ]);
    }
  
    /**
     * Determines if the minimum length is met by the search text.
     * @returns {*}
     */
    function isMinLengthMet () {
      return ($scope.searchText || '').length >= getMinLength();
    }
  
    //-- actions
  
    /**
     * Defines a public property with a handler and a default value.
     * @param key
     * @param handler
     * @param value
     */
    function defineProperty (key, handler, value) {
      Object.defineProperty(ctrl, key, {
        get: function () { return value; },
        set: function (newValue) {
          var oldValue = value;
          value        = newValue;
          handler(newValue, oldValue);
        }
      });
    }
  
    /**
     * Selects the item at the given index.
     * @param index
     */
    function select (index) {
      //-- force form to update state for validation
      $mdUtil.nextTick(function () {
        getDisplayValue(ctrl.matches[ index ]).then(function (val) {
          var ngModel = elements.$.input.controller('ngModel');
          ngModel.$setViewValue(val);
          ngModel.$render();
        }).finally(function () {
          $scope.selectedItem = ctrl.matches[ index ];
          setLoading(false);
        });
      }, false);
    }
  
    /**
     * Clears the searchText value and selected item.
     */
    function clearValue () {
      clearSelectedItem();
      clearSearchText();
    }
  
    /**
     * Clears the selected item
     */
    function clearSelectedItem () {
      // Reset our variables
      ctrl.index = 0;
      ctrl.matches = [];
    }
  
    /**
     * Clears the searchText value
     */
    function clearSearchText () {
      // Set the loading to true so we don't see flashes of content.
      // The flashing will only occur when an async request is running.
      // So the loading process will stop when the results had been retrieved.
      setLoading(true);
  
      $scope.searchText = '';
  
      // Normally, triggering the change / input event is unnecessary, because the browser detects it properly.
      // But some browsers are not detecting it properly, which means that we have to trigger the event.
      // Using the `input` is not working properly, because for example IE11 is not supporting the `input` event.
      // The `change` event is a good alternative and is supported by all supported browsers.
      var eventObj = document.createEvent('CustomEvent');
      eventObj.initCustomEvent('change', true, true, { value: '' });
      elements.input.dispatchEvent(eventObj);
  
      // For some reason, firing the above event resets the value of $scope.searchText if
      // $scope.searchText has a space character at the end, so we blank it one more time and then
      // focus.
      elements.input.blur();
      $scope.searchText = '';
      elements.input.focus();
    }
  
    /**
     * Fetches the results for the provided search text.
     * @param searchText
     */
    function fetchResults (searchText) {
      var items = $scope.$parent.$eval(itemExpr),
          term  = searchText.toLowerCase(),
          isList = angular.isArray(items),
          isPromise = !!items.then; // Every promise should contain a `then` property
  
      if (isList) onResultsRetrieved(items);
      else if (isPromise) handleAsyncResults(items);
  
      function handleAsyncResults(items) {
        if ( !items ) return;
  
        items = $q.when(items);
        fetchesInProgress++;
        setLoading(true);
  
        $mdUtil.nextTick(function () {
            items
              .then(onResultsRetrieved)
              .finally(function(){
                if (--fetchesInProgress === 0) {
                  setLoading(false);
                }
              });
        },true, $scope);
      }
  
      function onResultsRetrieved(matches) {
        cache[term] = matches;
  
        // Just cache the results if the request is now outdated.
        // The request becomes outdated, when the new searchText has changed during the result fetching.
        if ((searchText || '') !== ($scope.searchText || '')) {
          return;
        }
  
        handleResults(matches);
      }
    }
  
    /**
     * Updates the ARIA messages
     */
    function updateMessages () {
      getCurrentDisplayValue().then(function (msg) {
        ctrl.messages = [ getCountMessage(), msg ];
      });
    }
  
    /**
     * Returns the ARIA message for how many results match the current query.
     * @returns {*}
     */
    function getCountMessage () {
      if (lastCount === ctrl.matches.length) return '';
      lastCount = ctrl.matches.length;
      switch (ctrl.matches.length) {
        case 0:
          return 'There are no matches available.';
        case 1:
          return 'There is 1 match available.';
        default:
          return 'There are ' + ctrl.matches.length + ' matches available.';
      }
    }
  
    /**
     * Makes sure that the focused element is within view.
     */
    function updateScroll () {
      if (!elements.li[0]) return;
      var height = elements.li[0].offsetHeight,
          top = height * ctrl.index,
          bot = top + height,
          hgt = elements.scroller.clientHeight,
          scrollTop = elements.scroller.scrollTop;
      if (top < scrollTop) {
        scrollTo(top);
      } else if (bot > scrollTop + hgt) {
        scrollTo(bot - hgt);
      }
    }
  
    function isPromiseFetching() {
      return fetchesInProgress !== 0;
    }
  
    function scrollTo (offset) {
      elements.$.scrollContainer.controller('mdVirtualRepeatContainer').scrollTo(offset);
    }
  
    function notFoundVisible () {
      var textLength = (ctrl.scope.searchText || '').length;
  
      return ctrl.hasNotFound && !hasMatches() && (!ctrl.loading || isPromiseFetching()) && textLength >= getMinLength() && (hasFocus || noBlur) && !hasSelection();
    }
  
    /**
     * Starts the query to gather the results for the current searchText.  Attempts to return cached
     * results first, then forwards the process to `fetchResults` if necessary.
     */
    function handleQuery () {
      var searchText = $scope.searchText || '';
      var term = searchText.toLowerCase();
  
      // If caching is enabled and the current searchText is stored in the cache
      if (!$scope.noCache && cache[term]) {
        // The results should be handled as same as a normal un-cached request does.
        handleResults(cache[term]);
      } else {
        fetchResults(searchText);
      }
  
      ctrl.hidden = shouldHide();
    }
  
    /**
     * Handles the retrieved results by showing them in the autocompletes dropdown.
     * @param results Retrieved results
     */
    function handleResults(results) {
      ctrl.matches = results;
      ctrl.hidden  = shouldHide();
  
      // If loading is in progress, then we'll end the progress. This is needed for example,
      // when the `clear` button was clicked, because there we always show the loading process, to prevent flashing.
      if (ctrl.loading) setLoading(false);
  
      if ($scope.selectOnMatch) selectItemOnMatch();
  
      updateMessages();
      positionDropdown();
    }
  
    /**
     * If there is only one matching item and the search text matches its display value exactly,
     * automatically select that item.  Note: This function is only called if the user uses the
     * `md-select-on-match` flag.
     */
    function selectItemOnMatch () {
      var searchText = $scope.searchText,
          matches    = ctrl.matches,
          item       = matches[ 0 ];
      if (matches.length === 1) getDisplayValue(item).then(function (displayValue) {
        var isMatching = searchText == displayValue;
        if ($scope.matchInsensitive && !isMatching) {
          isMatching = searchText.toLowerCase() == displayValue.toLowerCase();
        }
  
        if (isMatching) select(0);
      });
    }
  
  }
  