arnaucube
arnaucube/socketioMEANseedPublic Self-hosted repository
socketioMEANseed/www/bower_components/angular-material/modules/js/autocomplete/autocomplete.js
www/bower_components/angular-material/modules/js/autocomplete/autocomplete.jsRaw
   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 */
  17angular.module('material.components.autocomplete', [
  18  'material.core',
  19  'material.components.icon',
  20  'material.components.virtualRepeat'
  21]);
  22
  23
  24MdAutocompleteCtrl['$inject'] = ["$scope", "$element", "$mdUtil", "$mdConstant", "$mdTheming", "$window", "$animate", "$rootElement", "$attrs", "$q", "$log", "$mdLiveAnnouncer"];angular
  25    .module('material.components.autocomplete')
  26    .controller('MdAutocompleteCtrl', MdAutocompleteCtrl);
  27
  28var ITEM_HEIGHT   = 48,
  29    MAX_ITEMS     = 5,
  30    MENU_PADDING  = 8,
  31    INPUT_PADDING = 2; // Padding provided by `md-input-container`
  32
  33function 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
1034MdAutocomplete['$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
1259function 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
1453MdAutocompleteItemScopeDirective['$inject'] = ["$compile", "$mdUtil"];angular
1454  .module('material.components.autocomplete')
1455  .directive('mdAutocompleteParentScope', MdAutocompleteItemScopeDirective);
1456
1457function 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
1529MdHighlightCtrl['$inject'] = ["$scope", "$element", "$attrs"];angular
1530    .module('material.components.autocomplete')
1531    .controller('MdHighlightCtrl', MdHighlightCtrl);
1532
1533function 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
1542MdHighlightCtrl.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 */
1560MdHighlightCtrl.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 */
1583MdHighlightCtrl.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 */
1605MdHighlightCtrl.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. */
1633MdHighlightCtrl.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 */
1644MdHighlightCtrl.prototype.sanitizeRegex = function(term) {
1645  return term && term.toString().replace(/[\\\^\$\*\+\?\.\(\)\|\{}\[\]]/g, '\\$&');
1646};
1647
1648
1649MdHighlight['$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
1682function 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);