// 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.RemoteObject');
goog.provide('plana.ui.ac.RemoteObjectMatcher');
goog.provide('plana.ui.ac.RemoteObjectMatcher.Event');
goog.provide('plana.ui.ac.RemoteObjectMatcher.EventType');
goog.require('goog.Disposable');
goog.require('goog.Uri');
goog.require('goog.Uri.QueryData');
goog.require('goog.events.Event');
goog.require('goog.events.EventHandler');
goog.require('goog.events.EventTarget');
goog.require('goog.net.EventType');
goog.require('goog.net.XhrIo');
goog.require('goog.net.XmlHttpFactory');
/**
* This class is an alternative to {@link goog.ui.ac.RemoteArrayMatcher}. It
* submits an ajax request to fetch an array of matches. The array may contain
* plain strings or objects. If the matches are objects, then the objects
* 'should' contain a 'caption' property. If they don't, a user will see the
* result of 'toString' in the suggestion list. This class fires events if the
* server returned matches, the ajax request failed (i.e. server error), or the
* server returned an invalid response for some reason.
*
* @constructor
* @extends {goog.events.EventTarget}
*
* @param {goog.Uri} uri The uri which generates the auto complete matches. The
* search term is passed to the server as the 'token' query param
* @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_multi Whether to allow multiple entries
* @param {boolean=} opt_noSimilar If true, request that the server does not do
* similarity matches for the input token against the dictionary
* The value is sent to the server as the 'use_similar' query param which is
* either "1" (opt_noSimilar==false) or "0" (opt_noSimilar==true)
*/
plana.ui.ac.RemoteObjectMatcher = function(
uri, opt_xhrIo, opt_xmlHttpFactory, opt_multi, opt_noSimilar) {
goog.events.EventTarget.call(this);
/**
* Reference to the URI used to request matches
* @type {goog.Uri}
* @protected
*/
this.uri = uri;
//set constant query parameters
var queryData = this.uri.getQueryData();
queryData.set(plana.ui.ac.RemoteObjectMatcher.USE_SIMILAR_PARA,
String(Number(opt_noSimilar || false)));
queryData.set(plana.ui.ac.RemoteObjectMatcher.MULTI_TOKEN_PARA,
String(Number(opt_multi || false)));
queryData = null;
/**
* The class for performing ajax requests
* @type {goog.net.XhrIo}
* @protected
*/
this.xhrIo = opt_xhrIo || new goog.net.XhrIo(opt_xmlHttpFactory);
/**
* Boolean flag whether the request object
* is ready to handle another request
* @type {boolean}
* @protected
*/
this.xhrIoReady = true;
/**
* A map of request headers to include in
* send requests
* @type {?Object}
* @private
*/
this.xhrIoRequestHeaders_ = null;
/**
* The type of request, i.e. 'GET' or 'POST'
* @type {string}
* @private
*/
this.xhrIoRequestType_ = 'POST';
/**
* The event handler used by this class to listen to
* ajax events
* @type {goog.events.EventHandler}
* @private
*/
this.eventHandler_ = new goog.events.EventHandler(this);
//start listening to ajax events
this.eventHandler_.listen(this.xhrIo,
goog.object.getValues(goog.net.EventType),
this.onRequestCompleted, false);
};
goog.inherits(plana.ui.ac.RemoteObjectMatcher, goog.events.EventTarget);
/**
* The name of the token parameter passed to the server
* @type {string}
*/
plana.ui.ac.RemoteObjectMatcher.TOKEN_PARA = 'token';
/**
* The parameter name of the flag that specifies whether the server
* should return matches that are similar to the search token
* @see goog.ui.ac.AutoComplete for an explanation of this flag
* @type {string}
*/
plana.ui.ac.RemoteObjectMatcher.USE_SIMILAR_PARA = 'user_similar';
/**
* The parameter name of the flag that specifies whether the autocomplete
* supports multiple tokens
* @type {string}
*/
plana.ui.ac.RemoteObjectMatcher.MULTI_TOKEN_PARA = 'multi';
/**
* The name of the parameter that specifies how many matches the
* server should return
* @type {string}
*/
plana.ui.ac.RemoteObjectMatcher.MAX_MATCHES_PARA = 'max_matches';
/**
* The name of the parameter that contains the entire input value,
* including previous tokens
* @type {string}
*/
plana.ui.ac.RemoteObjectMatcher.FULL_STRING_PARA = 'fullstring';
/**
* The property name that will contain the server matches, if
* the server is returning additional information along the matches,
* for example, total available matches on the server
* @type {string}
*/
plana.ui.ac.RemoteObjectMatcher.MATCHES_PROPERTY = 'matches';
/**
* The property name that will contain the total count of matches
* available on the server
* @type {string}
*/
plana.ui.ac.RemoteObjectMatcher.TOTAL_PROPERTY = 'total';
/**
* The property name of objects that contain the caption to display
* in the list of suggestions
* @type {string}
*/
plana.ui.ac.RemoteObjectMatcher.CAPTION_PROPERTY = 'caption';
/**
* @override
* @suppress {checkTypes}
*/
plana.ui.ac.RemoteObjectMatcher.prototype.disposeInternal = function() {
plana.ui.ac.RemoteObjectMatcher.superClass_.disposeInternal.call(this);
/** not strictly nec., because we're disposing of eventHandler_, but
* better to be explicit :) */
this.eventHandler_.unlisten(this.xhrIo,
goog.object.getValues(goog.net.EventType),
this.onRequestCompleted, false, this);
this.uri = null;
if (!this.xhrIoReady)
this.xhrIo.abort();
this.xhrIo.dispose();
this.xhrIo = null;
this.xhrIoReady = null;
this.xhrIoRequestHeaders_ = null;
this.xhrIoRequestType_ = null;
this.eventHandler_.dispose();
this.eventHandler_ = null;
};
/**
* Callback for when the server request completed.
* @param {goog.events.Event} e
* @protected
*/
plana.ui.ac.RemoteObjectMatcher.prototype.onRequestCompleted = function(e) {
/** make sure we're not disposed while the server request was pending */
if (this.isDisposed()) {
e.preventDefault();
e.stopPropagation();
return;
}
switch (e.type) {
case goog.net.EventType.SUCCESS:
var response = null;
try {
response = this.xhrIo.getResponseJson();
} catch (ex) {
response = null;
}
if (response != null) {
/**
* @type {Array.<String|Object>}
*/
var serverMatches;
/**
* @type {number}
*/
var totalMatches;
if (goog.isArray(response)) {
serverMatches = /**@type {Array.<String|Object>}*/ (response);
totalMatches = serverMatches.length;
} else {
serverMatches =
/**@type {Array.<String|Object>}*/
(response[plana.ui.ac.RemoteObjectMatcher.MATCHES_PROPERTY]);
totalMatches =
/**@type {number}*/
(response[plana.ui.ac.RemoteObjectMatcher.TOTAL_PROPERTY]);
}
/** @type {Array.<plana.ui.ac.RemoteObject>} */
var matches = [];
for (var i = 0, match; match = serverMatches[i]; ++i)
matches.push(new plana.ui.ac.RemoteObject(match));
this.dispatchEvent(
new plana.ui.ac.RemoteObjectMatcher.Event(
plana.ui.ac.RemoteObjectMatcher.EventType.MATCHES, this,
matches, totalMatches));
matches = null;
} else {
this.dispatchEvent(
new goog.events.Event(
plana.ui.ac.RemoteObjectMatcher.EventType.INVALID_RESPONSE, this));
}
break;
case goog.net.EventType.ERROR:
case goog.net.EventType.TIMEOUT:
this.dispatchEvent(
new goog.events.Event(
plana.ui.ac.RemoteObjectMatcher.EventType.FAILED_REQUEST, this));
break;
case goog.net.EventType.ABORT:
case goog.net.EventType.COMPLETE:
break;
case goog.net.EventType.READY:
this.xhrIoReady = true;
break;
}
};
/**
* Retrieve a set of matching rows from the server via ajax
* @param {string} token The text that should be matched; passed to the server
* as the 'token' query param
* @param {number} maxMatches The maximum number of matches requested from the
* server; passed as the 'max_matches' query param. The server is
* responsible for limiting the number of matches that are returned
* @param {string} fullstring The complete text, including token
*/
plana.ui.ac.RemoteObjectMatcher.prototype.requestMatches = function(
token, maxMatches, fullstring) {
if (!this.xhrIoReady) {
this.xhrIo.abort();
}
var queryData = this.uri.getQueryData();
queryData.set(plana.ui.ac.RemoteObjectMatcher.TOKEN_PARA, token);
queryData.set(plana.ui.ac.RemoteObjectMatcher.MAX_MATCHES_PARA,
String(maxMatches));
queryData.set(plana.ui.ac.RemoteObjectMatcher.FULL_STRING_PARA, fullstring);
this.xhrIoReady = false;
this.xhrIo.send(this.uri.getPath(),
/**@type {string} */
(this.xhrIoRequestType_),
queryData.toString(), this.xhrIoRequestHeaders_);
};
/**
* This function returns the query data of the request objec so that
* a user can modify it on the fly
* @return {goog.Uri.QueryData}
*/
plana.ui.ac.RemoteObjectMatcher.prototype.getQueryData = function() {
return this.uri.getQueryData();
};
/**
* Setter for a map of headers to add to requests.
* @param {?Object} headers Headers to send along ajax requests
*/
plana.ui.ac.RemoteObjectMatcher.prototype.setRequestHeaders = function(
headers) {
this.xhrIoRequestHeaders_ = headers;
};
/**
* Setter for the type of request, i.e. 'GET' or 'POST'.
* @param {!string} requestType The request type of the ajax request
*/
plana.ui.ac.RemoteObjectMatcher.prototype.setRequestType = function(
requestType) {
goog.asserts.assert(
goog.string.caseInsensitiveCompare(requestType, 'get') == 0 ||
goog.string.caseInsensitiveCompare(requestType, 'post') == 0,
'request type must be get or post');
this.xhrIoRequestType_ = requestType;
};
/**
* Setter for the timeout for ajax requests
* @param {!number} timeout Set to 0 for no timeout
*/
plana.ui.ac.RemoteObjectMatcher.prototype.setRequestTimeout = function(
timeout) {
this.xhrIo.setTimeoutInterval(timeout);
};
/**
* List of events dispatched by this class
* @enum {!string}
*/
plana.ui.ac.RemoteObjectMatcher.EventType = {
/**
* @desc Event dispatched if ajax request
* did not succeed
*/
FAILED_REQUEST: goog.events.getUniqueId('failed'),
/**
* @desc Event dispatched when an array of matches has
* been returned from the server
*/
MATCHES: goog.events.getUniqueId('matches'),
/**
* @desc Event dispatched if the server object was not
* an array
*/
INVALID_RESPONSE: goog.events.getUniqueId('invalid')
};
/**
* A custom event class that has a matches property
* @constructor
* @extends {goog.events.Event}
* @param {string} type The event type
* @param {plana.ui.ac.RemoteObjectMatcher} target The remote
* matcher instance that triggered the event
* @param {Array.<plana.ui.ac.RemoteObject>} matches Array of
* matches
* @param {number=} opt_total Optional number of total matches
* available on the server
*/
plana.ui.ac.RemoteObjectMatcher.Event = function(
type, target, matches, opt_total) {
goog.events.Event.call(this, type, target);
/**
* Optional total number of matches available on the server
* @type {number}
* @public
*/
this.total = opt_total || 0;
/**
* Optional array of matches
* @type {Array.<plana.ui.ac.RemoteObject>}
* @public
*/
this.matches = matches;
};
goog.inherits(plana.ui.ac.RemoteObjectMatcher.Event, goog.events.Event);
/**
* A class to wrap a suggestion item returned by the server.
* If the server object has a 'caption' property, this property
* will be used to display the row in the autocomplete component.
* Otherwise the 'toString' method is used
* @constructor
* @extends {goog.Disposable}
* @param {Object|string|null} data The match object returned by the server
*/
plana.ui.ac.RemoteObject = function(data) {
goog.Disposable.call(this);
/**
* A match object.
* @type {?(Object|string)}
* @private
*/
this.data_ = data;
};
goog.inherits(plana.ui.ac.RemoteObject, goog.Disposable);
/**
* This function returns a clone
* @return {plana.ui.ac.RemoteObject}
*/
plana.ui.ac.RemoteObject.prototype.clone = function() {
return new plana.ui.ac.RemoteObject(this.data_);
};
/**
* If the server match has a caption property, we return
* this property. Otherwise call 'toString'
* @return {!string}
* @override
*/
plana.ui.ac.RemoteObject.prototype.toString = function() {
if (this.data_ == null)
return '';
if (goog.isString(this.data_))
return this.data_;
if (goog.isDefAndNotNull(
this.data_[plana.ui.ac.RemoteObjectMatcher.CAPTION_PROPERTY]))
return this.data_[plana.ui.ac.RemoteObjectMatcher.CAPTION_PROPERTY];
return this.data_.toString();
};
/**
* @override
*/
plana.ui.ac.RemoteObject.prototype.disposeInternal = function() {
plana.ui.ac.RemoteObject.superClass_.disposeInternal.call(this);
this.data_ = null;
};
/**
* Returns the match data
* @return {?(Object|string)}
*/
plana.ui.ac.RemoteObject.prototype.getData = function() {
return this.data_;
};