Source: autocomplete.js

// Copyright (C) Plan-A Software Ltd. All Rights Reserved.
//
// Written by Kiran Lakhotia <kiran@plan-a-software.co.uk>, 2014
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

'use strict';

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

goog.require('goog.array');
goog.require('goog.events.Event');
goog.require('goog.events.EventType');
goog.require('goog.math.Size');
goog.require('goog.net.XhrIo');
goog.require('goog.net.XmlHttpFactory');
goog.require('goog.string');
goog.require('goog.ui.Component');
goog.require('goog.ui.ac.AutoComplete');
goog.require('goog.ui.ac.AutoComplete.EventType');
goog.require('goog.ui.ac.Renderer');
goog.require('plana.ui.ac.AutoCompleteRenderer');
goog.require('plana.ui.ac.CachingObjectMatcher');
goog.require('plana.ui.ac.InputHandler');
goog.require('plana.ui.ac.RemoteObject');

/**
 * This class is a wrapper around {@link goog.ui.ac.AutoComplete} that uses
 * a remote object matcher. The remote object matcher can retrieve autocomplete
 * suggestions as plain strings, or, custom objects. It is best if the objects
 * have a 'caption' property. This property is used to display the suggestions.
 * If an object does not have a 'caption' property, 'toString' is used instead.
 *
 * @constructor
 * @extends {goog.ui.Component}
 * @param {goog.Uri} uri The server resources to use for fetching a list of
 *     suggestions. You can add custom parameters to uri to pass to the server
 *     with every request
 * @param {boolean=} opt_multi Whether to allow multiple entries separated with
 *     semi-colons or commas
 * @param {plana.ui.ac.AutoCompleteRenderer=} opt_renderer An optional renderer
 *     that extends the default renderer, i.e. implements a 'createDom' and
 *     'getInput' method
 * @param {string=} opt_inputId Optional id to use for the autocomplete input
 *     element
 * @param {goog.net.XhrIo=} opt_xhrIo Optional XhrIo object to use. By default
 *     we create a new instance
 * @param {goog.net.XmlHttpFactory=} opt_xmlHttpFactory Optional factory to use
 *     when creating XMLHttpRequest objects
 * @param {boolean=} opt_useSimilar Use similar matches. e.g. "gost" => "ghost".
 *     This option is passed along to the server
 * @param {goog.dom.DomHelper=} opt_domHelper The dom helper
 */
plana.ui.ac.AutoComplete = function(
  uri, opt_multi, opt_renderer, opt_inputId,
  opt_xhrIo, opt_xmlHttpFactory, opt_useSimilar, opt_domHelper) {
  goog.ui.Component.call(this, opt_domHelper);

  /**
   * Flag whether the input supports separators and thus
   * we need to store an array of selected matches
   * @type {boolean}
   * @private
   */
  this.isArrayModel_ = opt_multi || false;

  /**
   * The id to use for the autocomplete input element
   * @type {string}
   * @private
   */
  this.inputId_ = opt_inputId || '';

  /**
   * Optional placeholder to show in the input textbox
   * @type {string}
   * @private
   */
  this.placeholder_ = '';

  /**
   * Custom renderer for this class. Its main job is to
   * attach custom classes to the container and its input
   * element
   * @type {plana.ui.ac.AutoCompleteRenderer}
   * @protected
   */
  this.componentRenderer = opt_renderer ||
    new plana.ui.ac.AutoCompleteRenderer();

  /**
   * The matcher that combines a local cache of matches with
   * the remote matcher
   * @type {plana.ui.ac.CachingObjectMatcher}
   * @protected
   */
  this.cachingMatcher =
    new plana.ui.ac.CachingObjectMatcher(uri, opt_xhrIo,
      opt_xmlHttpFactory, !! opt_multi, !opt_useSimilar);

  /**
   * The renderer to render the list of suggestions for the
   * autocomplete component
   * @type {goog.ui.ac.Renderer}
   * @private
   */
  this.autoCompleteRenderer_ = null;

  /**
   * The input handler that updates the text input when
   * a match is selected
   * @type {?plana.ui.ac.InputHandler}
   * @protected
   */
  this.inputHandler = null;

  /**
   * The actual autocomplete component
   * @type {?goog.ui.ac.AutoComplete}
   * @protected
   */
  this.autoComplete = null;

  /**
   * @see http://docs.closure-library.googlecode.com/git-history/7bb23f83ca959ae16e10ebc6734b0ba882629904/class_goog_ui_ac_InputHandler.html
   * Whether to prevent the default behavior (moving focus to another element)
   * when tab is pressed. This occurs by default only for multi-value mode.
   * @type {boolean}
   * @private
   */
  this.preventDefaultOnTab_ = !! opt_multi;

  /**
   * The DOM to display while results are fetched from the server
   * @type {?Element}
   * @private
   */
  this.fetchingMatchesDom_ = null;

  /**
   * The image or text to display while we're fetching
   * matches from the server, e.g. a waiting animation
   * @type {?(Element|string)}
   * @private
   */
  this.loadingContent_ = plana.ui.ac.AutoComplete.MSG_LOADING_DEFAULT;

  /**
   * The DOM to display if the server returned no matches
   * @type {?Element}
   * @private
   */
  this.noMatchesDom_ = null;

  /**
   * An optional message to display if the server did not find
   * any matches
   * @type {?(Element|string)}
   * @private
   */
  this.noMatchMsg_ = plana.ui.ac.AutoComplete.MSG_NO_MATCHES_FOUND;
};
goog.inherits(plana.ui.ac.AutoComplete, goog.ui.Component);

