Source: typeaheadsearch.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.ts.TypeaheadSearch');

goog.require('goog.Uri');
goog.require('goog.array');
goog.require('goog.events.Event');
goog.require('goog.events.EventType');
goog.require('goog.net.XhrIo');
goog.require('goog.net.XmlHttpFactory');
goog.require('plana.ui.ac.AutoComplete');
goog.require('plana.ui.ac.RemoteObject');
goog.require('plana.ui.ac.RemoteObjectMatcher');
goog.require('plana.ui.ac.RemoteObjectMatcher.EventType');
goog.require('plana.ui.ts.TypeaheadSearchRenderer');

/**
 * This class extends {@link plana.ui.ac.AutoComplete}
 * to provide an additional search button that can be used
 * to trigger a fulltext search by adding the 'fullsearch'
 * parameter to requests
 *
 * @constructor
 * @extends {plana.ui.ac.AutoComplete}
 * @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 {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.ts.TypeaheadSearch = function(
  uri, opt_multi, opt_inputId,
  opt_xhrIo, opt_xmlHttpFactory, opt_useSimilar, opt_domHelper) {
  plana.ui.ac.AutoComplete.call(this,
    uri, opt_multi, new plana.ui.ts.TypeaheadSearchRenderer(), opt_inputId,
    opt_xhrIo, opt_xmlHttpFactory, opt_useSimilar, opt_domHelper);

  /**
   * The last token that was used for a full text
   * search
   * @type {?string}
   * @private
   */
  this.lastSearchToken_ = null;

  /**
   * Flag whether we only search if the token changed
   * from the previous token used to search
   * @type {boolean}
   * @private
   */
  this.forceUniqueTokenSearch_ = true;

  /**
   * Flag whether we're currently doing a fulltext search
   * @type {boolean}
   * @private
   */
  this.searching_ = false;
};
goog.inherits(plana.ui.ts.TypeaheadSearch, plana.ui.ac.AutoComplete);


/**
 * The name of the parameter that specifies the server should
 * perform a full search, instead of a search for suggestions. This
 * parameter will be set to 1 if active
 * @type {string}
 */
plana.ui.ts.TypeaheadSearch.FULLTEXT_SEARCH_PARA = 'fulltextsearch';

/**
 * @override
 * @suppress {checkTypes}
 */
plana.ui.ts.TypeaheadSearch.prototype.disposeInternal = function() {
  plana.ui.ts.TypeaheadSearch.superClass_.disposeInternal.call(this);
  this.lastSearchToken_ = null;
  this.searching_ = null;
  this.forceUniqueTokenSearch_ = null;
};

/**
 * @override
 */
plana.ui.ts.TypeaheadSearch.prototype.enterDocument = function() {
  plana.ui.ts.TypeaheadSearch.superClass_.enterDocument.call(this);
  /**
   * @type {plana.ui.ts.TypeaheadSearchRenderer}
   */
  var renderer =
  /**@type {plana.ui.ts.TypeaheadSearchRenderer}*/
  (this.componentRenderer);
  var handler = this.getHandler();
  handler.listen(renderer.getSearchButton(this, this.dom_),
    goog.events.EventType.CLICK, this.onSearch_, false);
};


/**
 * @override
 */
plana.ui.ts.TypeaheadSearch.prototype.exitDocument = function() {
  /**
   * @type {plana.ui.ts.TypeaheadSearchRenderer}
   */
  var renderer =
  /**@type {plana.ui.ts.TypeaheadSearchRenderer}*/
  (this.componentRenderer);
  var handler = this.getHandler();
  handler.unlisten(renderer.getSearchButton(this, this.dom_),
    goog.events.EventType.CLICK, this.onSearch_, false);
  if (this.searching_) {
    var remoteMatcher = this.cachingMatcher.getRemoteMatcher();
    handler.unlisten(remoteMatcher, [
      plana.ui.ac.RemoteObjectMatcher.EventType.FAILED_REQUEST,
      plana.ui.ac.RemoteObjectMatcher.EventType.MATCHES,
      plana.ui.ac.RemoteObjectMatcher.EventType.INVALID_RESPONSE
    ], this.onMatches_, false);
  }
  plana.ui.ts.TypeaheadSearch.superClass_.exitDocument.call(this);
};

/**
 * Callback for events dispatched by the remote matcher. This function simply
 * fires a new event for the listeners attached to this class
 * @param {plana.ui.ac.RemoteObjectMatcher.Event} e The event object dispatched
 *     by the remote matcher object
 * @private
 */
