UNPKG

sku-pg

Version:

Skulpt is a Javascript implementation of Python 2.x. Python that runs in your browser!

854 lines (732 loc) 25.7 kB
// 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.AutoComplete.Renderer'); goog.provide('goog.ui.AutoComplete.Renderer.CustomRenderer'); goog.require('goog.dom'); goog.require('goog.dom.a11y'); goog.require('goog.dom.classes'); goog.require('goog.events.EventTarget'); goog.require('goog.iter'); goog.require('goog.string'); goog.require('goog.style'); goog.require('goog.ui.AutoComplete'); goog.require('goog.ui.IdGenerator'); 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.AutoComplete.Renderer = function(opt_parentNode, opt_customRenderer, opt_rightAlign, opt_useStandardHighlighting) { /** * 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_ = []; /** * 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(user): 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 != null ? opt_rightAlign : false; /** * Alignment lock that forces all alignment to the give type, RIGHT | LEFT. * This is set the first time the renderer is aligned. * @type {?string} * @private */ this.keepAligned_ = null; /** * Whether to align with top of target field * @type {boolean} * @private */ this.topAlign_ = false; // TODO(user): Remove once JSCompiler's undefined properties warnings // don't error for guarded properties. var magicProps = { renderRow: 0 }; }; goog.inherits(goog.ui.AutoComplete.Renderer, goog.events.EventTarget); /** * The delay before mouseover events are registered, in milliseconds * @type {number} */ goog.ui.AutoComplete.Renderer.DELAY_BEFORE_MOUSEOVER = 300; /** * Gets the renderer's element. * @return {Element} The main element that controls the rendered autocomplete. */ goog.ui.AutoComplete.Renderer.prototype.getElement = function() { return this.element_; }; /** * Set whether to align autocomplete to top of target element * @param {boolean} align If true, align to top. */ goog.ui.AutoComplete.Renderer.prototype.setTopAlign = function(align) { this.topAlign_ = align; }; /** * Set whether or not standard highlighting should be used when rendering rows. * @param {boolean} useStandardHighlighting true if standard highlighting used. */ goog.ui.AutoComplete.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.AutoComplete.Renderer.prototype.setHighlightAllTokens = function(highlightAllTokens) { this.highlightAllTokens_ = highlightAllTokens; }; /** * 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.AutoComplete.Renderer.prototype.renderRows = function(rows, token, opt_target) { this.token_ = token; this.rows_ = rows; this.hilitedRow_ = 0; this.startRenderingRows_ = goog.now(); this.target_ = opt_target; this.rowDivs_ = []; this.redraw(); }; /** * Hide the object. */ goog.ui.AutoComplete.Renderer.prototype.dismiss = function() { if (this.target_) { goog.dom.a11y.setActiveDescendant(this.target_, null); } if (this.visible_) { this.visible_ = false; goog.style.showElement(this.element_, false); } }; /** * Show the object. */ goog.ui.AutoComplete.Renderer.prototype.show = function() { if (!this.visible_) { this.visible_ = true; goog.style.showElement(this.element_, true); } }; /** * @return {boolean} True if the object is visible. */ goog.ui.AutoComplete.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.AutoComplete.Renderer.prototype.hiliteRow = function(index) { this.hiliteNone(); this.hilitedRow_ = index; if (index >= 0 && index < this.element_.childNodes.length) { var rowDiv = this.rowDivs_[index]; 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.AutoComplete.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.AutoComplete.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.AutoComplete.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.AutoComplete.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); var id = el.id = goog.ui.IdGenerator.getInstance().getNextUniqueId(); // 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); } 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(this.dom_.getDocument(), goog.events.EventType.CLICK, this.handleDocumentClick_, 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.AutoComplete.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'; } // 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(); } // Fix bug on Firefox on Mac where scrollbars can show through a floating div this.preventMacScrollbarResurface_(this.element_); 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.AutoComplete.Renderer.prototype.reposition = function() { if (this.target_ && this.reposition_) { var topLeft = goog.style.getPageOffset(this.target_); var locationNodeSize = goog.style.getSize(this.target_); var viewSize = goog.style.getSize(goog.style.getClientViewportElement( this.target_)); var elSize = goog.style.getSize(this.element_); topLeft.y = this.topAlign_ ? topLeft.y - elSize.height : topLeft.y + locationNodeSize.height; // If past right edge of screen, align with right side of location node // Also set param to keep aligning this widget to same side for // successive renders if ((this.rightAlign_ || topLeft.x + elSize.width > viewSize.width) && this.keepAligned_ != 'LEFT') { topLeft.x = topLeft.x + locationNodeSize.width - elSize.width; this.keepAligned_ = 'RIGHT'; } else { this.keepAligned_ = 'LEFT'; } goog.style.setPageOffset(this.element_, topLeft); 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.AutoComplete.Renderer.prototype.setAutoPosition = function(auto) { this.reposition_ = auto; }; /** * Disposes of the renderer and its associated HTML. */ goog.ui.AutoComplete.Renderer.prototype.disposeInternal = function() { goog.ui.AutoComplete.Renderer.superClass_.disposeInternal.call(this); 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.dom_.getDocument(), goog.events.EventType.CLICK, this.handleDocumentClick_, 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; } delete this.parent_; }; /** * Prevents scrollbars that are below the node from resurfacing through the * node. This is a known bug in Firefox on Mac. * * @param {Node} node The node to prevent scrollbars from resurfacing through. * @private */ goog.ui.AutoComplete.Renderer.prototype.preventMacScrollbarResurface_ = function(node) { if (goog.userAgent.GECKO && goog.userAgent.MAC) { node.style.width = ''; node.style.overflow = 'visible'; node.style.width = node.offsetWidth; node.style.overflow = 'auto'; } }; /** * 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.AutoComplete.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.AutoComplete.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 var re = new RegExp('(.*?)(^|\\W+)(' + 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]); textNodes.push(match[3]); lastIndex = re.lastIndex; match = re.exec(text); } textNodes.push(text.substring(lastIndex)); // Replace the tokens with bolded text. Each set of three textNodes // (starting at index idx) includes two nodes of text before the bolded // token, then a third node (at idx + 2) 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 = 3 * i; node.nodeValue = textNodes[idx] + textNodes[idx + 1]; var boldTag = this.dom_.createElement('b'); boldTag.className = this.highlightedClassName; this.dom_.appendChild(boldTag, this.dom_.createTextNode(textNodes[idx + 2])); 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 * 3); 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.AutoComplete.Renderer.prototype.getTokenRegExp_ = function(tokenOrArray) { var token = ''; if (!tokenOrArray) { return token; } // If highlighting all tokens, join them with '|' so the regular expression // will match on any of them. if (this.highlightAllTokens_) { if (goog.isArray(tokenOrArray)) { // Remove empty or whitespace entries from the array so the joined array // will only contain valid tokens. var tokenArray = goog.array.filter(tokenOrArray, function(str) { return !goog.string.isEmptySafe(str); }); tokenArray = goog.array.map(tokenArray, 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. if (goog.isArray(tokenOrArray)) { token = tokenOrArray.length > 0 ? goog.string.regExpEscape(tokenOrArray[0]) : ''; } else { 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.AutoComplete.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.AutoComplete.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.AutoComplete.Renderer.prototype.handleClick_ = function(e) { var index = this.getRowFromEventTarget_(/** @type {Element} */ (e.target)); if (index >= 0) { this.dispatchEvent({ type: goog.ui.AutoComplete.EventType.SELECT, row: this.rows_[index].id }); } e.stopPropagation(); }; /** * Handle the mousedown event and tell the AC not to dimiss. * @param {goog.events.Event} e Browser event object. * @private */ goog.ui.AutoComplete.Renderer.prototype.handleMouseDown_ = function(e) { this.dispatchEvent(goog.ui.AutoComplete.EventType.CANCEL_DISMISS); e.stopPropagation(); e.preventDefault(); }; /** * Handles the user clicking on the document. * @param {Object} e The document click event. * @private */ goog.ui.AutoComplete.Renderer.prototype.handleDocumentClick_ = function(e) { this.dispatchEvent(goog.ui.AutoComplete.EventType.DISMISS); }; /** * 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.AutoComplete.Renderer.prototype.handleMouseOver_ = function(e) { var index = this.getRowFromEventTarget_(/** @type {Element} */ (e.target)); if (index >= 0) { if ((goog.now() - this.startRenderingRows_) < goog.ui.AutoComplete.Renderer.DELAY_BEFORE_MOUSEOVER) { return; } this.dispatchEvent({ type: goog.ui.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.AutoComplete.Renderer.CustomRenderer = function() { }; /** * Renders the autocomplete box. * @param {goog.ui.AutoComplete.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.AutoComplete.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.AutoComplete.Renderer.CustomRenderer.prototype.renderRow = function(row, token, node) { };