chromium/third_party/google-closure-library/closure/goog/ui/ac/autocomplete.js

/**
 * @license
 * Copyright The Closure Library Authors.
 * SPDX-License-Identifier: Apache-2.0
 */

/**
 * @fileoverview Gmail-like AutoComplete logic.
 *
 * @see ../../demos/autocomplete-basic.html
 */

goog.provide('goog.ui.ac.AutoComplete');
goog.provide('goog.ui.ac.AutoComplete.EventType');

goog.require('goog.asserts');
goog.require('goog.events');
goog.require('goog.events.EventTarget');
goog.require('goog.object');
goog.require('goog.ui.ac.RenderOptions');
goog.requireType('goog.events.Event');
goog.requireType('goog.ui.ac.InputHandler');


/**
 * This is the central manager class for an AutoComplete instance. The matcher
 * can specify disabled rows that should not be hilited or selected by
 * implementing <code>isRowDisabled(row):boolean</code> for each autocomplete
 * row. No row will be considered disabled if this method is not implemented.
 *
 * @param {Object} matcher A data source and row matcher, implements
 *        <code>requestMatchingRows(token, maxMatches, matchCallback)</code>.
 * @param {goog.events.EventTarget} renderer An object that implements
 *        <code>
 *          isVisible():boolean<br>
 *          renderRows(rows:Array, token:string, target:Element);<br>
 *          hiliteId(row-id:number);<br>
 *          dismiss();<br>
 *          dispose():
 *        </code>.
 * @param {Object} selectionHandler An object that implements
 *        <code>
 *          selectRow(row);<br>
 *          update(opt_force);
 *        </code>.
 *
 * @constructor
 * @extends {goog.events.EventTarget}
 * @suppress {underscore}
 */
goog.ui.ac.AutoComplete = function(matcher, renderer, selectionHandler) {
  'use strict';
  goog.events.EventTarget.call(this);

  /**
   * A data-source which provides autocomplete suggestions.
   *
   * TODO(chrishenry): Tighten the type to !goog.ui.ac.AutoComplete.Matcher.
   *
   * @type {Object}
   * @protected
   * @suppress {underscore|visibility}
   */
  this.matcher_ = matcher;

  /**
   * A handler which interacts with the input DOM element (textfield, textarea,
   * or richedit).
   *
   * TODO(chrishenry): Tighten the type to !Object.
   *
   * @type {Object}
   * @protected
   * @suppress {underscore|visibility}
   */
  this.selectionHandler_ = selectionHandler;

  /**
   * A renderer to render/show/highlight/hide the autocomplete menu.
   * @type {goog.events.EventTarget}
   * @protected
   * @suppress {underscore|visibility}
   */
  this.renderer_ = renderer;
  goog.events.listen(
      renderer,
      [
        goog.ui.ac.AutoComplete.EventType.HILITE,
        goog.ui.ac.AutoComplete.EventType.SELECT,
        goog.ui.ac.AutoComplete.EventType.CANCEL_DISMISS,
        goog.ui.ac.AutoComplete.EventType.DISMISS
      ],
      this.handleEvent, false, this);

  /**
   * Currently typed token which will be used for completion.
   * @type {?string}
   * @protected
   * @suppress {underscore|visibility}
   */
  this.token_ = null;

  /**
   * Autocomplete suggestion items.
   * @type {Array<?>}
   * @protected
   * @suppress {underscore|visibility}
   */
  this.rows_ = [];

  /**
   * Id of the currently highlighted row.
   * @type {number}
   * @protected
   * @suppress {underscore|visibility}
   */
  this.hiliteId_ = -1;

  /**
   * Id of the first row in autocomplete menu. Note that new ids are assigned
   * every time new suggestions are fetched.
   *
   * TODO(chrishenry): Figure out what subclass does with this value
   * and whether we should expose a more proper API.
   *
   * @type {number}
   * @protected
   * @suppress {underscore|visibility}
   */
  this.firstRowId_ = 0;

  /**
   * The target HTML node for displaying.
   * @type {?Element}
   * @protected
   * @suppress {underscore|visibility}
   */
  this.target_ = null;

  /**
   * The timer id for dismissing autocomplete menu with a delay.
   * @type {?number}
   * @private
   */
  this.dismissTimer_ = null;

  /**
   * Mapping from text input element to the anchor element. If the
   * mapping does not exist, the input element will act as the anchor
   * element.
   * @type {Object<Element>}
   * @private
   */
  this.inputToAnchorMap_ = {};
};
goog.inherits(goog.ui.ac.AutoComplete, goog.events.EventTarget);


