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