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