/**
 * The maximum number of matches that should be returned
 * @type {number}
 * @private
 */
goog.ui.ac.AutoComplete.prototype.maxMatches_ = 10;


/**
 * True iff the first row should automatically be highlighted
 * @type {boolean}
 * @private
 */
goog.ui.ac.AutoComplete.prototype.autoHilite_ = true;


/**
 * True iff the user can unhilight all rows by pressing the up arrow.
 * @type {boolean}
 * @private
 */
goog.ui.ac.AutoComplete.prototype.allowFreeSelect_ = false;


/**
 * True iff item selection should wrap around from last to first. If
 *     allowFreeSelect_ is on in conjunction, there is a step of free selection
 *     before wrapping.
 * @type {boolean}
 * @private
 */
goog.ui.ac.AutoComplete.prototype.wrap_ = false;


/**
 * Whether completion from suggestion triggers fetching new suggestion.
 * @type {boolean}
 * @private
 */
goog.ui.ac.AutoComplete.prototype.triggerSuggestionsOnUpdate_ = false;


/**
 * Events associated with the autocomplete
 * @enum {string}
 */
goog.ui.ac.AutoComplete.EventType = {

  /** A row has been highlighted by the renderer */
  ROW_HILITE: 'rowhilite',

  // Note: The events below are used for internal autocomplete events only and
  // should not be used in non-autocomplete code.

  /** A row has been mouseovered and should be highlighted by the renderer. */
  HILITE: 'hilite',

  /** A row has been selected by the renderer */
  SELECT: 'select',

  /** A dismiss event has occurred */
  DISMISS: 'dismiss',

  /** Event that cancels a dismiss event */
  CANCEL_DISMISS: 'canceldismiss',

  /**
   * Field value was updated.  A row field is included and is non-null when a
   * row has been selected.  The value of the row typically includes fields:
   * contactData and formattedValue as well as a toString function (though none
   * of these fields are guaranteed to exist).  The row field may be used to
   * return custom-type row data.
   */
  UPDATE: 'update',

  /**
   * The list of suggestions has been updated, usually because either the list
   * has opened, or because the user has typed another character and the
   * suggestions have been updated, or the user has dismissed the autocomplete.
   */
  SUGGESTIONS_UPDATE: 'suggestionsupdate'
};


/**
 * @typedef {{
 *   requestMatchingRows:(!Function|undefined),
 *   isRowDisabled:(!Function|undefined)
 * }}
 */
goog.ui.ac.AutoComplete.Matcher;


/**
 * @return {!Object} The data source providing the `autocomplete
 *     suggestions.
 */
goog.ui.ac.AutoComplete.prototype.getMatcher = function() {
  'use strict';
  return goog.asserts.assert(this.matcher_);
};


/**
 * Sets the data source providing the autocomplete suggestions.
 *
 * See constructor documentation for the interface.
 *
 * @param {!Object} matcher The matcher.
 * @protected
 */
goog.ui.ac.AutoComplete.prototype.setMatcher = function(matcher) {
  'use strict';
  this.matcher_ = matcher;
};


/**
 * @return {!Object} The handler used to interact with the input DOM
 *     element (textfield, textarea, or richedit), e.g. to update the
 *     input DOM element with selected value.
 * @protected
 */
goog.ui.ac.AutoComplete.prototype.getSelectionHandler = function() {
  'use strict';
  return goog.asserts.assert(this.selectionHandler_);
};


/**
 * @return {goog.events.EventTarget} The renderer that
 *     renders/shows/highlights/hides the autocomplete menu.
 *     See constructor documentation for the expected renderer API.
 */
goog.ui.ac.AutoComplete.prototype.getRenderer = function() {
  'use strict';
  return this.renderer_;
};


