123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131 |
- // 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.TagName');
- 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}
- * @suppress {underscore}
- */
- 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 anchor element to position the rendered autocompleter against.
- * @protected {Element|undefined}
- */
- goog.ui.ac.Renderer.prototype.target_;
- /**
- * The element on which to base the width of the autocomplete.
- * @protected {Node}
- */
- goog.ui.ac.Renderer.prototype.widthProvider_;
- /**
- * The border width of the autocomplete dropdown, only used in calculating the
- * dropdown width.
- * @private {number}
- */
- goog.ui.ac.Renderer.prototype.borderWidth_ = 0;
- /**
- * 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.
- * @param {number=} opt_borderWidth The with of the border of the autocomplete,
- * which will be subtracted from the width of the autocomplete dropdown.
- */
- goog.ui.ac.Renderer.prototype.setWidthProvider = function(
- widthProvider, opt_borderWidth) {
- this.widthProvider_ = widthProvider;
- if (opt_borderWidth) {
- this.borderWidth_ = opt_borderWidth;
- }
- };
- /**
- * 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.visible_) {
- this.visible_ = false;
- this.toggleAriaMarkup_(false /* isShown */);
- 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;
- this.toggleAriaMarkup_(true /* isShown */);
- 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);
- }
- }
- };
- /**
- * Toggle the ARIA markup to add popup semantics when the target is shown and
- * to remove them when it is hidden.
- * @param {boolean} isShown Whether the menu is being shown.
- * @private
- */
- goog.ui.ac.Renderer.prototype.toggleAriaMarkup_ = function(isShown) {
- if (!this.target_) {
- return;
- }
- goog.a11y.aria.setState(this.target_, goog.a11y.aria.State.HASPOPUP, isShown);
- goog.a11y.aria.setState(
- goog.asserts.assert(this.element_), goog.a11y.aria.State.EXPANDED,
- isShown);
- goog.a11y.aria.setState(this.target_, goog.a11y.aria.State.EXPANDED, isShown);
- if (isShown) {
- goog.a11y.aria.setState(
- this.target_, goog.a11y.aria.State.OWNS, this.element_.id);
- } else {
- goog.a11y.aria.removeState(this.target_, goog.a11y.aria.State.OWNS);
- goog.a11y.aria.setActiveDescendant(this.target_, null);
- }
- };
- /**
- * @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(goog.dom.TagName.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 - this.borderWidth_ + '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_ || null;
- };
- /**
- * 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 preceded by non-alpha-numeric characters. Note: token could have |
- // operators in it, so we need to parenthesise it before adding \b to it.
- // or preceded 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(goog.dom.TagName.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(goog.dom.TagName.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) {};
|