blob: 49b494f8f2bffa0eac2111f08a75d3c7635b645f [file] [log] [blame]
// 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.a11y.aria');
goog.require('goog.a11y.aria.Role');
goog.require('goog.a11y.aria.State');
goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.dispose');
goog.require('goog.dom');
goog.require('goog.dom.NodeType');
goog.require('goog.dom.classlist');
goog.require('goog.events');
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.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');
/**
* 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. True by
* default.
* @extends {goog.events.EventTarget}
*/
goog.ui.ac.Renderer = function(opt_parentNode, opt_customRenderer,
opt_rightAlign, opt_useStandardHighlighting) {
goog.ui.ac.Renderer.base(this, 'constructor');
/**
* 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<!Object>}
* @private
*/
this.rows_ = [];
/**
* Array of the node divs that hold each result that is being displayed.
* @type {Array<Element>}
* @protected
* @suppress {underscore|visibility}
*/
this.rowDivs_ = [];
/**
* The index of the currently highlighted row
* @type {number}
* @protected
* @suppress {underscore|visibility}
*/
this.hilitedRow_ = -1;
/**
* The time that the rendering of the menu rows started
* @type {number}
* @protected
* @suppress {underscore|visibility}
*/
this.startRenderingRows_ = -1;
/**
* Store the current state for the renderer
* @type {boolean}
* @private
*/
this.visible_ = false;
/**
* Classname for the main element. This must be a single valid class name.
* @type {string}
*/
this.className = goog.getCssName('ac-renderer');
/**
* Classname for row divs. This must be a single valid class name.
* @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. This must be a single valid class name.
* 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 indicate whether matches should be done on whole words instead
* of any string.
* @type {boolean}
* @private
*/
this.matchWordBoundary_ = 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;
/**
* Whether we should limit the dropdown from extending past the bottom of the
* screen and instead show a scrollbar on the dropdown.
* @type {boolean}
* @private
*/
this.showScrollbarsIfTooLarge_ = false;
/**
* 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_;
/**
* A flag used to make sure we highlight only one match in the rendered row.
* @private {boolean}
*/
goog.ui.ac.Renderer.prototype.wasHighlightedAtLeastOnce_;
/**
* 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;
};
/**
* @return {boolean} Whether we should be aligning to the top of
* the target element.
*/
goog.ui.ac.Renderer.prototype.getTopAlign = function() {
return this.topAlign_;
};
/**
* 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;
};
/**
* @return {boolean} Whether the autocomplete menu should be right aligned.
*/
goog.ui.ac.Renderer.prototype.getRightAlign = function() {
return this.rightAlign_;
};
/**
* @param {boolean} show Whether we should limit the dropdown from extending
* past the bottom of the screen and instead show a scrollbar on the
* dropdown.
*/
goog.ui.ac.Renderer.prototype.setShowScrollbarsIfTooLarge = function(show) {
this.showScrollbarsIfTooLarge_ = show;
};
/**
* 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;
};
/**
* @param {boolean} matchWordBoundary Determines whether matches should be
* higlighted only when the token matches text at a whole-word boundary.
* True by default.
*/
goog.ui.ac.Renderer.prototype.setMatchWordBoundary =
function(matchWordBoundary) {
this.matchWordBoundary_ = matchWordBoundary;
};
/**
* 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;
};
/**
* @return {Element} The anchor element.
* @protected
*/
goog.ui.ac.Renderer.prototype.getAnchorElement = function() {
return this.anchorElement_;
};
/**
* Render the autocomplete UI
*
* @param {Array<!Object>} 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.a11y.aria.setActiveDescendant(this.target_, null);
}
if (this.visible_) {
this.visible_ = false;
// Clear ARIA popup role for the target input box.
if (this.target_) {
goog.a11y.aria.setState(this.target_,
goog.a11y.aria.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.setElementShown(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.a11y.aria.setRole(this.target_,
goog.a11y.aria.Role.COMBOBOX);
goog.a11y.aria.setState(this.target_,
goog.a11y.aria.State.AUTOCOMPLETE,
'list');
goog.a11y.aria.setState(this.target_,
goog.a11y.aria.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.setElementShown(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 row = index >= 0 && index < this.rows_.length ?
this.rows_[index] : undefined;
var rowDiv = index >= 0 && index < this.rowDivs_.length ?
this.rowDivs_[index] : undefined;
var evtObj = /** @lends {goog.events.Event.prototype} */ ({
type: goog.ui.ac.AutoComplete.EventType.ROW_HILITE,
rowNode: rowDiv,
row: row ? row.data : null
});
if (this.dispatchEvent(evtObj)) {
this.hiliteNone();
this.hilitedRow_ = index;
if (rowDiv) {
goog.dom.classlist.addAll(rowDiv, [this.activeClassName,
this.legacyActiveClassName_]);
if (this.target_) {
goog.a11y.aria.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.classlist.removeAll(
goog.asserts.assert(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} elem The container element.
* @private
*/
goog.ui.ac.Renderer.prototype.setMenuClasses_ = function(elem) {
goog.asserts.assert(elem);
// Legacy clients may set the renderer's className to a space-separated list
// or even have a trailing space.
goog.dom.classlist.addAll(elem, goog.string.trim(this.className).split(' '));
};
/**
* 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'});
if (this.showScrollbarsIfTooLarge_) {
// Make sure that the dropdown will get scrollbars if it isn't large
// enough to show all rows.
el.style.overflowY = 'auto';
}
this.element_ = el;
this.setMenuClasses_(el);
goog.a11y.aria.setRole(el, goog.a11y.aria.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.array.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);
};
/**
* @return {goog.positioning.Corner} The anchor corner to position the popup at.
* @protected
*/
goog.ui.ac.Renderer.prototype.getAnchorCorner = function() {
var anchorCorner = this.rightAlign_ ?
goog.positioning.Corner.BOTTOM_RIGHT :
goog.positioning.Corner.BOTTOM_LEFT;
if (this.topAlign_) {
anchorCorner = goog.positioning.flipCornerVertical(anchorCorner);
}
return anchorCorner;
};
/**
* 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.getAnchorCorner();
var overflowMode = goog.positioning.Overflow.ADJUST_X_EXCEPT_OFFSCREEN;
if (this.showScrollbarsIfTooLarge_) {
// positionAtAnchor will set the height of this.element_ when it runs
// (because of RESIZE_HEIGHT), and it will never increase it relative to
// its current value when it runs again. But if the user scrolls their
// page, then we might actually want a bigger height when the dropdown is
// displayed next time. So we clear the height before calling
// positionAtAnchor, so it is free to set the height as large as it
// chooses.
this.element_.style.height = '';
overflowMode |= goog.positioning.Overflow.RESIZE_HEIGHT;
}
goog.positioning.positionAtAnchor(
anchorElement, anchorCorner,
this.element_, goog.positioning.flipCornerVertical(anchorCorner),
null, null, overflowMode);
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;
};
/**
* @return {boolean} Whether the drop down will be autopositioned.
* @protected
*/
goog.ui.ac.Renderer.prototype.getAutoPosition = function() {
return this.reposition_;
};
/**
* @return {Element} The target element.
* @protected
*/
goog.ui.ac.Renderer.prototype.getTarget = function() {
return this.target_;
};
/**
* 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.ui.ac.Renderer.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) {
goog.dom.setTextContent(node, row.data.toString());
};
/**
* Goes through a node and all of its child nodes, replacing HTML text that
* matches a token with <b>token</b>.
* The replacement will happen on the first match or all matches depending on
* this.highlightAllTokens_ value.
*
* @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.startHiliteMatchingText_ =
function(node, tokenOrArray) {
this.wasHighlightedAtLeastOnce_ = false;
this.hiliteMatchingText_(node, tokenOrArray);
};
/**
* @param {Node} node Node to match.
* @param {string|Array<string>} tokenOrArray Token to match or array of tokens
* to match.
* @private
*/
goog.ui.ac.Renderer.prototype.hiliteMatchingText_ =
function(node, tokenOrArray) {
if (!this.highlightAllTokens_ && this.wasHighlightedAtLeastOnce_) {
return;
}
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: token could have |
// operators in it, so we need to parenthesise it before adding \b to it.
// or preceeded by non-alpha-numeric characters
//
// NOTE(user): When using word matches, this used to have
// a (^|\\W+) clause where it now has \\b but it caused various
// browsers to hang on really long strings. The (^|\\W+) matcher was also
// unnecessary, because \b already checks that the character before the
// is a non-word character, and ^ matches the start of the line or following
// a line terminator character, which is also \W. The regexp also used to
// have a capturing match before the \\b, which would capture the
// non-highlighted content, but that caused the regexp matching to run much
// slower than the current version.
var re = this.matchWordBoundary_ ?
new RegExp('\\b(?:' + token + ')', 'gi') :
new RegExp(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(text.substring(lastIndex, match.index));
textNodes.push(text.substring(match.index, re.lastIndex));
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('');
this.wasHighlightedAtLeastOnce_ = true;
} 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.isEmptyOrWhitespace(goog.string.makeSafe(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.isEmptyOrWhitespace(goog.string.makeSafe(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 element.
var elem = this.dom_.createDom('div', {
className: this.rowClassName,
id: goog.ui.IdGenerator.getInstance().getNextUniqueId()
});
goog.a11y.aria.setRole(elem, goog.a11y.aria.Role.OPTION);
if (this.customRenderer_ && this.customRenderer_.renderRow) {
this.customRenderer_.renderRow(row, token, elem);
} else {
this.renderRowContents_(row, token, elem);
}
if (token && this.useStandardHighlighting_) {
this.startHiliteMatchingText_(elem, token);
}
goog.dom.classlist.add(elem, this.rowClassName);
this.rowDivs_.push(elem);
return elem;
};
/**
* 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.classlist.contains(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(/** @lends {goog.events.Event.prototype} */ ({
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.
*
* Because of the type, this function cannot be documented with param JSDoc.
*
* The function expects the following parameters:
*
* renderer, goog.ui.ac.Renderer: The autocomplete renderer.
* element, Element: The main element that controls the rendered autocomplete.
* rows, Array: The current set of rows being displayed.
* token, string: The current token that has been entered. *
*
* @type {function(goog.ui.ac.Renderer, Element, Array, string)|
* null|undefined}
*/
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) {
};