/**
 * Sets the renderer that renders/shows/highlights/hides the autocomplete
 * menu.
 *
 * See constructor documentation for the expected renderer API.
 *
 * @param {goog.events.EventTarget} renderer The renderer.
 * @protected
 */
goog.ui.ac.AutoComplete.prototype.setRenderer = function(renderer) {
  'use strict';
  this.renderer_ = renderer;
};


/**
 * @return {?string} The currently typed token used for completion.
 * @protected
 */
goog.ui.ac.AutoComplete.prototype.getToken = function() {
  'use strict';
  return this.token_;
};


/**
 * Sets the current token (without changing the rendered autocompletion).
 *
 * NOTE(chrishenry): This method will likely go away when we figure
 * out a better API.
 *
 * @param {?string} token The new token.
 * @protected
 */
goog.ui.ac.AutoComplete.prototype.setTokenInternal = function(token) {
  'use strict';
  this.token_ = token;
};


/**
 * @param {number} index The suggestion index, must be within the
 *     interval [0, this.getSuggestionCount()).
 * @return {Object} The currently suggested item at the given index
 *     (or null if there is none).
 */
goog.ui.ac.AutoComplete.prototype.getSuggestion = function(index) {
  'use strict';
  return this.rows_[index];
};


/**
 * @return {!Array<?>} The current autocomplete suggestion items.
 */
goog.ui.ac.AutoComplete.prototype.getAllSuggestions = function() {
  'use strict';
  return goog.asserts.assert(this.rows_);
};


/**
 * @return {number} The number of currently suggested items.
 */
goog.ui.ac.AutoComplete.prototype.getSuggestionCount = function() {
  'use strict';
  return this.rows_.length;
};


/**
 * @return {number} The id (not index!) of the currently highlighted row.
 */
goog.ui.ac.AutoComplete.prototype.getHighlightedId = function() {
  'use strict';
  return this.hiliteId_;
};


/**
 * Generic event handler that handles any events this object is listening to.
 * @param {goog.events.Event} e Event Object.
 * @suppress {missingProperties} e.row
 */
goog.ui.ac.AutoComplete.prototype.handleEvent = function(e) {
  'use strict';
  var matcher = /** @type {?goog.ui.ac.AutoComplete.Matcher} */ (this.matcher_);

  if (e.target == this.renderer_) {
    switch (e.type) {
      case goog.ui.ac.AutoComplete.EventType.HILITE:
        this.hiliteId(/** @type {number} */ (e.row));
        break;

      case goog.ui.ac.AutoComplete.EventType.SELECT:
        var rowDisabled = false;

        // e.row can be either a valid row id or empty.
        if (typeof e.row === 'number') {
          var rowId = e.row;
          var index = this.getIndexOfId(rowId);
          var row = this.rows_[index];

          // Make sure the row selected is not a disabled row.
          rowDisabled =
              !!row && matcher.isRowDisabled && matcher.isRowDisabled(row);
          if (row && !rowDisabled && this.hiliteId_ != rowId) {
            // Event target row not currently highlighted - fix the mismatch.
            this.hiliteId(rowId);
          }
        }
        if (!rowDisabled) {
          // Note that rowDisabled can be false even if e.row does not
          // contain a valid row ID; at least one client depends on us
          // proceeding anyway.
          this.selectHilited();
        }
        break;

      case goog.ui.ac.AutoComplete.EventType.CANCEL_DISMISS:
        this.cancelDelayedDismiss();
        break;

      case goog.ui.ac.AutoComplete.EventType.DISMISS:
        this.dismissOnDelay();
        break;
    }
  }
};


/**
 * Sets the max number of matches to fetch from the Matcher.
 *
 * @param {number} max Max number of matches.
 */
goog.ui.ac.AutoComplete.prototype.setMaxMatches = function(max) {
  'use strict';
  this.maxMatches_ = max;
};


/**
 * Sets whether or not the first row should be highlighted by default.
 *
 * @param {boolean} autoHilite true iff the first row should be
 *      highlighted by default.
 */
goog.ui.ac.AutoComplete.prototype.setAutoHilite = function(autoHilite) {
  'use strict';
  this.autoHilite_ = autoHilite;
};