/**
 * @desc Default loading message while fetching results
 */
plana.ui.ac.AutoComplete.MSG_LOADING_DEFAULT =
  goog.getMsg('<i>Loading...</i>');

/**
 * @desc Default message if no matches were found on
 * the server
 */
plana.ui.ac.AutoComplete.MSG_NO_MATCHES_FOUND =
  goog.getMsg('<i>Could not find a match</i>');

/**
 * The class name of the row showing loading and no match found
 * messages
 * @type {!string}
 */
plana.ui.ac.AutoComplete.PLACEHOLDER_ROW_CSS = 'ac-fetching-row';

/**
 * @override
 * @suppress {checkTypes}
 */
plana.ui.ac.AutoComplete.prototype.disposeInternal = function() {
  plana.ui.ac.AutoComplete.superClass_.disposeInternal.call(this);
  this.isArrayModel_ = null;
  this.inputId_ = null;
  this.componentRenderer = null;
  this.cachingMatcher.dispose();
  this.cachingMatcher = null;
  if (this.autoCompleteRenderer_ != null) {
    this.autoCompleteRenderer_.dispose();
    this.autoCompleteRenderer_ = null;
  }
  if (this.inputHandler != null) {
    this.inputHandler.dispose();
    this.inputHandler = null;
  }
  if (this.autoComplete != null) {
    this.autoComplete.dispose();
    this.autoComplete = null;
  }
  this.preventDefaultOnTab_ = null;
  this.fetchingMatchesDom_ = null;
  this.loadingContent_ = null;
  this.noMatchesDom_ = null;
  this.noMatchMsg_ = null;
  this.placeholder_ = null;
};

/**
 * This component does not support decoration for now
 * @param {Element} element Element to decorate
 * @return {boolean} Return false
 * @override
 */
plana.ui.ac.AutoComplete.prototype.canDecorate = function(element) {
  return false;
};

/**
 * @override
 */
plana.ui.ac.AutoComplete.prototype.createDom = function() {
  var dom = this.dom_;

  var renderer = this.componentRenderer;
  var container = renderer.createDom(dom);
  this.setElementInternal(container);

  var model = /** @type {?Array.<Object|string>} */
    (plana.ui.ac.AutoComplete.superClass_.getModel.call(this));

  var input = renderer.getInput(this, dom);
  input['placeholder'] = this.placeholder_;
  input['id'] = this.inputId_;

  this.inputHandler = new plana.ui.ac.InputHandler(input, this.isArrayModel_);
  this.inputHandler.setParentEventTarget(this);

  //make sure we initialize the inputHandler if necessary
  this.setModel(model);

  this.autoCompleteRenderer_ = new goog.ui.ac.Renderer(container);
  this.autoCompleteRenderer_.setAutoPosition(true);

  this.autoComplete = new goog.ui.ac.AutoComplete(
    this.cachingMatcher, this.autoCompleteRenderer_, this.inputHandler);

  this.autoComplete.setParentEventTarget(this);

  this.createLoadingDom_();

  this.createNoMatchDom_();
};