plana.ui.ts.TypeaheadSearch.prototype.onMatches_ = function(e) {
  var remoteMatcher = this.cachingMatcher.getRemoteMatcher();
  var queryData = remoteMatcher.getQueryData();
  queryData.remove(plana.ui.ts.TypeaheadSearch.FULLTEXT_SEARCH_PARA);

  var handler = this.getHandler();
  handler.unlisten(remoteMatcher, [
    plana.ui.ac.RemoteObjectMatcher.EventType.FAILED_REQUEST,
    plana.ui.ac.RemoteObjectMatcher.EventType.MATCHES,
    plana.ui.ac.RemoteObjectMatcher.EventType.INVALID_RESPONSE
  ], this.onMatches_, false);

  this.cachingMatcher.disableLocalCache(false);

  var input = this.inputHandler.getInput();
  var searchString = input.value;
  input.disabled = false;
  input.focus();
  this.searching_ = false;
  this.hidePlaceHolders();

  /** @type {Array.<plana.ui.ac.RemoteObject>} */
  var matches = [];
  switch (e.type) {
    case plana.ui.ac.RemoteObjectMatcher.EventType.FAILED_REQUEST:
    case plana.ui.ac.RemoteObjectMatcher.EventType.INVALID_RESPONSE:
      break;
    case plana.ui.ac.RemoteObjectMatcher.EventType.MATCHES:
      matches = e.matches;
      break;
  }
  if (matches.length == 0) {
    //no match
    this.dispatchEvent({
      type: plana.ui.ts.TypeaheadSearch.EventType.NO_MATCH,
      token: searchString,
      matches: [],
      total: 0
    });
  } else {
    //send matches to listeners
    this.dispatchEvent({
      type: plana.ui.ts.TypeaheadSearch.EventType.MATCHES,
      token: searchString,
      matches: goog.array.map(matches, function(m, indx, a) {
        return m.getData();
      }),
      total: e.total
    });
  }
};

/**
 * Callback for when the search button is explicitly pressed. Only search
 * if the input value is unknown
 * @param {goog.events.BrowserEvent} e
 * @private
 */
plana.ui.ts.TypeaheadSearch.prototype.onSearch_ = function(e) {
  if (this.searching_) return;

  var input = this.inputHandler.getInput();
  var token = input.value;

  if (this.lastSearchToken_ != token ||
    this.forceUniqueTokenSearch_ == false) {

    this.searching_ = true;
    this.lastSearchToken_ = token;
    /*
     * disable input so we don't cancel this search
     * by a user typing more text
     */
    input.disabled = true;
    this.cachingMatcher.disableLocalCache(true);
    var remoteMatcher = this.cachingMatcher.getRemoteMatcher();
    var queryData = remoteMatcher.getQueryData();
    queryData.set(plana.ui.ts.TypeaheadSearch.FULLTEXT_SEARCH_PARA, 1);
    var handler = this.getHandler();
    handler.listen(remoteMatcher, [
      plana.ui.ac.RemoteObjectMatcher.EventType.FAILED_REQUEST,
      plana.ui.ac.RemoteObjectMatcher.EventType.MATCHES,
      plana.ui.ac.RemoteObjectMatcher.EventType.INVALID_RESPONSE
    ], this.onMatches_, false);
    remoteMatcher.requestMatches('', -1, token);
  }
};

/**
 * Callback for when a user pressed a key. Here we are only interested
 * in the enter key, which will trigger a search on the server.
 * @param {goog.events.KeyEvent} e The key event dispatched by the
 *     key handler of the input handler control
 * @override
 */
plana.ui.ts.TypeaheadSearch.prototype.onKey = function(e) {
  switch (e.keyCode) {
    case goog.events.KeyCodes.ENTER:
    case goog.events.KeyCodes.MAC_ENTER:
      if (!this.autoComplete.selectHilited()) {
        this.onSearch_(null);
        return;
      }
  }
  plana.ui.ts.TypeaheadSearch.superClass_.onKey.call(this, e);
};

/**
 * Setter whether we only do a fulltext search if the search string (i.e. input
 * value) changed from the previous search
 * @param {boolean} unique
 */
plana.ui.ts.TypeaheadSearch.prototype.setForceUniqueSearch = function(unique) {
  this.forceUniqueTokenSearch_ = unique;
};

/**
 * List of event types dispatched by this UI
 * component.
 * @enum {!string}
 */
plana.ui.ts.TypeaheadSearch.EventType = {
  /**
   * @desc The event dispatched if the filter text does
   *    not match any menu items.
   */
  NO_MATCH: goog.events.getUniqueId('nomatch'),
  /**
   * @desc The event dispatched if a search resulted in
   *    matches.
   */
  MATCHES: goog.events.getUniqueId('match')
};