/**
 * Sets whether or not the up/down arrow can unhilite all rows.
 *
 * @param {boolean} allowFreeSelect true iff the up arrow can unhilite all rows.
 */
goog.ui.ac.AutoComplete.prototype.setAllowFreeSelect = function(
    allowFreeSelect) {
  'use strict';
  this.allowFreeSelect_ = allowFreeSelect;
};


/**
 * Sets whether or not selections can wrap around the edges.
 *
 * @param {boolean} wrap true iff sections should wrap around the edges.
 */
goog.ui.ac.AutoComplete.prototype.setWrap = function(wrap) {
  'use strict';
  this.wrap_ = wrap;
};


/**
 * Sets whether or not to request new suggestions immediately after completion
 * of a suggestion.
 *
 * @param {boolean} triggerSuggestionsOnUpdate true iff completion should fetch
 *     new suggestions.
 */
goog.ui.ac.AutoComplete.prototype.setTriggerSuggestionsOnUpdate = function(
    triggerSuggestionsOnUpdate) {
  'use strict';
  this.triggerSuggestionsOnUpdate_ = triggerSuggestionsOnUpdate;
};


/**
 * Sets the token to match against.  This triggers calls to the Matcher to
 * fetch the matches (up to maxMatches), and then it triggers a call to
 * <code>renderer.renderRows()</code>.
 *
 * @param {string} token The string for which to search in the Matcher.
 * @param {string=} opt_fullString Optionally, the full string in the input
 *     field.
 */
goog.ui.ac.AutoComplete.prototype.setToken = function(token, opt_fullString) {
  'use strict';
  if (this.token_ == token) {
    return;
  }
  this.token_ = token;
  this.matcher_.requestMatchingRows(
      this.token_, this.maxMatches_, goog.bind(this.matchListener_, this),
      opt_fullString);
  this.cancelDelayedDismiss();
};


/**
 * Gets the current target HTML node for displaying autocomplete UI.
 * @return {Element} The current target HTML node for displaying autocomplete
 *     UI.
 */
goog.ui.ac.AutoComplete.prototype.getTarget = function() {
  'use strict';
  return this.target_;
};


/**
 * Sets the current target HTML node for displaying autocomplete UI.
 * Can be an implementation specific definition of how to display UI in relation
 * to the target node.
 * This target will be passed into  <code>renderer.renderRows()</code>
 *
 * @param {Element} target The current target HTML node for displaying
 *     autocomplete UI.
 */
goog.ui.ac.AutoComplete.prototype.setTarget = function(target) {
  'use strict';
  this.target_ = target;
};


/**
 * @return {boolean} Whether the autocomplete's renderer is open.
 * @suppress {missingProperties}
 */
goog.ui.ac.AutoComplete.prototype.isOpen = function() {
  'use strict';
  return this.renderer_.isVisible();
};


/**
 * @return {number} Number of rows in the autocomplete.
 * @deprecated Use this.getSuggestionCount().
 */
goog.ui.ac.AutoComplete.prototype.getRowCount = function() {
  'use strict';
  return this.getSuggestionCount();
};


/**
 * Moves the hilite to the next non-disabled row.
 * Calls renderer.hiliteId() when there's something to do.
 * @return {boolean} Returns true on a successful hilite.
 */
goog.ui.ac.AutoComplete.prototype.hiliteNext = function() {
  'use strict';
  var lastId = this.firstRowId_ + this.rows_.length - 1;
  var toHilite = this.hiliteId_;
  // Hilite the next row, skipping any disabled rows.
  for (var i = 0; i < this.rows_.length; i++) {
    // Increment to the next row.
    if (toHilite >= this.firstRowId_ && toHilite < lastId) {
      toHilite++;
    } else if (toHilite == -1) {
      toHilite = this.firstRowId_;
    } else if (this.allowFreeSelect_ && toHilite == lastId) {
      this.hiliteId(-1);
      return false;
    } else if (this.wrap_ && toHilite == lastId) {
      toHilite = this.firstRowId_;
    } else {
      return false;
    }

    if (this.hiliteId(toHilite)) {
      return true;
    }
  }
  return false;
};