/**
 * This function creates the DOM to show a message if there are no
 * matches
 * @private
 */
plana.ui.ac.AutoComplete.prototype.createNoMatchDom_ = function() {
  if (this.noMatchMsg_ == null) {
    if (this.noMatchesDom_ != null) {
      var parent = this.dom_.getParentElement(this.noMatchesDom_);
      if (parent != null)
        this.dom_.removeNode(this.noMatchesDom_);
    }
    this.noMatchesDom_ = null;
  } else {
    var dom = this.dom_;
    if (this.noMatchesDom_ == null) {
      this.noMatchesDom_ = dom.createDom('div', {
        'class': plana.ui.ac.AutoComplete.PLACEHOLDER_ROW_CSS
      });
    }
    if (goog.isString(this.noMatchMsg_))
      this.noMatchesDom_.innerHTML = this.noMatchMsg_;
    else {
      dom.removeChildren(this.noMatchesDom_);
      dom.appendChild(this.noMatchesDom_,
        /** @type {!Node}*/
        (this.noMatchMsg_));
    }
  }
};

/**
 * This function creates the DOM to show the loading image or text
 * @private
 */
plana.ui.ac.AutoComplete.prototype.createLoadingDom_ = function() {
  if (this.loadingContent_ == null) {
    if (this.fetchingMatchesDom_ != null) {
      var parent = this.dom_.getParentElement(this.fetchingMatchesDom_);
      if (parent != null)
        this.dom_.removeNode(this.fetchingMatchesDom_);
    }
    this.fetchingMatchesDom_ = null;
  } else {
    var dom = this.dom_;
    if (this.fetchingMatchesDom_ == null) {
      this.fetchingMatchesDom_ = dom.createDom('div', {
        'class': plana.ui.ac.AutoComplete.PLACEHOLDER_ROW_CSS
      });
    }
    if (goog.isString(this.loadingContent_))
      this.fetchingMatchesDom_.innerHTML = this.loadingContent_;
    else {
      dom.removeChildren(this.fetchingMatchesDom_);
      dom.appendChild(this.fetchingMatchesDom_, this.loadingContent_);
    }
  }
};

/**
 * @override
 */
plana.ui.ac.AutoComplete.prototype.enterDocument = function() {
  plana.ui.ac.AutoComplete.superClass_.enterDocument.call(this);
  var handler = this.getHandler();
  handler.listen(this.autoComplete, [
    goog.ui.ac.AutoComplete.EventType.UPDATE,
    goog.ui.ac.AutoComplete.EventType.SUGGESTIONS_UPDATE,
    goog.ui.ac.AutoComplete.EventType.DISMISS
  ], this.onUpdate_, false);
  handler.listen(this.inputHandler,
    goog.object.getValues(plana.ui.ac.InputHandler.EventType),
    this.onInputEvent_, false);
  handler.listen(this.inputHandler,
    goog.events.KeyHandler.EventType.KEY,
    this.onKey, false);
};

/**
 * @override
 */
plana.ui.ac.AutoComplete.prototype.exitDocument = function() {
  var handler = this.getHandler();
  handler.unlisten(this.autoComplete, [
    goog.ui.ac.AutoComplete.EventType.UPDATE,
    goog.ui.ac.AutoComplete.EventType.SUGGESTIONS_UPDATE,
    goog.ui.ac.AutoComplete.EventType.DISMISS
  ], this.onUpdate_, false);
  handler.unlisten(this.inputHandler,
    goog.object.getValues(plana.ui.ac.InputHandler.EventType),
    this.onInputEvent_, false);
  handler.unlisten(this.inputHandler,
    goog.events.KeyHandler.EventType.KEY,
    this.onKey, false);
  plana.ui.ac.AutoComplete.superClass_.exitDocument.call(this);
};

/**
 * This function adjust the width of the autocomplete container
 * to be the same size as the input
 * @private
 */
