This commit is contained in:
Éric Lemoine
2013-03-11 13:35:17 +01:00
parent 849774dceb
commit f150259eee
1189 changed files with 341774 additions and 2001 deletions
@@ -0,0 +1,50 @@
// Copyright 2012 The Closure Library Authors. All Rights Reserved.
//
// 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.
/**
* @fileoverview Utility methods supporting the autocomplete package.
*
* @see ../../demos/autocomplete-basic.html
*/
goog.provide('goog.ui.ac');
goog.require('goog.ui.ac.ArrayMatcher');
goog.require('goog.ui.ac.AutoComplete');
goog.require('goog.ui.ac.InputHandler');
goog.require('goog.ui.ac.Renderer');
/**
* Factory function for building a basic autocomplete widget that autocompletes
* an inputbox or text area from a data array.
* @param {Array} data Data array.
* @param {Element} input Input element or text area.
* @param {boolean=} opt_multi Whether to allow multiple entries separated with
* semi-colons or commas.
* @param {boolean=} opt_useSimilar use similar matches. e.g. "gost" => "ghost".
* @return {!goog.ui.ac.AutoComplete} A new autocomplete object.
*/
goog.ui.ac.createSimpleAutoComplete =
function(data, input, opt_multi, opt_useSimilar) {
var matcher = new goog.ui.ac.ArrayMatcher(data, !opt_useSimilar);
var renderer = new goog.ui.ac.Renderer();
var inputHandler = new goog.ui.ac.InputHandler(null, null, !!opt_multi);
var autoComplete = new goog.ui.ac.AutoComplete(
matcher, renderer, inputHandler);
inputHandler.attachAutoComplete(autoComplete);
inputHandler.attachInputs(input);
return autoComplete;
};
@@ -0,0 +1,164 @@
// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// 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.
/**
* @fileoverview Basic class for matching words in an array.
*
*/
goog.provide('goog.ui.ac.ArrayMatcher');
goog.require('goog.iter');
goog.require('goog.string');
/**
* Basic class for matching words in an array
* @constructor
* @param {Array} rows Dictionary of items to match. Can be objects if they
* have a toString method that returns the value to match against.
* @param {boolean=} opt_noSimilar if true, do not do similarity matches for the
* input token against the dictionary.
*/
goog.ui.ac.ArrayMatcher = function(rows, opt_noSimilar) {
this.rows_ = rows;
this.useSimilar_ = !opt_noSimilar;
};
/**
* Replaces the rows that this object searches over.
* @param {Array} rows Dictionary of items to match.
*/
goog.ui.ac.ArrayMatcher.prototype.setRows = function(rows) {
this.rows_ = rows;
};
/**
* Function used to pass matches to the autocomplete
* @param {string} token Token to match.
* @param {number} maxMatches Max number of matches to return.
* @param {Function} matchHandler callback to execute after matching.
* @param {string=} opt_fullString The full string from the input box.
*/
goog.ui.ac.ArrayMatcher.prototype.requestMatchingRows =
function(token, maxMatches, matchHandler, opt_fullString) {
var matches = this.getPrefixMatches(token, maxMatches);
if (matches.length == 0 && this.useSimilar_) {
matches = this.getSimilarRows(token, maxMatches);
}
matchHandler(token, matches);
};
/**
* Matches the token against the start of words in the row.
* @param {string} token Token to match.
* @param {number} maxMatches Max number of matches to return.
* @return {Array} Rows that match.
*/
goog.ui.ac.ArrayMatcher.prototype.getPrefixMatches =
function(token, maxMatches) {
var matches = [];
if (token != '') {
var escapedToken = goog.string.regExpEscape(token);
var matcher = new RegExp('(^|\\W+)' + escapedToken, 'i');
goog.iter.some(this.rows_, function(row) {
if (String(row).match(matcher)) {
matches.push(row);
}
return matches.length >= maxMatches;
});
}
return matches;
};
/**
* Matches the token against similar rows, by calculating "distance" between the
* terms.
* @param {string} token Token to match.
* @param {number} maxMatches Max number of matches to return.
* @return {Array} The best maxMatches rows.
*/
goog.ui.ac.ArrayMatcher.prototype.getSimilarRows =
function(token, maxMatches) {
var results = [];
goog.iter.forEach(this.rows_, function(row, index) {
var str = token.toLowerCase();
var txt = String(row).toLowerCase();
var score = 0;
if (txt.indexOf(str) != -1) {
score = parseInt((txt.indexOf(str) / 4).toString(), 10);
} else {
var arr = str.split('');
var lastPos = -1;
var penalty = 10;
for (var i = 0, c; c = arr[i]; i++) {
var pos = txt.indexOf(c);
if (pos > lastPos) {
var diff = pos - lastPos - 1;
if (diff > penalty - 5) {
diff = penalty - 5;
}
score += diff;
lastPos = pos;
} else {
score += penalty;
penalty += 5;
}
}
}
if (score < str.length * 6) {
results.push({
str: row,
score: score,
index: index
});
}
});
results.sort(function(a, b) {
var diff = a.score - b.score;
if (diff != 0) {
return diff;
}
return a.index - b.index;
});
var matches = [];
for (var i = 0; i < maxMatches && i < results.length; i++) {
matches.push(results[i].str);
}
return matches;
};
@@ -0,0 +1,736 @@
// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// 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.
/**
* @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.events');
goog.require('goog.events.EventTarget');
/**
* This is the central manager class for an AutoComplete instance.
*
* @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}
*/
goog.ui.ac.AutoComplete = function(matcher, renderer, selectionHandler) {
goog.events.EventTarget.call(this);
/**
* A data-source which provides autocomplete suggestions.
* @type {Object}
* @protected
* @suppress {underscore}
*/
this.matcher_ = matcher;
/**
* A handler which interacts with the input DOM element (textfield, textarea,
* or richedit).
* @type {Object}
* @protected
* @suppress {underscore}
*/
this.selectionHandler_ = selectionHandler;
/**
* A renderer to render/show/highlight/hide the autocomplete menu.
* @type {goog.events.EventTarget}
* @protected
* @suppress {underscore}
*/
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);
/**
* Currently typed token which will be used for completion.
* @type {?string}
* @protected
* @suppress {underscore}
*/
this.token_ = null;
/**
* Autcomplete suggestion items.
* @type {Array}
* @protected
* @suppress {underscore}
*/
this.rows_ = [];
/**
* Id of the currently highlighted row.
* @type {number}
* @protected
* @suppress {underscore}
*/
this.hiliteId_ = -1;
/**
* Id of the first row in autocomplete menu. Note that new ids are assigned
* everytime new suggestions are fetched.
* @type {number}
* @protected
* @suppress {underscore}
*/
this.firstRowId_ = 0;
/**
* The target HTML node for displaying.
* @type {Element}
* @protected
* @suppress {underscore}
*/
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'
};
/**
* Returns the renderer that renders/shows/highlights/hides the autocomplete
* menu.
* @return {goog.events.EventTarget} Renderer used by the this widget.
*/
goog.ui.ac.AutoComplete.prototype.getRenderer = function() {
return this.renderer_;
};
/**
* Generic event handler that handles any events this object is listening to.
* @param {goog.events.Event} e Event Object.
*/
goog.ui.ac.AutoComplete.prototype.handleEvent = function(e) {
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:
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) {
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) {
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) {
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) {
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) {
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) {
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() {
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) {
this.target_ = target;
};
/**
* @return {boolean} Whether the autocomplete's renderer is open.
*/
goog.ui.ac.AutoComplete.prototype.isOpen = function() {
return this.renderer_.isVisible();
};
/**
* @return {number} Number of rows in the autocomplete.
*/
goog.ui.ac.AutoComplete.prototype.getRowCount = function() {
return this.rows_.length;
};
/**
* Moves the hilite to the next row, or does nothing if we're already at the
* end of the current set of matches. Calls renderer.hiliteId() when there's
* something to do.
* @return {boolean} Returns true on a successful hilite.
*/
goog.ui.ac.AutoComplete.prototype.hiliteNext = function() {
var lastId = this.firstRowId_ + this.rows_.length - 1;
if (this.hiliteId_ >= this.firstRowId_ && this.hiliteId_ < lastId) {
this.hiliteId(this.hiliteId_ + 1);
return true;
} else if (this.hiliteId_ == -1) {
this.hiliteId(this.firstRowId_);
return true;
} else if (this.hiliteId_ == lastId) {
if (this.allowFreeSelect_) {
this.hiliteId(-1);
return false;
} else if (this.wrap_) {
this.hiliteId(this.firstRowId_);
return true;
}
}
return false;
};
/**
* Moves the hilite to the previous row, or does nothing if we're already at
* the beginning of the current set of matches. Calls renderer.hiliteId()
* when there's something to do.
* @return {boolean} Returns true on a successful hilite.
*/
goog.ui.ac.AutoComplete.prototype.hilitePrev = function() {
if (this.hiliteId_ > this.firstRowId_) {
this.hiliteId(this.hiliteId_ - 1);
return true;
} else if (this.allowFreeSelect_ && this.hiliteId_ == this.firstRowId_) {
this.hiliteId(-1);
return false;
} else if (this.wrap_ &&
(this.hiliteId_ == -1 || this.hiliteId_ == this.firstRowId_)) {
var lastId = this.firstRowId_ + this.rows_.length - 1;
this.hiliteId(lastId);
return true;
}
return false;
};
/**
* Hilites the id if it's valid, otherwise does nothing.
* @param {number} id A row id (not index).
* @return {boolean} Whether the id was hilited.
*/
goog.ui.ac.AutoComplete.prototype.hiliteId = function(id) {
this.hiliteId_ = id;
this.renderer_.hiliteId(id);
return this.getIndexOfId(id) != -1;
};
/**
* Hilites the index, if it's valid, 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) {
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() {
var index = this.getIndexOfId(this.hiliteId_);
if (index != -1) {
var selectedRow = this.rows_[index];
var suppressUpdate = 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
});
if (this.triggerSuggestionsOnUpdate_) {
this.selectionHandler_.update(true);
}
}
return true;
} else {
this.dismiss();
this.dispatchEvent(
{
type: goog.ui.ac.AutoComplete.EventType.UPDATE,
row: 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() {
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() {
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);
};
/**
* Call a dismiss after a delay, if there's already a dismiss active, ignore.
*/
goog.ui.ac.AutoComplete.prototype.dismissOnDelay = function() {
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() {
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() {
// 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() {
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) {
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.
*/
goog.ui.ac.AutoComplete.prototype.renderRows = function(rows, opt_options) {
// 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();
}
if ((autoHilite || indexToHilite >= 0) &&
rendRows.length != 0 &&
this.token_) {
var idToHilite = indexToHilite >= 0 ?
this.getIdOfIndex_(indexToHilite) : this.firstRowId_;
this.hiliteId(idToHilite);
} else {
this.hiliteId_ = -1;
}
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) {
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) {
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) {
// 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) {
// 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.
goog.array.forEach(arguments, function(input) {
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) {
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) {
var inputHandler = /** @type {goog.ui.ac.InputHandler} */
(this.selectionHandler_);
inputHandler.update(opt_force);
};
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,114 @@
// Copyright 2007 The Closure Library Authors. All Rights Reserved.
//
// 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.
/**
* @fileoverview Factory class to create a simple autocomplete that will match
* from an array of data provided via ajax.
*
* @see ../../demos/autocompleteremote.html
*/
goog.provide('goog.ui.ac.Remote');
goog.require('goog.ui.ac.AutoComplete');
goog.require('goog.ui.ac.InputHandler');
goog.require('goog.ui.ac.RemoteArrayMatcher');
goog.require('goog.ui.ac.Renderer');
/**
* Factory class for building a remote autocomplete widget that autocompletes
* an inputbox or text area from a data array provided via ajax.
* @param {string} url The Uri which generates the auto complete matches.
* @param {Element} input Input element or text area.
* @param {boolean=} opt_multi Whether to allow multiple entries; defaults
* to false.
* @param {boolean=} opt_useSimilar Whether to use similar matches; e.g.
* "gost" => "ghost".
* @constructor
* @extends {goog.ui.ac.AutoComplete}
*/
goog.ui.ac.Remote = function(url, input, opt_multi, opt_useSimilar) {
var matcher = new goog.ui.ac.RemoteArrayMatcher(url, !opt_useSimilar);
this.matcher_ = matcher;
var renderer = new goog.ui.ac.Renderer();
var inputhandler = new goog.ui.ac.InputHandler(null, null, !!opt_multi, 300);
goog.ui.ac.AutoComplete.call(this, matcher, renderer, inputhandler);
inputhandler.attachAutoComplete(this);
inputhandler.attachInputs(input);
};
goog.inherits(goog.ui.ac.Remote, goog.ui.ac.AutoComplete);
/**
* Set whether or not standard highlighting should be used when rendering rows.
* @param {boolean} useStandardHighlighting true if standard highlighting used.
*/
goog.ui.ac.Remote.prototype.setUseStandardHighlighting =
function(useStandardHighlighting) {
this.renderer_.setUseStandardHighlighting(useStandardHighlighting);
};
/**
* Gets the attached InputHandler object.
* @return {goog.ui.ac.InputHandler} The input handler.
*/
goog.ui.ac.Remote.prototype.getInputHandler = function() {
return /** @type {goog.ui.ac.InputHandler} */ (
this.selectionHandler_);
};
/**
* Set the send method ("GET", "POST") for the matcher.
* @param {string} method The send method; default: GET.
*/
goog.ui.ac.Remote.prototype.setMethod = function(method) {
this.matcher_.setMethod(method);
};
/**
* Set the post data for the matcher.
* @param {string} content Post data.
*/
goog.ui.ac.Remote.prototype.setContent = function(content) {
this.matcher_.setContent(content);
};
/**
* Set the HTTP headers for the matcher.
* @param {Object|goog.structs.Map} headers Map of headers to add to the
* request.
*/
goog.ui.ac.Remote.prototype.setHeaders = function(headers) {
this.matcher_.setHeaders(headers);
};
/**
* Set the timeout interval for the matcher.
* @param {number} interval Number of milliseconds after which an
* incomplete request will be aborted; 0 means no timeout is set.
*/
goog.ui.ac.Remote.prototype.setTimeoutInterval = function(interval) {
this.matcher_.setTimeoutInterval(interval);
};
@@ -0,0 +1,270 @@
// Copyright 2007 The Closure Library Authors. All Rights Reserved.
//
// 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.
/**
* @fileoverview Class that retrieves autocomplete matches via an ajax call.
*
*/
goog.provide('goog.ui.ac.RemoteArrayMatcher');
goog.require('goog.Disposable');
goog.require('goog.Uri');
goog.require('goog.events');
goog.require('goog.json');
goog.require('goog.net.XhrIo');
/**
* An array matcher that requests matches via ajax.
* @param {string} url The Uri which generates the auto complete matches. The
* search term is passed to the server as the 'token' query param.
* @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).
* @constructor
* @extends {goog.Disposable}
*/
goog.ui.ac.RemoteArrayMatcher = function(url, opt_noSimilar) {
goog.Disposable.call(this);
/**
* The base URL for the ajax call. The token and max_matches are added as
* query params.
* @type {string}
* @private
*/
this.url_ = url;
/**
* Whether similar matches should be found as well. This is sent as a hint
* to the server only.
* @type {boolean}
* @private
*/
this.useSimilar_ = !opt_noSimilar;
/**
* The XhrIo object used for making remote requests. When a new request
* is made, the current one is aborted and the new one sent.
* @type {goog.net.XhrIo}
* @private
*/
this.xhr_ = new goog.net.XhrIo();
};
goog.inherits(goog.ui.ac.RemoteArrayMatcher, goog.Disposable);
/**
* The HTTP send method (GET, POST) to use when making the ajax call.
* @type {string}
* @private
*/
goog.ui.ac.RemoteArrayMatcher.prototype.method_ = 'GET';
/**
* Data to submit during a POST.
* @type {string|undefined}
* @private
*/
goog.ui.ac.RemoteArrayMatcher.prototype.content_ = undefined;
/**
* Headers to send with every HTTP request.
* @type {Object|goog.structs.Map}
* @private
*/
goog.ui.ac.RemoteArrayMatcher.prototype.headers_ = null;
/**
* Key to the listener on XHR. Used to clear previous listeners.
* @type {?number}
* @private
*/
goog.ui.ac.RemoteArrayMatcher.prototype.lastListenerKey_ = null;
/**
* Set the send method ("GET", "POST").
* @param {string} method The send method; default: GET.
*/
goog.ui.ac.RemoteArrayMatcher.prototype.setMethod = function(method) {
this.method_ = method;
};
/**
* Set the post data.
* @param {string} content Post data.
*/
goog.ui.ac.RemoteArrayMatcher.prototype.setContent = function(content) {
this.content_ = content;
};
/**
* Set the HTTP headers.
* @param {Object|goog.structs.Map} headers Map of headers to add to the
* request.
*/
goog.ui.ac.RemoteArrayMatcher.prototype.setHeaders = function(headers) {
this.headers_ = headers;
};
/**
* Set the timeout interval.
* @param {number} interval Number of milliseconds after which an
* incomplete request will be aborted; 0 means no timeout is set.
*/
goog.ui.ac.RemoteArrayMatcher.prototype.setTimeoutInterval =
function(interval) {
this.xhr_.setTimeoutInterval(interval);
};
/**
* Builds a complete GET-style URL, given the base URI and autocomplete related
* parameter values.
* <b>Override this to build any customized lookup URLs.</b>
* <b>Can be used to change request method and any post content as well.</b>
* @param {string} uri The base URI of the request target.
* @param {string} token Current token in autocomplete.
* @param {number} maxMatches Maximum number of matches required.
* @param {boolean} useSimilar A hint to the server.
* @param {string=} opt_fullString Complete text in the input element.
* @return {?string} The complete url. Return null if no request should be sent.
* @protected
*/
goog.ui.ac.RemoteArrayMatcher.prototype.buildUrl = function(uri,
token, maxMatches, useSimilar, opt_fullString) {
var url = new goog.Uri(uri);
url.setParameterValue('token', token);
url.setParameterValue('max_matches', String(maxMatches));
url.setParameterValue('use_similar', String(Number(useSimilar)));
return url.toString();
};
/**
* Returns whether the suggestions should be updated?
* <b>Override this to prevent updates eg - when token is empty.</b>
* @param {string} uri The base URI of the request target.
* @param {string} token Current token in autocomplete.
* @param {number} maxMatches Maximum number of matches required.
* @param {boolean} useSimilar A hint to the server.
* @param {string=} opt_fullString Complete text in the input element.
* @return {boolean} Whether new matches be requested.
* @protected
*/
goog.ui.ac.RemoteArrayMatcher.prototype.shouldRequestMatches =
function(uri, token, maxMatches, useSimilar, opt_fullString) {
return true;
};
/**
* Parses and retrieves the array of suggestions from XHR response.
* <b>Override this if the response is not a simple JSON array.</b>
* @param {string} responseText The XHR response text.
* @return {Array.<string>} The array of suggestions.
* @protected
*/
goog.ui.ac.RemoteArrayMatcher.prototype.parseResponseText = function(
responseText) {
var matches = [];
// If there is no response text, unsafeParse will throw a syntax error.
if (responseText) {
/** @preserveTry */
try {
matches = goog.json.unsafeParse(responseText);
} catch (exception) {
}
}
return /** @type {Array.<string>} */ (matches);
};
/**
* Handles the XHR response.
* @param {string} token The XHR autocomplete token.
* @param {Function} matchHandler The AutoComplete match handler.
* @param {goog.events.Event} event The XHR success event.
*/
goog.ui.ac.RemoteArrayMatcher.prototype.xhrCallback = function(token,
matchHandler, event) {
var text = event.target.getResponseText();
matchHandler(token, this.parseResponseText(text));
};
/**
* 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 {Function} matchHandler Callback to execute on the result after
* matching.
* @param {string=} opt_fullString The full string from the input box.
*/
goog.ui.ac.RemoteArrayMatcher.prototype.requestMatchingRows =
function(token, maxMatches, matchHandler, opt_fullString) {
if (!this.shouldRequestMatches(this.url_, token, maxMatches, this.useSimilar_,
opt_fullString)) {
return;
}
// Set the query params on the URL.
var url = this.buildUrl(this.url_, token, maxMatches, this.useSimilar_,
opt_fullString);
if (!url) {
// Do nothing if there is no URL.
return;
}
// The callback evals the server response and calls the match handler on
// the array of matches.
var callback = goog.bind(this.xhrCallback, this, token, matchHandler);
// Abort the current request and issue the new one; prevent requests from
// being queued up by the browser with a slow server
if (this.xhr_.isActive()) {
this.xhr_.abort();
}
// This ensures if previous XHR is aborted or ends with error, the
// corresponding success-callbacks are cleared.
if (this.lastListenerKey_) {
goog.events.unlistenByKey(this.lastListenerKey_);
}
// Listen once ensures successful callback gets cleared by itself.
this.lastListenerKey_ = goog.events.listenOnce(this.xhr_,
goog.net.EventType.SUCCESS, callback);
this.xhr_.send(url, this.method_, this.content_, this.headers_);
};
/** @override */
goog.ui.ac.RemoteArrayMatcher.prototype.disposeInternal = function() {
this.xhr_.dispose();
goog.ui.ac.RemoteArrayMatcher.superClass_.disposeInternal.call(
this);
};
@@ -0,0 +1,936 @@
// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// 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.
/**
* @fileoverview Class for rendering the results of an auto complete and
* allow the user to select an row.
*
*/
goog.provide('goog.ui.ac.Renderer');
goog.provide('goog.ui.ac.Renderer.CustomRenderer');
goog.require('goog.dispose');
goog.require('goog.dom');
goog.require('goog.dom.a11y');
goog.require('goog.dom.classes');
goog.require('goog.events.Event');
goog.require('goog.events.EventTarget');
goog.require('goog.events.EventType');
goog.require('goog.fx.dom.FadeInAndShow');
goog.require('goog.fx.dom.FadeOutAndHide');
goog.require('goog.iter');
goog.require('goog.positioning');
goog.require('goog.positioning.Corner');
goog.require('goog.positioning.Overflow');
goog.require('goog.string');
goog.require('goog.style');
goog.require('goog.ui.IdGenerator');
goog.require('goog.ui.ac.AutoComplete.EventType');
goog.require('goog.userAgent');
/**
* Class for rendering the results of an auto-complete in a drop down list.
*
* @constructor
* @param {Element=} opt_parentNode optional reference to the parent element
* that will hold the autocomplete elements. goog.dom.getDocument().body
* will be used if this is null.
* @param {?({renderRow}|{render})=} opt_customRenderer Custom full renderer to
* render each row. Should be something with a renderRow or render method.
* @param {boolean=} opt_rightAlign Determines if the autocomplete will always
* be right aligned. False by default.
* @param {boolean=} opt_useStandardHighlighting Determines if standard
* highlighting should be applied to each row of data. Standard highlighting
* bolds every matching substring for a given token in each row.
* @extends {goog.events.EventTarget}
*/
goog.ui.ac.Renderer = function(opt_parentNode, opt_customRenderer,
opt_rightAlign, opt_useStandardHighlighting) {
goog.base(this);
/**
* Reference to the parent element that will hold the autocomplete elements
* @type {Element}
* @private
*/
this.parent_ = opt_parentNode || goog.dom.getDocument().body;
/**
* Dom helper for the parent element's document.
* @type {goog.dom.DomHelper}
* @private
*/
this.dom_ = goog.dom.getDomHelper(this.parent_);
/**
* Whether to reposition the autocomplete UI below the target node
* @type {boolean}
* @private
*/
this.reposition_ = !opt_parentNode;
/**
* Reference to the main element that controls the rendered autocomplete
* @type {Element}
* @private
*/
this.element_ = null;
/**
* The current token that has been entered
* @type {string}
* @private
*/
this.token_ = '';
/**
* Array used to store the current set of rows being displayed
* @type {Array}
* @private
*/
this.rows_ = [];
/**
* Array of the node divs that hold each result that is being displayed.
* @type {Array.<Element>}
* @protected
* @suppress {underscore}
*/
this.rowDivs_ = [];
/**
* The index of the currently highlighted row
* @type {number}
* @protected
* @suppress {underscore}
*/
this.hilitedRow_ = -1;
/**
* The time that the rendering of the menu rows started
* @type {number}
* @protected
* @suppress {underscore}
*/
this.startRenderingRows_ = -1;
/**
* Store the current state for the renderer
* @type {boolean}
* @private
*/
this.visible_ = false;
/**
* Classname for the main element
* @type {string}
*/
this.className = goog.getCssName('ac-renderer');
/**
* Classname for row divs
* @type {string}
*/
this.rowClassName = goog.getCssName('ac-row');
// TODO(gboyer): Remove this as soon as we remove references and ensure that
// no groups are pushing javascript using this.
/**
* The old class name for active row. This name is deprecated because its
* name is generic enough that a typical implementation would require a
* descendant selector.
* Active row will have rowClassName & activeClassName &
* legacyActiveClassName.
* @type {string}
* @private
*/
this.legacyActiveClassName_ = goog.getCssName('active');
/**
* Class name for active row div.
* Active row will have rowClassName & activeClassName &
* legacyActiveClassName.
* @type {string}
*/
this.activeClassName = goog.getCssName('ac-active');
/**
* Class name for the bold tag highlighting the matched part of the text.
* @type {string}
*/
this.highlightedClassName = goog.getCssName('ac-highlighted');
/**
* Custom full renderer
* @type {?({renderRow}|{render})}
* @private
*/
this.customRenderer_ = opt_customRenderer || null;
/**
* Flag to indicate whether standard highlighting should be applied.
* this is set to true if left unspecified to retain existing
* behaviour for autocomplete clients
* @type {boolean}
* @private
*/
this.useStandardHighlighting_ = opt_useStandardHighlighting != null ?
opt_useStandardHighlighting : true;
/**
* Flag to set all tokens as highlighted in the autocomplete row.
* @type {boolean}
* @private
*/
this.highlightAllTokens_ = false;
/**
* Determines if the autocomplete will always be right aligned
* @type {boolean}
* @private
*/
this.rightAlign_ = !!opt_rightAlign;
/**
* Whether to align with top of target field
* @type {boolean}
* @private
*/
this.topAlign_ = false;
/**
* Duration (in msec) of fade animation when menu is shown/hidden.
* Setting to 0 (default) disables animation entirely.
* @type {number}
* @private
*/
this.menuFadeDuration_ = 0;
/**
* Animation in progress, if any.
* @type {goog.fx.Animation|undefined}
*/
this.animation_;
};
goog.inherits(goog.ui.ac.Renderer, goog.events.EventTarget);
/**
* The anchor element to position the rendered autocompleter against.
* @type {Element}
* @private
*/
goog.ui.ac.Renderer.prototype.anchorElement_;
/**
* The element on which to base the width of the autocomplete.
* @type {Node}
* @private
*/
goog.ui.ac.Renderer.prototype.widthProvider_;
/**
* The delay before mouseover events are registered, in milliseconds
* @type {number}
* @const
*/
goog.ui.ac.Renderer.DELAY_BEFORE_MOUSEOVER = 300;
/**
* Gets the renderer's element.
* @return {Element} The main element that controls the rendered autocomplete.
*/
goog.ui.ac.Renderer.prototype.getElement = function() {
return this.element_;
};
/**
* Sets the width provider element. The provider is only used on redraw and as
* such will not automatically update on resize.
* @param {Node} widthProvider The element whose width should be mirrored.
*/
goog.ui.ac.Renderer.prototype.setWidthProvider = function(widthProvider) {
this.widthProvider_ = widthProvider;
};
/**
* Set whether to align autocomplete to top of target element
* @param {boolean} align If true, align to top.
*/
goog.ui.ac.Renderer.prototype.setTopAlign = function(align) {
this.topAlign_ = align;
};
/**
* Set whether to align autocomplete to the right of the target element.
* @param {boolean} align If true, align to right.
*/
goog.ui.ac.Renderer.prototype.setRightAlign = function(align) {
this.rightAlign_ = align;
};
/**
* Set whether or not standard highlighting should be used when rendering rows.
* @param {boolean} useStandardHighlighting true if standard highlighting used.
*/
goog.ui.ac.Renderer.prototype.setUseStandardHighlighting =
function(useStandardHighlighting) {
this.useStandardHighlighting_ = useStandardHighlighting;
};
/**
* Set whether or not to highlight all matching tokens rather than just the
* first.
* @param {boolean} highlightAllTokens Whether to highlight all matching tokens
* rather than just the first.
*/
goog.ui.ac.Renderer.prototype.setHighlightAllTokens =
function(highlightAllTokens) {
this.highlightAllTokens_ = highlightAllTokens;
};
/**
* Sets the duration (in msec) of the fade animation when menu is shown/hidden.
* Setting to 0 (default) disables animation entirely.
* @param {number} duration Duration (in msec) of the fade animation (or 0 for
* no animation).
*/
goog.ui.ac.Renderer.prototype.setMenuFadeDuration = function(duration) {
this.menuFadeDuration_ = duration;
};
/**
* Sets the anchor element for the subsequent call to renderRows.
* @param {Element} anchor The anchor element.
*/
goog.ui.ac.Renderer.prototype.setAnchorElement = function(anchor) {
this.anchorElement_ = anchor;
};
/**
* Render the autocomplete UI
*
* @param {Array} rows Matching UI rows.
* @param {string} token Token we are currently matching against.
* @param {Element=} opt_target Current HTML node, will position popup beneath
* this node.
*/
goog.ui.ac.Renderer.prototype.renderRows = function(rows, token, opt_target) {
this.token_ = token;
this.rows_ = rows;
this.hilitedRow_ = -1;
this.startRenderingRows_ = goog.now();
this.target_ = opt_target;
this.rowDivs_ = [];
this.redraw();
};
/**
* Hide the object.
*/
goog.ui.ac.Renderer.prototype.dismiss = function() {
if (this.target_) {
goog.dom.a11y.setActiveDescendant(this.target_, null);
}
if (this.visible_) {
this.visible_ = false;
// Clear ARIA popup role for the target input box.
if (this.target_) {
goog.dom.a11y.setState(this.target_, goog.dom.a11y.State.HASPOPUP, false);
}
if (this.menuFadeDuration_ > 0) {
goog.dispose(this.animation_);
this.animation_ = new goog.fx.dom.FadeOutAndHide(this.element_,
this.menuFadeDuration_);
this.animation_.play();
} else {
goog.style.showElement(this.element_, false);
}
}
};
/**
* Show the object.
*/
goog.ui.ac.Renderer.prototype.show = function() {
if (!this.visible_) {
this.visible_ = true;
// Set ARIA roles and states for the target input box.
if (this.target_) {
goog.dom.a11y.setRole(this.target_, goog.dom.a11y.Role.COMBOBOX);
goog.dom.a11y.setState(
this.target_, goog.dom.a11y.State.AUTOCOMPLETE, 'list');
goog.dom.a11y.setState(this.target_, goog.dom.a11y.State.HASPOPUP, true);
}
if (this.menuFadeDuration_ > 0) {
goog.dispose(this.animation_);
this.animation_ = new goog.fx.dom.FadeInAndShow(this.element_,
this.menuFadeDuration_);
this.animation_.play();
} else {
goog.style.showElement(this.element_, true);
}
}
};
/**
* @return {boolean} True if the object is visible.
*/
goog.ui.ac.Renderer.prototype.isVisible = function() {
return this.visible_;
};
/**
* Sets the 'active' class of the nth item.
* @param {number} index Index of the item to highlight.
*/
goog.ui.ac.Renderer.prototype.hiliteRow = function(index) {
var rowDiv = index >= 0 && index < this.rowDivs_.length ?
this.rowDivs_[index] : undefined;
var evtObj = {type: goog.ui.ac.AutoComplete.EventType.ROW_HILITE,
rowNode: rowDiv};
if (this.dispatchEvent(evtObj)) {
this.hiliteNone();
this.hilitedRow_ = index;
if (rowDiv) {
goog.dom.classes.add(rowDiv, this.activeClassName,
this.legacyActiveClassName_);
if (this.target_) {
goog.dom.a11y.setActiveDescendant(this.target_, rowDiv);
}
goog.style.scrollIntoContainerView(rowDiv, this.element_);
}
}
};
/**
* Removes the 'active' class from the currently selected row.
*/
goog.ui.ac.Renderer.prototype.hiliteNone = function() {
if (this.hilitedRow_ >= 0) {
goog.dom.classes.remove(this.rowDivs_[this.hilitedRow_],
this.activeClassName, this.legacyActiveClassName_);
}
};
/**
* Sets the 'active' class of the item with a given id.
* @param {number} id Id of the row to hilight. If id is -1 then no rows get
* hilited.
*/
goog.ui.ac.Renderer.prototype.hiliteId = function(id) {
if (id == -1) {
this.hiliteRow(-1);
} else {
for (var i = 0; i < this.rows_.length; i++) {
if (this.rows_[i].id == id) {
this.hiliteRow(i);
return;
}
}
}
};
/**
* Sets CSS classes on autocomplete conatainer element.
*
* @param {Element} elt The container element.
* @private
*/
goog.ui.ac.Renderer.prototype.setMenuClasses_ = function(elt) {
goog.dom.classes.add(elt, this.className);
};
/**
* If the main HTML element hasn't been made yet, creates it and appends it
* to the parent.
* @private
*/
goog.ui.ac.Renderer.prototype.maybeCreateElement_ = function() {
if (!this.element_) {
// Make element and add it to the parent
var el = this.dom_.createDom('div', {style: 'display:none'});
this.element_ = el;
this.setMenuClasses_(el);
goog.dom.a11y.setRole(el, goog.dom.a11y.Role.LISTBOX);
el.id = goog.ui.IdGenerator.getInstance().getNextUniqueId();
this.dom_.appendChild(this.parent_, el);
// Add this object as an event handler
goog.events.listen(el, goog.events.EventType.CLICK,
this.handleClick_, false, this);
goog.events.listen(el, goog.events.EventType.MOUSEDOWN,
this.handleMouseDown_, false, this);
goog.events.listen(el, goog.events.EventType.MOUSEOVER,
this.handleMouseOver_, false, this);
}
};
/**
* Redraw (or draw if this is the first call) the rendered auto-complete drop
* down.
*/
goog.ui.ac.Renderer.prototype.redraw = function() {
// Create the element if it doesn't yet exist
this.maybeCreateElement_();
// For top aligned with target (= bottom aligned element),
// we need to hide and then add elements while hidden to prevent
// visible repositioning
if (this.topAlign_) {
this.element_.style.visibility = 'hidden';
}
if (this.widthProvider_) {
var width = this.widthProvider_.clientWidth + 'px';
this.element_.style.minWidth = width;
}
// Remove the current child nodes
this.rowDivs_.length = 0;
this.dom_.removeChildren(this.element_);
// Generate the new rows (use forEach so we can change rows_ from an
// array to a different datastructure if required)
if (this.customRenderer_ && this.customRenderer_.render) {
this.customRenderer_.render(this, this.element_, this.rows_, this.token_);
} else {
var curRow = null;
goog.iter.forEach(this.rows_, function(row) {
row = this.renderRowHtml(row, this.token_);
if (this.topAlign_) {
// Aligned with top of target = best match at bottom
this.element_.insertBefore(row, curRow);
} else {
this.dom_.appendChild(this.element_, row);
}
curRow = row;
}, this);
}
// Don't show empty result sets
if (this.rows_.length == 0) {
this.dismiss();
return;
} else {
this.show();
}
this.reposition();
// Make the autocompleter unselectable, so that it
// doesn't steal focus from the input field when clicked.
goog.style.setUnselectable(this.element_, true);
};
/**
* Repositions the auto complete popup relative to the location node, if it
* exists and the auto position has been set.
*/
goog.ui.ac.Renderer.prototype.reposition = function() {
if (this.target_ && this.reposition_) {
var anchorElement = this.anchorElement_ || this.target_;
var anchorCorner = this.rightAlign_ ?
goog.positioning.Corner.BOTTOM_RIGHT :
goog.positioning.Corner.BOTTOM_LEFT;
if (this.topAlign_) {
anchorCorner = goog.positioning.flipCornerVertical(anchorCorner);
}
goog.positioning.positionAtAnchor(
anchorElement, anchorCorner,
this.element_, goog.positioning.flipCornerVertical(anchorCorner),
null, null, goog.positioning.Overflow.ADJUST_X_EXCEPT_OFFSCREEN);
if (this.topAlign_) {
// This flickers, but is better than the alternative of positioning
// in the wrong place and then moving.
this.element_.style.visibility = 'visible';
}
}
};
/**
* Sets whether the renderer should try to determine where to position the
* drop down.
* @param {boolean} auto Whether to autoposition the drop down.
*/
goog.ui.ac.Renderer.prototype.setAutoPosition = function(auto) {
this.reposition_ = auto;
};
/**
* Disposes of the renderer and its associated HTML.
* @override
* @protected
*/
goog.ui.ac.Renderer.prototype.disposeInternal = function() {
if (this.element_) {
goog.events.unlisten(this.element_, goog.events.EventType.CLICK,
this.handleClick_, false, this);
goog.events.unlisten(this.element_, goog.events.EventType.MOUSEDOWN,
this.handleMouseDown_, false, this);
goog.events.unlisten(this.element_, goog.events.EventType.MOUSEOVER,
this.handleMouseOver_, false, this);
this.dom_.removeNode(this.element_);
this.element_ = null;
this.visible_ = false;
}
goog.dispose(this.animation_);
this.parent_ = null;
goog.base(this, 'disposeInternal');
};
/**
* Generic function that takes a row and renders a DOM structure for that row.
*
* Normally this will only be matching a maximum of 20 or so items. Even with
* 40 rows, DOM this building is fine.
*
* @param {Object} row Object representing row.
* @param {string} token Token to highlight.
* @param {Node} node The node to render into.
* @private
*/
goog.ui.ac.Renderer.prototype.renderRowContents_ =
function(row, token, node) {
node.innerHTML = goog.string.htmlEscape(row.data.toString());
};
/**
* Goes through a node and all of its child nodes, replacing HTML text that
* matches a token with <b>token</b>.
*
* @param {Node} node Node to match.
* @param {string|Array.<string>} tokenOrArray Token to match or array of tokens
* to match. By default, only the first match will be highlighted. If
* highlightAllTokens is set, then all tokens appearing at the start of a
* word, in whatever order and however many times, will be highlighted.
* @private
*/
goog.ui.ac.Renderer.prototype.hiliteMatchingText_ =
function(node, tokenOrArray) {
if (node.nodeType == goog.dom.NodeType.TEXT) {
var rest = null;
if (goog.isArray(tokenOrArray) &&
tokenOrArray.length > 1 &&
!this.highlightAllTokens_) {
rest = goog.array.slice(tokenOrArray, 1);
}
var token = this.getTokenRegExp_(tokenOrArray);
if (token.length == 0) return;
var text = node.nodeValue;
// Create a regular expression to match a token at the beginning of a line
// or preceeded by non-alpha-numeric characters
// NOTE(user): this used to have a (^|\\W+) clause where it now has \\b
// but it caused various browsers to hang on really long strings. It is
// also excessive, because .*?\W+ is the same as .*?\b since \b already
// checks that the character before the token is a non-word character
// (the only time the regexp is different is if token begins with a
// non-word character), and ^ matches the start of the line or following
// a line terminator character, which is also \W. The initial group cannot
// just be .*? as it will miss line terminators (which is what the \W+
// clause used to match). Instead we use [\s\S] to match every character,
// including line terminators.
var re = new RegExp('([\\s\\S]*?)\\b(' + token + ')', 'gi');
var textNodes = [];
var lastIndex = 0;
// Find all matches
// Note: text.split(re) has inconsistencies between IE and FF, so
// manually recreated the logic
var match = re.exec(text);
var numMatches = 0;
while (match) {
numMatches++;
textNodes.push(match[1]);
textNodes.push(match[2]);
lastIndex = re.lastIndex;
match = re.exec(text);
}
textNodes.push(text.substring(lastIndex));
// Replace the tokens with bolded text. Each pair of textNodes
// (starting at index idx) includes a node of text before the bolded
// token, and a node (at idx + 1) consisting of what should be
// enclosed in bold tags.
if (textNodes.length > 1) {
var maxNumToBold = !this.highlightAllTokens_ ? 1 : numMatches;
for (var i = 0; i < maxNumToBold; i++) {
var idx = 2 * i;
node.nodeValue = textNodes[idx];
var boldTag = this.dom_.createElement('b');
boldTag.className = this.highlightedClassName;
this.dom_.appendChild(boldTag,
this.dom_.createTextNode(textNodes[idx + 1]));
boldTag = node.parentNode.insertBefore(boldTag, node.nextSibling);
node.parentNode.insertBefore(this.dom_.createTextNode(''),
boldTag.nextSibling);
node = boldTag.nextSibling;
}
// Append the remaining text nodes to the end.
var remainingTextNodes = goog.array.slice(textNodes, maxNumToBold * 2);
node.nodeValue = remainingTextNodes.join('');
} else if (rest) {
this.hiliteMatchingText_(node, rest);
}
} else {
var child = node.firstChild;
while (child) {
var nextChild = child.nextSibling;
this.hiliteMatchingText_(child, tokenOrArray);
child = nextChild;
}
}
};
/**
* Transforms a token into a string ready to be put into the regular expression
* in hiliteMatchingText_.
* @param {string|Array.<string>} tokenOrArray The token or array to get the
* regex string from.
* @return {string} The regex-ready token.
* @private
*/
goog.ui.ac.Renderer.prototype.getTokenRegExp_ = function(tokenOrArray) {
var token = '';
if (!tokenOrArray) {
return token;
}
if (goog.isArray(tokenOrArray)) {
// Remove invalid tokens from the array, which may leave us with nothing.
tokenOrArray = goog.array.filter(tokenOrArray, function(str) {
return !goog.string.isEmptySafe(str);
});
}
// If highlighting all tokens, join them with '|' so the regular expression
// will match on any of them.
if (this.highlightAllTokens_) {
if (goog.isArray(tokenOrArray)) {
var tokenArray = goog.array.map(tokenOrArray, goog.string.regExpEscape);
token = tokenArray.join('|');
} else {
// Remove excess whitespace from the string so bars will separate valid
// tokens in the regular expression.
token = goog.string.collapseWhitespace(tokenOrArray);
token = goog.string.regExpEscape(token);
token = token.replace(/ /g, '|');
}
} else {
// Not highlighting all matching tokens. If tokenOrArray is a string, use
// that as the token. If it is an array, use the first element in the
// array.
// TODO(user): why is this this way?. We should match against all
// tokens in the array, but only accept the first match.
if (goog.isArray(tokenOrArray)) {
token = tokenOrArray.length > 0 ?
goog.string.regExpEscape(tokenOrArray[0]) : '';
} else {
// For the single-match string token, we refuse to match anything if
// the string begins with a non-word character, as matches by definition
// can only occur at the start of a word. (This also handles the
// goog.string.isEmptySafe(tokenOrArray) case.)
if (!/^\W/.test(tokenOrArray)) {
token = goog.string.regExpEscape(tokenOrArray);
}
}
}
return token;
};
/**
* Render a row by creating a div and then calling row rendering callback or
* default row handler
*
* @param {Object} row Object representing row.
* @param {string} token Token to highlight.
* @return {Element} An element with the rendered HTML.
*/
goog.ui.ac.Renderer.prototype.renderRowHtml = function(row, token) {
// Create and return the node
var node = this.dom_.createDom('div', {
className: this.rowClassName,
id: goog.ui.IdGenerator.getInstance().getNextUniqueId()
});
goog.dom.a11y.setRole(node, goog.dom.a11y.Role.OPTION);
if (this.customRenderer_ && this.customRenderer_.renderRow) {
this.customRenderer_.renderRow(row, token, node);
} else {
this.renderRowContents_(row, token, node);
}
if (token && this.useStandardHighlighting_) {
this.hiliteMatchingText_(node, token);
}
goog.dom.classes.add(node, this.rowClassName);
this.rowDivs_.push(node);
return node;
};
/**
* Given an event target looks up through the parents till it finds a div. Once
* found it will then look to see if that is one of the childnodes, if it is
* then the index is returned, otherwise -1 is returned.
* @param {Element} et HtmlElement.
* @return {number} Index corresponding to event target.
* @private
*/
goog.ui.ac.Renderer.prototype.getRowFromEventTarget_ = function(et) {
while (et && et != this.element_ &&
!goog.dom.classes.has(et, this.rowClassName)) {
et = /** @type {Element} */ (et.parentNode);
}
return et ? goog.array.indexOf(this.rowDivs_, et) : -1;
};
/**
* Handle the click events. These are redirected to the AutoComplete object
* which then makes a callback to select the correct row.
* @param {goog.events.Event} e Browser event object.
* @private
*/
goog.ui.ac.Renderer.prototype.handleClick_ = function(e) {
var index = this.getRowFromEventTarget_(/** @type {Element} */ (e.target));
if (index >= 0) {
this.dispatchEvent({
type: goog.ui.ac.AutoComplete.EventType.SELECT,
row: this.rows_[index].id
});
}
e.stopPropagation();
};
/**
* Handle the mousedown event and prevent the AC from losing focus.
* @param {goog.events.Event} e Browser event object.
* @private
*/
goog.ui.ac.Renderer.prototype.handleMouseDown_ = function(e) {
e.stopPropagation();
e.preventDefault();
};
/**
* Handle the mousing events. These are redirected to the AutoComplete object
* which then makes a callback to set the correctly highlighted row. This is
* because the AutoComplete can move the focus as well, and there is no sense
* duplicating the code
* @param {goog.events.Event} e Browser event object.
* @private
*/
goog.ui.ac.Renderer.prototype.handleMouseOver_ = function(e) {
var index = this.getRowFromEventTarget_(/** @type {Element} */ (e.target));
if (index >= 0) {
if ((goog.now() - this.startRenderingRows_) <
goog.ui.ac.Renderer.DELAY_BEFORE_MOUSEOVER) {
return;
}
this.dispatchEvent({
type: goog.ui.ac.AutoComplete.EventType.HILITE,
row: this.rows_[index].id
});
}
};
/**
* Class allowing different implementations to custom render the autocomplete.
* Extending classes should override the render function.
* @constructor
*/
goog.ui.ac.Renderer.CustomRenderer = function() {
};
/**
* Renders the autocomplete box. May be set to null.
* @type {function(goog.ui.ac.Renderer, Element, Array, string)|
* null|undefined}
* param {goog.ui.ac.Renderer} renderer The autocomplete renderer.
* param {Element} element The main element that controls the rendered
* autocomplete.
* param {Array} rows The current set of rows being displayed.
* param {string} token The current token that has been entered.
*/
goog.ui.ac.Renderer.CustomRenderer.prototype.render = function(
renderer, element, rows, token) {
};
/**
* Generic function that takes a row and renders a DOM structure for that row.
* @param {Object} row Object representing row.
* @param {string} token Token to highlight.
* @param {Node} node The node to render into.
*/
goog.ui.ac.Renderer.CustomRenderer.prototype.renderRow =
function(row, token, node) {
};
@@ -0,0 +1,80 @@
// Copyright 2012 The Closure Library Authors. All Rights Reserved.
//
// 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.
/**
* @fileoverview Options for rendering matches.
*
*/
goog.provide('goog.ui.ac.RenderOptions');
/**
* A simple class that contains options for rendering a set of autocomplete
* matches. Used as an optional argument in the callback from the matcher.
* @constructor
*/
goog.ui.ac.RenderOptions = function() {
};
/**
* Whether the current highlighting is to be preserved when displaying the new
* set of matches.
* @type {boolean}
* @private
*/
goog.ui.ac.RenderOptions.prototype.preserveHilited_ = false;
/**
* Whether the first match is to be highlighted. When undefined the autoHilite
* flag of the autocomplete is used.
* @type {boolean|undefined}
* @private
*/
goog.ui.ac.RenderOptions.prototype.autoHilite_;
/**
* @param {boolean} flag The new value for the preserveHilited_ flag.
*/
goog.ui.ac.RenderOptions.prototype.setPreserveHilited = function(flag) {
this.preserveHilited_ = flag;
};
/**
* @return {boolean} The value of the preserveHilited_ flag.
*/
goog.ui.ac.RenderOptions.prototype.getPreserveHilited = function() {
return this.preserveHilited_;
};
/**
* @param {boolean} flag The new value for the autoHilite_ flag.
*/
goog.ui.ac.RenderOptions.prototype.setAutoHilite = function(flag) {
this.autoHilite_ = flag;
};
/**
* @return {boolean|undefined} The value of the autoHilite_ flag.
*/
goog.ui.ac.RenderOptions.prototype.getAutoHilite = function() {
return this.autoHilite_;
};
@@ -0,0 +1,58 @@
// Copyright 2007 The Closure Library Authors. All Rights Reserved.
//
// 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.
/**
* @fileoverview Class for managing the interactions between a rich autocomplete
* object and a text-input or textarea.
*
*/
goog.provide('goog.ui.ac.RichInputHandler');
goog.require('goog.ui.ac.InputHandler');
/**
* Class for managing the interaction between an autocomplete object and a
* text-input or textarea.
* @param {?string=} opt_separators Seperators to split multiple entries.
* @param {?string=} opt_literals Characters used to delimit text literals.
* @param {?boolean=} opt_multi Whether to allow multiple entries
* (Default: true).
* @param {?number=} opt_throttleTime Number of milliseconds to throttle
* keyevents with (Default: 150).
* @constructor
* @extends {goog.ui.ac.InputHandler}
*/
goog.ui.ac.RichInputHandler = function(opt_separators, opt_literals,
opt_multi, opt_throttleTime) {
goog.ui.ac.InputHandler.call(this, opt_separators, opt_literals,
opt_multi, opt_throttleTime);
};
goog.inherits(goog.ui.ac.RichInputHandler, goog.ui.ac.InputHandler);
/**
* Selects the given rich row. The row's select(target) method is called.
* @param {Object} row The row to select.
* @return {boolean} Whether to suppress the update event.
* @override
*/
goog.ui.ac.RichInputHandler.prototype.selectRow = function(row) {
var suppressUpdate = goog.ui.ac.RichInputHandler.superClass_
.selectRow.call(this, row);
row.select(this.ac_.getTarget());
return suppressUpdate;
};
@@ -0,0 +1,107 @@
// Copyright 2007 The Closure Library Authors. All Rights Reserved.
//
// 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.
/**
* @fileoverview Factory class to create a rich autocomplete that will match
* from an array of data provided via ajax. The server returns a complex data
* structure that is used with client-side javascript functions to render the
* results.
*
* The server sends a list of the form:
* [["type1", {...}, {...}, ...], ["type2", {...}, {...}, ...], ...]
* The first element of each sublist is a string designating the type of the
* hashes in the sublist, each of which represents one match. The type string
* must be the name of a function(item) which converts the hash into a rich
* row that contains both a render(node, token) and a select(target) method.
* The render method is called by the renderer when rendering the rich row,
* and the select method is called by the RichInputHandler when the rich row is
* selected.
*
* @see ../../demos/autocompleterichremote.html
*/
goog.provide('goog.ui.ac.RichRemote');
goog.require('goog.ui.ac.AutoComplete');
goog.require('goog.ui.ac.Remote');
goog.require('goog.ui.ac.Renderer');
goog.require('goog.ui.ac.RichInputHandler');
goog.require('goog.ui.ac.RichRemoteArrayMatcher');
/**
* Factory class to create a rich autocomplete widget that autocompletes an
* inputbox or textarea from data provided via ajax. The server returns a
* complex data structure that is used with client-side javascript functions to
* render the results.
* @param {string} url The Uri which generates the auto complete matches.
* @param {Element} input Input element or text area.
* @param {boolean=} opt_multi Whether to allow multiple entries; defaults
* to false.
* @param {boolean=} opt_useSimilar Whether to use similar matches; e.g.
* "gost" => "ghost".
* @constructor
* @extends {goog.ui.ac.Remote}
*/
goog.ui.ac.RichRemote = function(url, input, opt_multi, opt_useSimilar) {
// Create a custom renderer that renders rich rows. The renderer calls
// row.render(node, token) for each row.
var customRenderer = {};
customRenderer.renderRow = function(row, token, node) {
return row.data.render(node, token);
};
/**
* A standard renderer that uses a custom row renderer to display the
* rich rows generated by this autocomplete widget.
* @type {goog.ui.ac.Renderer}
* @private
*/
var renderer = new goog.ui.ac.Renderer(null, customRenderer);
this.renderer_ = renderer;
/**
* A remote matcher that parses rich results returned by the server.
* @type {goog.ui.ac.RichRemoteArrayMatcher}
* @private
*/
var matcher = new goog.ui.ac.RichRemoteArrayMatcher(url,
!opt_useSimilar);
this.matcher_ = matcher;
/**
* An input handler that calls select on a row when it is selected.
* @type {goog.ui.ac.RichInputHandler}
* @private
*/
var inputhandler = new goog.ui.ac.RichInputHandler(null, null,
!!opt_multi, 300);
// Create the widget and connect it to the input handler.
goog.ui.ac.AutoComplete.call(this, matcher, renderer, inputhandler);
inputhandler.attachAutoComplete(this);
inputhandler.attachInputs(input);
};
goog.inherits(goog.ui.ac.RichRemote, goog.ui.ac.Remote);
/**
* Set the filter that is called before the array matches are returned.
* @param {Function} rowFilter A function(rows) that returns an array of rows as
* a subset of the rows input array.
*/
goog.ui.ac.RichRemote.prototype.setRowFilter = function(rowFilter) {
this.matcher_.setRowFilter(rowFilter);
};
@@ -0,0 +1,124 @@
// Copyright 2007 The Closure Library Authors. All Rights Reserved.
//
// 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.
/**
* @fileoverview Class that retrieves rich autocomplete matches, represented as
* a structured list of lists, via an ajax call. The first element of each
* sublist is the name of a client-side javascript function that converts the
* remaining sublist elements into rich rows.
*
*/
goog.provide('goog.ui.ac.RichRemoteArrayMatcher');
goog.require('goog.ui.ac.RemoteArrayMatcher');
/**
* An array matcher that requests rich matches via ajax and converts them into
* rich rows.
* @param {string} url The Uri which generates the auto complete matches. The
* search term is passed to the server as the 'token' query param.
* @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).
* @constructor
* @extends {goog.ui.ac.RemoteArrayMatcher}
*/
goog.ui.ac.RichRemoteArrayMatcher = function(url, opt_noSimilar) {
goog.ui.ac.RemoteArrayMatcher.call(this, url, opt_noSimilar);
/**
* A function(rows) that is called before the array matches are returned.
* It runs client-side and filters the results given by the server before
* being rendered by the client.
* @type {Function}
* @private
*/
this.rowFilter_ = null;
};
goog.inherits(goog.ui.ac.RichRemoteArrayMatcher, goog.ui.ac.RemoteArrayMatcher);
/**
* Set the filter that is called before the array matches are returned.
* @param {Function} rowFilter A function(rows) that returns an array of rows as
* a subset of the rows input array.
*/
goog.ui.ac.RichRemoteArrayMatcher.prototype.setRowFilter = function(rowFilter) {
this.rowFilter_ = rowFilter;
};
/**
* Retrieve a set of matching rows from the server via ajax and convert them
* into rich rows.
* @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 {Function} matchHandler Callback to execute on the result after
* matching.
* @override
*/
goog.ui.ac.RichRemoteArrayMatcher.prototype.requestMatchingRows =
function(token, maxMatches, matchHandler) {
// The RichRemoteArrayMatcher must map over the results and filter them
// before calling the request matchHandler. This is done by passing
// myMatchHandler to RemoteArrayMatcher.requestMatchingRows which maps,
// filters, and then calls matchHandler.
var myMatchHandler = goog.bind(function(token, matches) {
/** @preserveTry */
try {
var rows = [];
for (var i = 0; i < matches.length; i++) {
var func = /** @type {!Function} */
(goog.json.unsafeParse(matches[i][0]));
for (var j = 1; j < matches[i].length; j++) {
var richRow = func(matches[i][j]);
rows.push(richRow);
// If no render function was provided, set the node's innerHTML.
if (typeof richRow.render == 'undefined') {
richRow.render = function(node, token) {
node.innerHTML = richRow.toString();
};
}
// If no select function was provided, set the text of the input.
if (typeof richRow.select == 'undefined') {
richRow.select = function(target) {
target.value = richRow.toString();
};
}
}
}
if (this.rowFilter_) {
rows = this.rowFilter_(rows);
}
matchHandler(token, rows);
} catch (exception) {
// TODO(user): Is this what we want?
matchHandler(token, []);
}
}, this);
// Call the super's requestMatchingRows with myMatchHandler
goog.ui.ac.RichRemoteArrayMatcher.superClass_
.requestMatchingRows.call(this, token, maxMatches, myMatchHandler);
};