/**
 * Moves the hilite to the previous non-disabled row.  Calls
 * renderer.hiliteId() when there's something to do.
 * @return {boolean} Returns true on a successful hilite.
 */
goog.ui.ac.AutoComplete.prototype.hilitePrev = function() {
  'use strict';
  var lastId = this.firstRowId_ + this.rows_.length - 1;
  var toHilite = this.hiliteId_;
  // Hilite the previous row, skipping any disabled rows.
  for (var i = 0; i < this.rows_.length; i++) {
    // Decrement to the previous row.
    if (toHilite > this.firstRowId_) {
      toHilite--;
    } else if (this.allowFreeSelect_ && toHilite == this.firstRowId_) {
      this.hiliteId(-1);
      return false;
    } else if (this.wrap_ && (toHilite == -1 || toHilite == this.firstRowId_)) {
      toHilite = lastId;
    } else {
      return false;
    }

    if (this.hiliteId(toHilite)) {
      return true;
    }
  }
  return false;
};


/**
 * Hilites the id if it's valid and the row is not disabled, otherwise does
 * nothing.
 * @param {number} id A row id (not index).
 * @return {boolean} Whether the id was hilited. Returns false if the row is
 *     disabled.
 */
goog.ui.ac.AutoComplete.prototype.hiliteId = function(id) {
  'use strict';
  var index = this.getIndexOfId(id);
  var row = this.rows_[index];
  var rowDisabled =
      !!row && this.matcher_.isRowDisabled && this.matcher_.isRowDisabled(row);
  if (!rowDisabled) {
    this.hiliteId_ = id;
    this.renderer_.hiliteId(id);
    return index != -1;
  }
  return false;
};


/**
 * Hilites the index, if it's valid and the row is not disabled, otherwise does
 * nothing.
 * @param {number} index The row's index.
 * @return {boolean} Whether the index was hilited.
 */
goog.ui.ac.AutoComplete.prototype.hiliteIndex = function(index) {
  'use strict';
  return this.hiliteId(this.getIdOfIndex_(index));
};


/**
 * If there are any current matches, this passes the hilited row data to
 * <code>selectionHandler.selectRow()</code>
 * @return {boolean} Whether there are any current matches.
 */
goog.ui.ac.AutoComplete.prototype.selectHilited = function() {
  'use strict';
  var index = this.getIndexOfId(this.hiliteId_);
  if (index != -1) {
    var selectedRow = this.rows_[index];
    var suppressUpdate =
        /** @type {!goog.ui.ac.InputHandler} */ (this.selectionHandler_)
            .selectRow(selectedRow);
    if (this.triggerSuggestionsOnUpdate_) {
      this.token_ = null;
      this.dismissOnDelay();
    } else {
      this.dismiss();
    }
    if (!suppressUpdate) {
      this.dispatchEvent({
        type: goog.ui.ac.AutoComplete.EventType.UPDATE,
        row: selectedRow,
        index: index
      });
      if (this.triggerSuggestionsOnUpdate_) {
        this.selectionHandler_.update(true);
      }
    }
    return true;
  } else {
    this.dismiss();
    this.dispatchEvent({
      type: goog.ui.ac.AutoComplete.EventType.UPDATE,
      row: null,
      index: null
    });
    return false;
  }
};


/**
 * Returns whether or not the autocomplete is open and has a highlighted row.
 * @return {boolean} Whether an autocomplete row is highlighted.
 */
goog.ui.ac.AutoComplete.prototype.hasHighlight = function() {
  'use strict';
  return this.isOpen() && this.getIndexOfId(this.hiliteId_) != -1;
};


/**
 * Clears out the token, rows, and hilite, and calls
 * <code>renderer.dismiss()</code>
 */
goog.ui.ac.AutoComplete.prototype.dismiss = function() {
  'use strict';
  this.hiliteId_ = -1;
  this.token_ = null;
  this.firstRowId_ += this.rows_.length;
  this.rows_ = [];
  window.clearTimeout(this.dismissTimer_);
  this.dismissTimer_ = null;
  this.renderer_.dismiss();
  this.dispatchEvent(goog.ui.ac.AutoComplete.EventType.SUGGESTIONS_UPDATE);
  this.dispatchEvent(goog.ui.ac.AutoComplete.EventType.DISMISS);
};