plana.ui.ac.AutoComplete.prototype.setSuggestionListWidth_ = function() {
  var autoCompleteRenderer = this.autoCompleteRenderer_;
  if (!autoCompleteRenderer.isVisible()) {
    var renderer = this.componentRenderer;
    var input = renderer.getInput(this, this.dom_);
    var suggestionContainer = autoCompleteRenderer.getElement();
    if (input && suggestionContainer) {
      var inputSize = goog.style.getSize(input);
      goog.style.setWidth(suggestionContainer, inputSize.width);
    }
  }
};

/**
 * This function sets the focus to the text input
 */
plana.ui.ac.AutoComplete.prototype.focus = function() {
  if (this.inputHandler)
    this.inputHandler.getInput().focus();
};

/**
 * Setter for the placeholder text of the input
 * @param {!string} label
 */
plana.ui.ac.AutoComplete.prototype.setPlaceholder = function(label) {
  this.placeholder_ = label;
  if (this.inputHandler)
    this.inputHandler.getInput()['placeholder'] = label;
};

/**
 * Set the HTTP headers. Wrapper around
 * {@link plana.ui.ac.RemoteObjectMatcher#setHeaders}
 * @param {?Object} headers
 */
plana.ui.ac.AutoComplete.prototype.setHeaders = function(headers) {
  this.cachingMatcher.getRemoteMatcher().setRequestHeaders(headers);
};

/**
 * This function sets the content to show while fetching matches
 * from the server
 * @param {Element|string|null} content
 */
plana.ui.ac.AutoComplete.prototype.setLoadingContent = function(content) {
  this.loadingContent_ = content;
  this.createLoadingDom_();
};

/**
 * This function sets the content to show if the token does not
 * match anything
 * @param {Element|string|null} content
 */
plana.ui.ac.AutoComplete.prototype.setNoMatchContent = function(content) {
  this.noMatchMsg_ = content;
  this.createNoMatchDom_();
};

/**
 * This function returns the renderer used to render this component
 * (i.e. wrap the input element inside a div)
 * @return {plana.ui.ac.AutoCompleteRenderer}
 */
plana.ui.ac.AutoComplete.prototype.getRenderer = function() {
  return this.componentRenderer;
};

/**
 * This function returns the actual autocomplete UI
 * @return {goog.ui.ac.AutoComplete}
 */
plana.ui.ac.AutoComplete.prototype.getAutoComplete = function() {
  return this.autoComplete;
};

/**
 * This function returns the input handler used by the
 * autocomplete UI
 * @return {plana.ui.ac.InputHandler}
 */
plana.ui.ac.AutoComplete.prototype.getInputHandler = function() {
  return this.inputHandler;
};

/**
 * This function returns the cached-based remote matcher used by this
 * component
 * @return {plana.ui.ac.CachingObjectMatcher}
 */
plana.ui.ac.AutoComplete.prototype.getCachingMatcher = function() {
  return this.cachingMatcher;
};

/**
 * Callback for events dispatched by the inputhandler
 * @param {goog.events.Event|
 * {
 *  type: string,
 *  target: plana.ui.ac.InputHandler,
 *  token: string,
 *  fullstring: string
 * }} e The event dispatched by the
 *     input handler
 * @private
 */
plana.ui.ac.AutoComplete.prototype.onInputEvent_ = function(e) {
  switch (e.type) {
    case plana.ui.ac.InputHandler.EventType.TEXT_CHANGED:
      this.autoComplete.setToken(e.token, e.fullstring);
      break;
    case plana.ui.ac.InputHandler.EventType.DISMISS:
      this.autoComplete.dismiss();
      break;
    case plana.ui.ac.InputHandler.EventType.SELECT_HIGHLIGHTED:
      if (this.autoComplete.selectHilited())
        e.preventDefault();
      break;
  }
};

/**
 * Callback for KEY events dispatched by the input element associated
 * with this autocomplete. Here we listen for up/down and tab keys
 * to navigate the suggestion list, and for the enter key to select
 * a highlighted item
 * @param {goog.events.KeyEvent} e The key event dispatched by the
 *     key handler of the input handler control. The key event is
 *     forwarded to here because we set the parent event target of
 *     this.inputHandler
 * @protected
 */
