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