/**
 * Call a dismiss after a delay, if there's already a dismiss active, ignore.
 */
goog.ui.ac.AutoComplete.prototype.dismissOnDelay = function() {
  'use strict';
  if (!this.dismissTimer_) {
    this.dismissTimer_ = window.setTimeout(goog.bind(this.dismiss, this), 100);
  }
};


/**
 * Cancels any delayed dismiss events immediately.
 * @return {boolean} Whether a delayed dismiss was cancelled.
 * @private
 */
goog.ui.ac.AutoComplete.prototype.immediatelyCancelDelayedDismiss_ =
    function() {
  'use strict';
  if (this.dismissTimer_) {
    window.clearTimeout(this.dismissTimer_);
    this.dismissTimer_ = null;
    return true;
  }
  return false;
};


/**
 * Cancel the active delayed dismiss if there is one.
 */
goog.ui.ac.AutoComplete.prototype.cancelDelayedDismiss = function() {
  'use strict';
  // Under certain circumstances a cancel event occurs immediately prior to a
  // delayedDismiss event that it should be cancelling. To handle this situation
  // properly, a timer is used to stop that event.
  // Using only the timer creates undesirable behavior when the cancel occurs
  // less than 10ms before the delayed dismiss timout ends. If that happens the
  // clearTimeout() will occur too late and have no effect.
  if (!this.immediatelyCancelDelayedDismiss_()) {
    window.setTimeout(
        goog.bind(this.immediatelyCancelDelayedDismiss_, this), 10);
  }
};


/** @override */
goog.ui.ac.AutoComplete.prototype.disposeInternal = function() {
  'use strict';
  goog.ui.ac.AutoComplete.superClass_.disposeInternal.call(this);
  delete this.inputToAnchorMap_;
  this.renderer_.dispose();
  this.selectionHandler_.dispose();
  this.matcher_ = null;
};


/**
 * Callback passed to Matcher when requesting matches for a token.
 * This might be called synchronously, or asynchronously, or both, for
 * any implementation of a Matcher.
 * If the Matcher calls this back, with the same token this AutoComplete
 * has set currently, then this will package the matching rows in object
 * of the form
 * <pre>
 * {
 *   id: an integer ID unique to this result set and AutoComplete instance,
 *   data: the raw row data from Matcher
 * }
 * </pre>
 *
 * @param {string} matchedToken Token that corresponds with the rows.
 * @param {!Array<?>} rows Set of data that match the given token.
 * @param {(boolean|goog.ui.ac.RenderOptions)=} opt_options If true,
 *     keeps the currently hilited (by index) element hilited. If false not.
 *     Otherwise a RenderOptions object.
 * @private
 */
goog.ui.ac.AutoComplete.prototype.matchListener_ = function(
    matchedToken, rows, opt_options) {
  'use strict';
  if (this.token_ != matchedToken) {
    // Matcher's response token doesn't match current token.
    // This is probably an async response that came in after
    // the token was changed, so don't do anything.
    return;
  }

  this.renderRows(rows, opt_options);
};


/**
 * Renders the rows and adds highlighting.
 * @param {!Array<?>} rows Set of data that match the given token.
 * @param {(boolean|goog.ui.ac.RenderOptions)=} opt_options If true,
 *     keeps the currently hilited (by index) element hilited. If false not.
 *     Otherwise a RenderOptions object.
 * @suppress {missingProperties}
 */