plana.ui.ac.AutoComplete.prototype.onKey = function(e) {
  switch (e.keyCode) {
    // If the menu is open and 'down' caused a change then prevent the default
    // action and prevent scrolling.  If the box isn't a multi autocomplete
    // and the menu isn't open, we force it open now.
    case goog.events.KeyCodes.DOWN:
      if (this.autoComplete.isOpen()) {
        this.autoComplete.hiliteNext();
        e.preventDefault();
      } else if (!this.isArrayModel_) {
        this.inputHandler.update(true);
        e.preventDefault();
      }
      break;
      // If the menu is open and 'up' caused a change then prevent the default
      // action and prevent scrolling.
    case goog.events.KeyCodes.UP:
      if (this.autoComplete.isOpen()) {
        this.autoComplete.hilitePrev();
        e.preventDefault();
      }
      break;

      // If tab key is pressed, select the current highlighted item. The default
      // action is also prevented if the input is a multi input, to prevent the
      // user tabbing out of the field.
    case goog.events.KeyCodes.TAB:
      if (this.autoComplete.isOpen() && !e.shiftKey) {
        // Ensure the menu is up to date before completing.
        this.inputHandler.update(true);
        if (this.autoComplete.selectHilited() && this.preventDefaultOnTab_) {
          e.preventDefault();
        }
      } else {
        this.autoComplete.dismiss();
      }
      break;
    case goog.events.KeyCodes.ESC:
      this.autoComplete.dismiss();
      break;
    case goog.events.KeyCodes.ENTER:
    case goog.events.KeyCodes.MAC_ENTER:
      this.autoComplete.selectHilited();
      break;
  }
};

/**
 * Callback for when a user selected an item from the list of
 * suggestions or when a user modified the text input.
 * This function saves the selected item (or null) as the model of
 * this component. It also shows/hides the fetching and no results
 * found messages
 * @param {goog.events.Event} e The event object with additional
 *     row and index properties
 * @private
 */
plana.ui.ac.AutoComplete.prototype.onUpdate_ = function(e) {
  switch (e.type) {
    case goog.ui.ac.AutoComplete.EventType.UPDATE:
      var eventData =
      /**@type {{
            type: string,
            row: ?plana.ui.ac.RemoteObject,
            index: number
          }}*/
      (e);

      /**
       * @type {Object|string|null}
       */
      var rowData = null;
      if (eventData.row != null)
        rowData =
          ( /**@type {!plana.ui.ac.RemoteObject}*/ (eventData.row)).getData();
      /**
       * Forward the event to any listeners that might be
       * interested when an update occurred
       */
      this.dispatchEvent({
        type: e.type,
        data: rowData
      });
      e.stopPropagation();
      break;
    case goog.ui.ac.AutoComplete.EventType.SUGGESTIONS_UPDATE:
      this.setSuggestionListWidth_();
      var state = this.cachingMatcher.getState();
      var renderer = this.autoCompleteRenderer_;
      var dom = this.dom_;
      switch (state) {
        case plana.ui.ac.CachingObjectMatcher.State.FETCHING:
          if (this.fetchingMatchesDom_ != null) {
            var notShowing =
              this.dom_.getParentElement(this.fetchingMatchesDom_) == null;
            if (notShowing) {
              dom.appendChild(renderer.getElement(), this.fetchingMatchesDom_);
              renderer.show();
              renderer.reposition();
            }
          }
          if (this.noMatchesDom_ != null &&
            this.dom_.getParentElement(this.noMatchesDom_) != null) {
            dom.removeNode(this.noMatchesDom_);
          }
          break;
        case plana.ui.ac.CachingObjectMatcher.State.NO_MATCH:
          if (this.fetchingMatchesDom_ != null &&
            this.dom_.getParentElement(this.fetchingMatchesDom_) != null) {
            dom.removeNode(this.fetchingMatchesDom_);
          }
          if (this.noMatchesDom_ != null) {
            var notShowing =
              this.dom_.getParentElement(this.noMatchesDom_) == null;
            if (notShowing) {
              dom.appendChild(renderer.getElement(), this.noMatchesDom_);
              renderer.show();
              renderer.reposition();
            }
          }
          break;
        case plana.ui.ac.CachingObjectMatcher.State.READY:
        case plana.ui.ac.CachingObjectMatcher.State.ERROR:
          this.hidePlaceHolders();
          break;
      }
      break;
    case goog.ui.ac.AutoComplete.EventType.DISMISS:
      this.hidePlaceHolders();
      this.autoCompleteRenderer_.dismiss();
      break;
    default:
      throw 'invalid caching matcher state';
  }
};