goog.ui.ac.AutoComplete.prototype.renderRows = function(rows, opt_options) {
  'use strict';
  // The optional argument should be a RenderOptions object.  It can be a
  // boolean for backwards compatibility, defaulting to false.
  var optionsObj = goog.typeOf(opt_options) == 'object' && opt_options;

  var preserveHilited =
      optionsObj ? optionsObj.getPreserveHilited() : opt_options;
  var indexToHilite = preserveHilited ? this.getIndexOfId(this.hiliteId_) : -1;

  // Current token matches the matcher's response token.
  this.firstRowId_ += this.rows_.length;
  this.rows_ = rows;
  var rendRows = [];
  for (var i = 0; i < rows.length; ++i) {
    rendRows.push({id: this.getIdOfIndex_(i), data: rows[i]});
  }

  var anchor = null;
  if (this.target_) {
    anchor = this.inputToAnchorMap_[goog.getUid(this.target_)] || this.target_;
  }
  this.renderer_.setAnchorElement(anchor);
  this.renderer_.renderRows(rendRows, this.token_, this.target_);

  var autoHilite = this.autoHilite_;
  if (optionsObj && optionsObj.getAutoHilite() !== undefined) {
    autoHilite = optionsObj.getAutoHilite();
  }
  this.hiliteId_ = -1;
  if ((autoHilite || indexToHilite >= 0) && rendRows.length != 0 &&
      this.token_) {
    if (indexToHilite >= 0) {
      this.hiliteId(this.getIdOfIndex_(indexToHilite));
    } else {
      // Hilite the first non-disabled row.
      this.hiliteNext();
    }
  }
  this.dispatchEvent(goog.ui.ac.AutoComplete.EventType.SUGGESTIONS_UPDATE);
};


/**
 * Gets the index corresponding to a particular id.
 * @param {number} id A unique id for the row.
 * @return {number} A valid index into rows_, or -1 if the id is invalid.
 * @protected
 */
goog.ui.ac.AutoComplete.prototype.getIndexOfId = function(id) {
  'use strict';
  var index = id - this.firstRowId_;
  if (index < 0 || index >= this.rows_.length) {
    return -1;
  }
  return index;
};


/**
 * Gets the id corresponding to a particular index.  (Does no checking.)
 * @param {number} index The index of a row in the result set.
 * @return {number} The id that currently corresponds to that index.
 * @private
 */
goog.ui.ac.AutoComplete.prototype.getIdOfIndex_ = function(index) {
  'use strict';
  return this.firstRowId_ + index;
};


/**
 * Attach text areas or input boxes to the autocomplete by DOM reference.  After
 * elements are attached to the autocomplete, when a user types they will see
 * the autocomplete drop down.
 * @param {...Element} var_args Variable args: Input or text area elements to
 *     attach the autocomplete too.
 */
goog.ui.ac.AutoComplete.prototype.attachInputs = function(var_args) {
  'use strict';
  // Delegate to the input handler
  var inputHandler = /** @type {goog.ui.ac.InputHandler} */
      (this.selectionHandler_);
  inputHandler.attachInputs.apply(inputHandler, arguments);
};


/**
 * Detach text areas or input boxes to the autocomplete by DOM reference.
 * @param {...Element} var_args Variable args: Input or text area elements to
 *     detach from the autocomplete.
 */
goog.ui.ac.AutoComplete.prototype.detachInputs = function(var_args) {
  'use strict';
  // Delegate to the input handler
  var inputHandler = /** @type {goog.ui.ac.InputHandler} */
      (this.selectionHandler_);
  inputHandler.detachInputs.apply(inputHandler, arguments);

  // Remove mapping from input to anchor if one exists.
  Array.prototype.forEach.call(arguments, function(input) {
    'use strict';
    goog.object.remove(this.inputToAnchorMap_, goog.getUid(input));
  }, this);
};


/**
 * Attaches the autocompleter to a text area or text input element
 * with an anchor element. The anchor element is the element the
 * autocomplete box will be positioned against.
 * @param {Element} inputElement The input element. May be 'textarea',
 *     text 'input' element, or any other element that exposes similar
 *     interface.
 * @param {Element} anchorElement The anchor element.
 */
goog.ui.ac.AutoComplete.prototype.attachInputWithAnchor = function(
    inputElement, anchorElement) {
  'use strict';
  this.inputToAnchorMap_[goog.getUid(inputElement)] = anchorElement;
  this.attachInputs(inputElement);
};


/**
 * Forces an update of the display.
 * @param {boolean=} opt_force Whether to force an update.
 */
goog.ui.ac.AutoComplete.prototype.update = function(opt_force) {
  'use strict';
  var inputHandler = /** @type {goog.ui.ac.InputHandler} */
      (this.selectionHandler_);
  inputHandler.update(opt_force);
};