/**
 * This function removes any loading or no-match messages
 * @protected
 */
plana.ui.ac.AutoComplete.prototype.hidePlaceHolders = function() {
  var dom = this.dom_;
  if (this.fetchingMatchesDom_ != null &&
    dom.getParentElement(this.fetchingMatchesDom_) != null) {
    dom.removeNode(this.fetchingMatchesDom_);
  }
  if (this.noMatchesDom_ != null &&
    dom.getParentElement(this.noMatchesDom_) != null) {
    dom.removeNode(this.noMatchesDom_);
  }
};

/**
 * @see http://docs.closure-library.googlecode.com/git-history/7bb23f83ca959ae16e10ebc6734b0ba882629904/class_goog_ui_ac_InputHandler.html
 * Sets whether we will prevent the default input behavior (moving focus to the
 * next focusable  element) on TAB.
 * @param {boolean} newValue Whether to preventDefault on TAB.
 */
plana.ui.ac.AutoComplete.prototype.setPreventDefaultOnTab = function(newValue) {
  this.preventDefaultOnTab_ = newValue;
};

/**
 * @param {*} model The list of objects or string
 *     with which to initialize the text input of the autocomplete
 *     input
 * @override
 */
plana.ui.ac.AutoComplete.prototype.setModel = function(model) {
  plana.ui.ac.AutoComplete.superClass_.setModel.call(this,
    /** @type {?Array.<Object|string>} */
    (model));
  var matches;
  if (model == null)
    matches = [];
  else if (goog.isArray(model))
    matches = model;
  else
    matches = [model];
  if (this.inputHandler)
    this.inputHandler.setMatchedObjects(matches);
};

/**
 * @override
 * @return {Object|string|null|Array.<Object|string>}
 */
plana.ui.ac.AutoComplete.prototype.getModel = function() {
  if (this.inputHandler) {
    var matches = this.inputHandler.getMatchedObjects();
    if (this.isArrayModel_)
      return matches;
    else {
      if (matches.length == 0)
        return null;
      return matches[0];
    }
  }
  var model = /** @type {Object|string|null|Array.<Object|string>} */
    (plana.ui.ac.AutoComplete.superClass_.getModel.call(this));
  return model;
};

/**
 * It is possible when a user pastes text in the textbox, and the
 * autocomplete supports separators, that {@link #getModel} does not
 * return the list of items that are displayed in the textbox. For
 * example, if a user pastes
 *     complete,non-matching nonsense
 * the input handler will not return any matches.
 * Use this function to get the list of entries displayed in the
 * textbox that have not been matched with a server result.
 * @return {Array.<string>}
 */
plana.ui.ac.AutoComplete.prototype.getNonMatches = function() {
  if (this.inputHandler == null) return [];
  /**
   * @type {?plana.ui.ac.InputHandler}
   */
  var inputHandler = this.inputHandler;
  var entries = inputHandler.getEntries();
  var matches = inputHandler.getMatchedObjects();
  var filtered = goog.array.filter(entries,
    /**
     * @param  {!string} text
     * @param  {number} indx
     * @return {boolean}
     */

    function(text, indx) {
      if (goog.string.isEmptySafe(text)) return false;
      text = goog.string.trim(text);
      for (var i = 0, match; match = matches[i]; ++i) {
        var remove = false;
        if (goog.isString(match)) {
          if (inputHandler.areStringsEqual(goog.string.trim(match), text))
            remove = true;
        } else {
          /**
           * @type {string|undefined}
           */
          var caption =
            match[plana.ui.ac.RemoteObjectMatcher.CAPTION_PROPERTY];
          if (goog.isDefAndNotNull(caption)) {
            caption = goog.string.trim(caption);
            if (inputHandler.areStringsEqual(caption, text)) {
              remove = true;
            }
          } else {
            remove = inputHandler.areStringsEqual(
              goog.string.trim(match.toString()),
              text);
          }
        }
        if (remove == true) {
          return false;
        }
      }
      return true;
    });
  inputHandler = null;
  return goog.array.map(filtered, function(text, indx) {
    return goog.string.trim(text);
  });
};