|  | // Licensed to the Software Freedom Conservancy (SFC) under one | 
|  | // or more contributor license agreements.  See the NOTICE file | 
|  | // distributed with this work for additional information | 
|  | // regarding copyright ownership.  The SFC licenses this file | 
|  | // to you 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 DOM manipulation and querying routines. | 
|  | */ | 
|  |  | 
|  | goog.provide('bot.dom'); | 
|  |  | 
|  | goog.require('bot'); | 
|  | goog.require('bot.color'); | 
|  | goog.require('bot.dom.core'); | 
|  | goog.require('bot.locators.xpath'); | 
|  | goog.require('bot.userAgent'); | 
|  | goog.require('goog.array'); | 
|  | goog.require('goog.dom'); | 
|  | goog.require('goog.dom.DomHelper'); | 
|  | goog.require('goog.dom.NodeType'); | 
|  | goog.require('goog.dom.TagName'); | 
|  | goog.require('goog.math'); | 
|  | goog.require('goog.math.Coordinate'); | 
|  | goog.require('goog.math.Rect'); | 
|  | goog.require('goog.string'); | 
|  | goog.require('goog.style'); | 
|  | goog.require('goog.userAgent'); | 
|  |  | 
|  |  | 
|  | /** | 
|  | * Whether Shadow DOM operations are supported by the browser. | 
|  | * @const {boolean} | 
|  | */ | 
|  | bot.dom.IS_SHADOW_DOM_ENABLED = (typeof ShadowRoot === 'function'); | 
|  |  | 
|  |  | 
|  | /** | 
|  | * Retrieves the active element for a node's owner document. | 
|  | * @param {(!Node|!Window)} nodeOrWindow The node whose owner document to get | 
|  | *     the active element for. | 
|  | * @return {?Element} The active element, if any. | 
|  | */ | 
|  | bot.dom.getActiveElement = function(nodeOrWindow) { | 
|  | var active = goog.dom.getActiveElement( | 
|  | goog.dom.getOwnerDocument(nodeOrWindow)); | 
|  | // IE has the habit of returning an empty object from | 
|  | // goog.dom.getActiveElement instead of null. | 
|  | if (goog.userAgent.IE && | 
|  | active && | 
|  | typeof active.nodeType === 'undefined') { | 
|  | return null; | 
|  | } | 
|  | return active; | 
|  | }; | 
|  |  | 
|  |  | 
|  | /** | 
|  | * @const | 
|  | */ | 
|  | bot.dom.isElement = bot.dom.core.isElement; | 
|  |  | 
|  |  | 
|  | /** | 
|  | * Returns whether an element is in an interactable state: whether it is shown | 
|  | * to the user, ignoring its opacity, and whether it is enabled. | 
|  | * | 
|  | * @param {!Element} element The element to check. | 
|  | * @return {boolean} Whether the element is interactable. | 
|  | * @see bot.dom.isShown. | 
|  | * @see bot.dom.isEnabled | 
|  | */ | 
|  | bot.dom.isInteractable = function(element) { | 
|  | return bot.dom.isShown(element, /*ignoreOpacity=*/true) && | 
|  | bot.dom.isEnabled(element) && | 
|  | !bot.dom.hasPointerEventsDisabled_(element); | 
|  | }; | 
|  |  | 
|  |  | 
|  | /** | 
|  | * @param {!Element} element Element. | 
|  | * @return {boolean} Whether element is set by the CSS pointer-events property | 
|  | *     not to be interactable. | 
|  | * @private | 
|  | */ | 
|  | bot.dom.hasPointerEventsDisabled_ = function(element) { | 
|  | if (goog.userAgent.IE || | 
|  | (goog.userAgent.GECKO && !bot.userAgent.isEngineVersion('1.9.2'))) { | 
|  | // Don't support pointer events | 
|  | return false; | 
|  | } | 
|  | return bot.dom.getEffectiveStyle(element, 'pointer-events') == 'none'; | 
|  | }; | 
|  |  | 
|  |  | 
|  | /** | 
|  | * @const | 
|  | */ | 
|  | bot.dom.isSelectable = bot.dom.core.isSelectable; | 
|  |  | 
|  |  | 
|  | /** | 
|  | * @const | 
|  | */ | 
|  | bot.dom.isSelected = bot.dom.core.isSelected; | 
|  |  | 
|  |  | 
|  | /** | 
|  | * List of the focusable fields, according to | 
|  | * http://www.w3.org/TR/html401/interact/scripts.html#adef-onfocus | 
|  | * @private {!Array.<!goog.dom.TagName>} | 
|  | * @const | 
|  | */ | 
|  | bot.dom.FOCUSABLE_FORM_FIELDS_ = [ | 
|  | goog.dom.TagName.A, | 
|  | goog.dom.TagName.AREA, | 
|  | goog.dom.TagName.BUTTON, | 
|  | goog.dom.TagName.INPUT, | 
|  | goog.dom.TagName.LABEL, | 
|  | goog.dom.TagName.SELECT, | 
|  | goog.dom.TagName.TEXTAREA | 
|  | ]; | 
|  |  | 
|  |  | 
|  | /** | 
|  | * Returns whether a node is a focusable element.  An element may receive focus | 
|  | * if it is a form field, has a non-negative tabindex, or is editable. | 
|  | * @param {!Element} element The node to test. | 
|  | * @return {boolean} Whether the node is focusable. | 
|  | */ | 
|  | bot.dom.isFocusable = function(element) { | 
|  | return goog.array.some(bot.dom.FOCUSABLE_FORM_FIELDS_, tagNameMatches) || | 
|  | (bot.dom.getAttribute(element, 'tabindex') != null && | 
|  | Number(bot.dom.getProperty(element, 'tabIndex')) >= 0) || | 
|  | bot.dom.isEditable(element); | 
|  |  | 
|  | function tagNameMatches(tagName) { | 
|  | return bot.dom.isElement(element, tagName); | 
|  | } | 
|  | }; | 
|  |  | 
|  |  | 
|  | /** | 
|  | * @const | 
|  | */ | 
|  | bot.dom.getProperty = bot.dom.core.getProperty; | 
|  |  | 
|  |  | 
|  | /** | 
|  | * @const | 
|  | */ | 
|  | bot.dom.getAttribute = bot.dom.core.getAttribute; | 
|  |  | 
|  |  | 
|  | /** | 
|  | * List of elements that support the "disabled" attribute, as defined by the | 
|  | * HTML 4.01 specification. | 
|  | * @private {!Array.<!goog.dom.TagName>} | 
|  | * @const | 
|  | * @see http://www.w3.org/TR/html401/interact/forms.html#h-17.12.1 | 
|  | */ | 
|  | bot.dom.DISABLED_ATTRIBUTE_SUPPORTED_ = [ | 
|  | goog.dom.TagName.BUTTON, | 
|  | goog.dom.TagName.INPUT, | 
|  | goog.dom.TagName.OPTGROUP, | 
|  | goog.dom.TagName.OPTION, | 
|  | goog.dom.TagName.SELECT, | 
|  | goog.dom.TagName.TEXTAREA | 
|  | ]; | 
|  |  | 
|  |  | 
|  | /** | 
|  | * Determines if an element is enabled. An element is considered enabled if it | 
|  | * does not support the "disabled" attribute, or if it is not disabled. | 
|  | * @param {!Element} el The element to test. | 
|  | * @return {boolean} Whether the element is enabled. | 
|  | */ | 
|  | bot.dom.isEnabled = function(el) { | 
|  | var isSupported = goog.array.some( | 
|  | bot.dom.DISABLED_ATTRIBUTE_SUPPORTED_, | 
|  | function(tagName) { return bot.dom.isElement(el, tagName); }); | 
|  | if (!isSupported) { | 
|  | return true; | 
|  | } | 
|  |  | 
|  | if (bot.dom.getProperty(el, 'disabled')) { | 
|  | return false; | 
|  | } | 
|  |  | 
|  | // The element is not explicitly disabled, but if it is an OPTION or OPTGROUP, | 
|  | // we must test if it inherits its state from a parent. | 
|  | if (el.parentNode && | 
|  | el.parentNode.nodeType == goog.dom.NodeType.ELEMENT && | 
|  | bot.dom.isElement(el, goog.dom.TagName.OPTGROUP) || | 
|  | bot.dom.isElement(el, goog.dom.TagName.OPTION)) { | 
|  | return bot.dom.isEnabled(/**@type{!Element}*/ (el.parentNode)); | 
|  | } | 
|  |  | 
|  | // Is there an ancestor of the current element that is a disabled fieldset | 
|  | // and whose child is also an ancestor-or-self of the current element but is | 
|  | // not the first legend child of the fieldset. If so then the element is | 
|  | // disabled. | 
|  | return !goog.dom.getAncestor(el, function(e) { | 
|  | var parent = e.parentNode; | 
|  |  | 
|  | if (parent && | 
|  | bot.dom.isElement(parent, goog.dom.TagName.FIELDSET) && | 
|  | bot.dom.getProperty(/** @type {!Element} */ (parent), 'disabled')) { | 
|  | if (!bot.dom.isElement(e, goog.dom.TagName.LEGEND)) { | 
|  | return true; | 
|  | } | 
|  |  | 
|  | var sibling = e; | 
|  | // Are there any previous legend siblings? If so then we are not the | 
|  | // first and the element is disabled | 
|  | while (sibling = goog.dom.getPreviousElementSibling(sibling)) { | 
|  | if (bot.dom.isElement(sibling, goog.dom.TagName.LEGEND)) { | 
|  | return true; | 
|  | } | 
|  | } | 
|  | } | 
|  | return false; | 
|  | }, true); | 
|  | }; | 
|  |  | 
|  |  | 
|  | /** | 
|  | * List of input types that create text fields. | 
|  | * @private {!Array.<string>} | 
|  | * @const | 
|  | * @see http://www.whatwg.org/specs/web-apps/current-work/multipage/the-input-element.html#attr-input-type | 
|  | */ | 
|  | bot.dom.TEXTUAL_INPUT_TYPES_ = [ | 
|  | 'text', | 
|  | 'search', | 
|  | 'tel', | 
|  | 'url', | 
|  | 'email', | 
|  | 'password', | 
|  | 'number' | 
|  | ]; | 
|  |  | 
|  |  | 
|  | /** | 
|  | * TODO: Add support for designMode elements. | 
|  | * | 
|  | * @param {!Element} element The element to check. | 
|  | * @return {boolean} Whether the element accepts user-typed text. | 
|  | */ | 
|  | bot.dom.isTextual = function(element) { | 
|  | if (bot.dom.isElement(element, goog.dom.TagName.TEXTAREA)) { | 
|  | return true; | 
|  | } | 
|  |  | 
|  | if (bot.dom.isElement(element, goog.dom.TagName.INPUT)) { | 
|  | var type = element.type.toLowerCase(); | 
|  | return goog.array.contains(bot.dom.TEXTUAL_INPUT_TYPES_, type); | 
|  | } | 
|  |  | 
|  | if (bot.dom.isContentEditable(element)) { | 
|  | return true; | 
|  | } | 
|  |  | 
|  | return false; | 
|  | }; | 
|  |  | 
|  |  | 
|  | /** | 
|  | * @param {!Element} element The element to check. | 
|  | * @return {boolean} Whether the element is a file input. | 
|  | */ | 
|  | bot.dom.isFileInput = function(element) { | 
|  | if (bot.dom.isElement(element, goog.dom.TagName.INPUT)) { | 
|  | var type = element.type.toLowerCase(); | 
|  | return type == 'file'; | 
|  | } | 
|  |  | 
|  | return false; | 
|  | }; | 
|  |  | 
|  |  | 
|  | /** | 
|  | * @param {!Element} element The element to check. | 
|  | * @param {string} inputType The type of input to check. | 
|  | * @return {boolean} Whether the element is an input with specified type. | 
|  | */ | 
|  | bot.dom.isInputType = function(element, inputType) { | 
|  | if (bot.dom.isElement(element, goog.dom.TagName.INPUT)) { | 
|  | var type = element.type.toLowerCase(); | 
|  | return type == inputType; | 
|  | } | 
|  |  | 
|  | return false; | 
|  | }; | 
|  |  | 
|  |  | 
|  | /** | 
|  | * @param {!Element} element The element to check. | 
|  | * @return {boolean} Whether the element is contentEditable. | 
|  | */ | 
|  | bot.dom.isContentEditable = function(element) { | 
|  | // Check if browser supports contentEditable. | 
|  | if (!goog.isDef(element['contentEditable'])) { | 
|  | return false; | 
|  | } | 
|  |  | 
|  | // Checking the element's isContentEditable property is preferred except for | 
|  | // IE where that property is not reliable on IE versions 7, 8, and 9. | 
|  | if (!goog.userAgent.IE && goog.isDef(element['isContentEditable'])) { | 
|  | return element.isContentEditable; | 
|  | } | 
|  |  | 
|  | // For IE and for browsers where contentEditable is supported but | 
|  | // isContentEditable is not, traverse up the ancestors: | 
|  | function legacyIsContentEditable(e) { | 
|  | if (e.contentEditable == 'inherit') { | 
|  | var parent = bot.dom.getParentElement(e); | 
|  | return parent ? legacyIsContentEditable(parent) : false; | 
|  | } else { | 
|  | return e.contentEditable == 'true'; | 
|  | } | 
|  | } | 
|  | return legacyIsContentEditable(element); | 
|  | }; | 
|  |  | 
|  |  | 
|  | /** | 
|  | * TODO: Merge isTextual into this function and move to bot.dom. | 
|  | * For Puppet, requires adding support to getVisibleText for grabbing | 
|  | * text from all textual elements. | 
|  | * | 
|  | * Whether the element may contain text the user can edit. | 
|  | * | 
|  | * @param {!Element} element The element to check. | 
|  | * @return {boolean} Whether the element accepts user-typed text. | 
|  | */ | 
|  | bot.dom.isEditable = function(element) { | 
|  | return (bot.dom.isTextual(element) || | 
|  | bot.dom.isFileInput(element) || | 
|  | bot.dom.isInputType(element, 'range') || | 
|  | bot.dom.isInputType(element, 'date') || | 
|  | bot.dom.isInputType(element, 'month') || | 
|  | bot.dom.isInputType(element, 'week') || | 
|  | bot.dom.isInputType(element, 'time') || | 
|  | bot.dom.isInputType(element, 'datetime-local') || | 
|  | bot.dom.isInputType(element, 'color')) && | 
|  | !bot.dom.getProperty(element, 'readOnly'); | 
|  | }; | 
|  |  | 
|  |  | 
|  | /** | 
|  | * Returns the parent element of the given node, or null. This is required | 
|  | * because the parent node may not be another element. | 
|  | * | 
|  | * @param {!Node} node The node who's parent is desired. | 
|  | * @return {Element} The parent element, if available, null otherwise. | 
|  | */ | 
|  | bot.dom.getParentElement = function(node) { | 
|  | var elem = node.parentNode; | 
|  |  | 
|  | while (elem && | 
|  | elem.nodeType != goog.dom.NodeType.ELEMENT && | 
|  | elem.nodeType != goog.dom.NodeType.DOCUMENT && | 
|  | elem.nodeType != goog.dom.NodeType.DOCUMENT_FRAGMENT) { | 
|  | elem = elem.parentNode; | 
|  | } | 
|  | return /** @type {Element} */ (bot.dom.isElement(elem) ? elem : null); | 
|  | }; | 
|  |  | 
|  |  | 
|  | /** | 
|  | * Retrieves an explicitly-set, inline style value of an element. This returns | 
|  | * '' if there isn't a style attribute on the element or if this style property | 
|  | * has not been explicitly set in script. | 
|  | * | 
|  | * @param {!Element} elem Element to get the style value from. | 
|  | * @param {string} styleName Name of the style property in selector-case. | 
|  | * @return {string} The value of the style property. | 
|  | */ | 
|  | bot.dom.getInlineStyle = function(elem, styleName) { | 
|  | return goog.style.getStyle(elem, styleName); | 
|  | }; | 
|  |  | 
|  |  | 
|  | /** | 
|  | * Retrieves the implicitly-set, effective style of an element, or null if it is | 
|  | * unknown. It returns the computed style where available; otherwise it looks | 
|  | * up the DOM tree for the first style value not equal to 'inherit,' using the | 
|  | * IE currentStyle of each node if available, and otherwise the inline style. | 
|  | * Since the computed, current, and inline styles can be different, the return | 
|  | * value of this function is not always consistent across browsers. See: | 
|  | * http://code.google.com/p/doctype/wiki/ArticleComputedStyleVsCascadedStyle | 
|  | * | 
|  | * @param {!Element} elem Element to get the style value from. | 
|  | * @param {string} propertyName Name of the CSS property. | 
|  | * @return {?string} The value of the style property, or null. | 
|  | */ | 
|  | bot.dom.getEffectiveStyle = function(elem, propertyName) { | 
|  | var styleName = goog.string.toCamelCase(propertyName); | 
|  | if (styleName == 'float' || | 
|  | styleName == 'cssFloat' || | 
|  | styleName == 'styleFloat') { | 
|  | styleName = bot.userAgent.IE_DOC_PRE9 ? 'styleFloat' : 'cssFloat'; | 
|  | } | 
|  | var style = goog.style.getComputedStyle(elem, styleName) || | 
|  | bot.dom.getCascadedStyle_(elem, styleName); | 
|  | if (style === null) { | 
|  | return null; | 
|  | } | 
|  | return bot.color.standardizeColor(styleName, style); | 
|  | }; | 
|  |  | 
|  |  | 
|  | /** | 
|  | * Looks up the DOM tree for the first style value not equal to 'inherit,' using | 
|  | * the currentStyle of each node if available, and otherwise the inline style. | 
|  | * | 
|  | * @param {!Element} elem Element to get the style value from. | 
|  | * @param {string} styleName CSS style property in camelCase. | 
|  | * @return {?string} The value of the style property, or null. | 
|  | * @private | 
|  | */ | 
|  | bot.dom.getCascadedStyle_ = function(elem, styleName) { | 
|  | var style = elem.currentStyle || elem.style; | 
|  | var value = style[styleName]; | 
|  | if (!goog.isDef(value) && goog.isFunction(style.getPropertyValue)) { | 
|  | value = style.getPropertyValue(styleName); | 
|  | } | 
|  |  | 
|  | if (value != 'inherit') { | 
|  | return goog.isDef(value) ? value : null; | 
|  | } | 
|  | var parent = bot.dom.getParentElement(elem); | 
|  | return parent ? bot.dom.getCascadedStyle_(parent, styleName) : null; | 
|  | }; | 
|  |  | 
|  |  | 
|  | /** | 
|  | * Extracted code from bot.dom.isShown. | 
|  | * | 
|  | * @param {!Element} elem The element to consider. | 
|  | * @param {boolean} ignoreOpacity Whether to ignore the element's opacity | 
|  | *     when determining whether it is shown. | 
|  | * @param {function(!Element):boolean} parentsDisplayedFn a function that's used | 
|  | *     to tell if the chain of ancestors are all shown. | 
|  | * @return {boolean} Whether or not the element is visible. | 
|  | * @private | 
|  | */ | 
|  | bot.dom.isShown_ = function(elem, ignoreOpacity, parentsDisplayedFn) { | 
|  | if (!bot.dom.isElement(elem)) { | 
|  | throw new Error('Argument to isShown must be of type Element'); | 
|  | } | 
|  |  | 
|  | // By convention, BODY element is always shown: BODY represents the document | 
|  | // and even if there's nothing rendered in there, user can always see there's | 
|  | // the document. | 
|  | if (bot.dom.isElement(elem, goog.dom.TagName.BODY)) { | 
|  | return true; | 
|  | } | 
|  |  | 
|  | // Option or optgroup is shown iff enclosing select is shown (ignoring the | 
|  | // select's opacity). | 
|  | if (bot.dom.isElement(elem, goog.dom.TagName.OPTION) || | 
|  | bot.dom.isElement(elem, goog.dom.TagName.OPTGROUP)) { | 
|  | var select = /**@type {Element}*/ (goog.dom.getAncestor(elem, function(e) { | 
|  | return bot.dom.isElement(e, goog.dom.TagName.SELECT); | 
|  | })); | 
|  | return !!select && bot.dom.isShown_(select, true, parentsDisplayedFn); | 
|  | } | 
|  |  | 
|  | // Image map elements are shown if image that uses it is shown, and | 
|  | // the area of the element is positive. | 
|  | var imageMap = bot.dom.maybeFindImageMap_(elem); | 
|  | if (imageMap) { | 
|  | return !!imageMap.image && | 
|  | imageMap.rect.width > 0 && imageMap.rect.height > 0 && | 
|  | bot.dom.isShown_( | 
|  | imageMap.image, ignoreOpacity, parentsDisplayedFn); | 
|  | } | 
|  |  | 
|  | // Any hidden input is not shown. | 
|  | if (bot.dom.isElement(elem, goog.dom.TagName.INPUT) && | 
|  | elem.type.toLowerCase() == 'hidden') { | 
|  | return false; | 
|  | } | 
|  |  | 
|  | // Any NOSCRIPT element is not shown. | 
|  | if (bot.dom.isElement(elem, goog.dom.TagName.NOSCRIPT)) { | 
|  | return false; | 
|  | } | 
|  |  | 
|  | // Any element with hidden/collapsed visibility is not shown. | 
|  | var visibility = bot.dom.getEffectiveStyle(elem, 'visibility'); | 
|  | if (visibility == 'collapse' || visibility == 'hidden') { | 
|  | return false; | 
|  | } | 
|  |  | 
|  | if (!parentsDisplayedFn(elem)) { | 
|  | return false; | 
|  | } | 
|  |  | 
|  | // Any transparent element is not shown. | 
|  | if (!ignoreOpacity && bot.dom.getOpacity(elem) == 0) { | 
|  | return false; | 
|  | } | 
|  |  | 
|  | // Any element without positive size dimensions is not shown. | 
|  | function positiveSize(e) { | 
|  | var rect = bot.dom.getClientRect(e); | 
|  | if (rect.height > 0 && rect.width > 0) { | 
|  | return true; | 
|  | } | 
|  | // A vertical or horizontal SVG Path element will report zero width or | 
|  | // height but is "shown" if it has a positive stroke-width. | 
|  | if (bot.dom.isElement(e, 'PATH') && (rect.height > 0 || rect.width > 0)) { | 
|  | var strokeWidth = bot.dom.getEffectiveStyle(e, 'stroke-width'); | 
|  | return !!strokeWidth && (parseInt(strokeWidth, 10) > 0); | 
|  | } | 
|  | // Zero-sized elements should still be considered to have positive size | 
|  | // if they have a child element or text node with positive size, unless | 
|  | // the element has an 'overflow' style of 'hidden'. | 
|  | return bot.dom.getEffectiveStyle(e, 'overflow') != 'hidden' && | 
|  | goog.array.some(e.childNodes, function(n) { | 
|  | return n.nodeType == goog.dom.NodeType.TEXT || | 
|  | (bot.dom.isElement(n) && positiveSize(n)); | 
|  | }); | 
|  | } | 
|  | if (!positiveSize(elem)) { | 
|  | return false; | 
|  | } | 
|  |  | 
|  | // Elements that are hidden by overflow are not shown. | 
|  | function hiddenByOverflow(e) { | 
|  | return bot.dom.getOverflowState(e) == bot.dom.OverflowState.HIDDEN && | 
|  | goog.array.every(e.childNodes, function(n) { | 
|  | return !bot.dom.isElement(n) || hiddenByOverflow(n) || | 
|  | !positiveSize(n); | 
|  | }); | 
|  | } | 
|  | return !hiddenByOverflow(elem); | 
|  | }; | 
|  |  | 
|  |  | 
|  | /** | 
|  | * Determines whether an element is what a user would call "shown". This means | 
|  | * that the element is shown in the viewport of the browser, and only has | 
|  | * height and width greater than 0px, and that its visibility is not "hidden" | 
|  | * and its display property is not "none". | 
|  | * Options and Optgroup elements are treated as special cases: they are | 
|  | * considered shown iff they have a enclosing select element that is shown. | 
|  | * | 
|  | * Elements in Shadow DOMs with younger shadow roots are not visible, and | 
|  | * elements distributed into shadow DOMs check the visibility of the | 
|  | * ancestors in the Composed DOM, rather than their ancestors in the logical | 
|  | * DOM. | 
|  | * | 
|  | * @param {!Element} elem The element to consider. | 
|  | * @param {boolean=} opt_ignoreOpacity Whether to ignore the element's opacity | 
|  | *     when determining whether it is shown; defaults to false. | 
|  | * @return {boolean} Whether or not the element is visible. | 
|  | */ | 
|  | bot.dom.isShown = function(elem, opt_ignoreOpacity) { | 
|  | /** | 
|  | * Determines whether an element or its parents have `display: none` set | 
|  | * @param {!Node} e the element | 
|  | * @return {!boolean} | 
|  | */ | 
|  | function displayed(e) { | 
|  | if (bot.dom.isElement(e)) { | 
|  | var elem = /** @type {!Element} */ (e); | 
|  | if (bot.dom.getEffectiveStyle(elem, 'display') == 'none') { | 
|  | return false; | 
|  | } | 
|  | } | 
|  |  | 
|  | var parent = bot.dom.getParentNodeInComposedDom(e); | 
|  |  | 
|  | if (bot.dom.IS_SHADOW_DOM_ENABLED && (parent instanceof ShadowRoot)) { | 
|  | if (parent.host.shadowRoot !== parent) { | 
|  | // There is a younger shadow root, which will take precedence over | 
|  | // the shadow this element is in, thus this element won't be | 
|  | // displayed. | 
|  | return false; | 
|  | } else { | 
|  | parent = parent.host; | 
|  | } | 
|  | } | 
|  |  | 
|  | if (parent && (parent.nodeType == goog.dom.NodeType.DOCUMENT || | 
|  | parent.nodeType == goog.dom.NodeType.DOCUMENT_FRAGMENT)) { | 
|  | return true; | 
|  | } | 
|  |  | 
|  | // Child of DETAILS element is not shown unless the DETAILS element is open | 
|  | // or the child is a SUMMARY element. | 
|  | if (parent && bot.dom.isElement(parent, goog.dom.TagName.DETAILS) && | 
|  | !parent.open && !bot.dom.isElement(e, goog.dom.TagName.SUMMARY)) { | 
|  | return false; | 
|  | } | 
|  |  | 
|  | return !!parent && displayed(parent); | 
|  | } | 
|  |  | 
|  | return bot.dom.isShown_(elem, !!opt_ignoreOpacity, displayed); | 
|  | }; | 
|  |  | 
|  |  | 
|  | /** | 
|  | * The kind of overflow area in which an element may be located. NONE if it does | 
|  | * not overflow any ancestor element; HIDDEN if it overflows and cannot be | 
|  | * scrolled into view; SCROLL if it overflows but can be scrolled into view. | 
|  | * | 
|  | * @enum {string} | 
|  | */ | 
|  | bot.dom.OverflowState = { | 
|  | NONE: 'none', | 
|  | HIDDEN: 'hidden', | 
|  | SCROLL: 'scroll' | 
|  | }; | 
|  |  | 
|  |  | 
|  | /** | 
|  | * Returns the overflow state of the given element. | 
|  | * | 
|  | * If an optional coordinate or rectangle region is provided, returns the | 
|  | * overflow state of that region relative to the element. A coordinate is | 
|  | * treated as a 1x1 rectangle whose top-left corner is the coordinate. | 
|  | * | 
|  | * @param {!Element} elem Element. | 
|  | * @param {!(goog.math.Coordinate|goog.math.Rect)=} opt_region | 
|  | *     Coordinate or rectangle relative to the top-left corner of the element. | 
|  | * @return {bot.dom.OverflowState} Overflow state of the element. | 
|  | */ | 
|  | bot.dom.getOverflowState = function(elem, opt_region) { | 
|  | var region = bot.dom.getClientRegion(elem, opt_region); | 
|  | var ownerDoc = goog.dom.getOwnerDocument(elem); | 
|  | var htmlElem = ownerDoc.documentElement; | 
|  | var bodyElem = ownerDoc.body; | 
|  | var htmlOverflowStyle = bot.dom.getEffectiveStyle(htmlElem, 'overflow'); | 
|  | var treatAsFixedPosition; | 
|  |  | 
|  | // Return the closest ancestor that the given element may overflow. | 
|  | function getOverflowParent(e) { | 
|  | var position = bot.dom.getEffectiveStyle(e, 'position'); | 
|  | if (position == 'fixed') { | 
|  | treatAsFixedPosition = true; | 
|  | // Fixed-position element may only overflow the viewport. | 
|  | return e == htmlElem ? null : htmlElem; | 
|  | } else { | 
|  | var parent = bot.dom.getParentElement(e); | 
|  | while (parent && !canBeOverflowed(parent)) { | 
|  | parent = bot.dom.getParentElement(parent); | 
|  | } | 
|  | return parent; | 
|  | } | 
|  |  | 
|  | function canBeOverflowed(container) { | 
|  | // The HTML element can always be overflowed. | 
|  | if (container == htmlElem) { | 
|  | return true; | 
|  | } | 
|  | // An element cannot overflow an element with an inline or contents display style. | 
|  | var containerDisplay = /** @type {string} */ ( | 
|  | bot.dom.getEffectiveStyle(container, 'display')); | 
|  | if (goog.string.startsWith(containerDisplay, 'inline') || | 
|  | (containerDisplay == 'contents')) { | 
|  | return false; | 
|  | } | 
|  | // An absolute-positioned element cannot overflow a static-positioned one. | 
|  | if (position == 'absolute' && | 
|  | bot.dom.getEffectiveStyle(container, 'position') == 'static') { | 
|  | return false; | 
|  | } | 
|  | return true; | 
|  | } | 
|  | } | 
|  |  | 
|  | // Return the x and y overflow styles for the given element. | 
|  | function getOverflowStyles(e) { | 
|  | // When the <html> element has an overflow style of 'visible', it assumes | 
|  | // the overflow style of the body, and the body is really overflow:visible. | 
|  | var overflowElem = e; | 
|  | if (htmlOverflowStyle == 'visible') { | 
|  | // Note: bodyElem will be null/undefined in SVG documents. | 
|  | if (e == htmlElem && bodyElem) { | 
|  | overflowElem = bodyElem; | 
|  | } else if (e == bodyElem) { | 
|  | return {x: 'visible', y: 'visible'}; | 
|  | } | 
|  | } | 
|  | var overflow = { | 
|  | x: bot.dom.getEffectiveStyle(overflowElem, 'overflow-x'), | 
|  | y: bot.dom.getEffectiveStyle(overflowElem, 'overflow-y') | 
|  | }; | 
|  | // The <html> element cannot have a genuine 'visible' overflow style, | 
|  | // because the viewport can't expand; 'visible' is really 'auto'. | 
|  | if (e == htmlElem) { | 
|  | overflow.x = overflow.x == 'visible' ? 'auto' : overflow.x; | 
|  | overflow.y = overflow.y == 'visible' ? 'auto' : overflow.y; | 
|  | } | 
|  | return overflow; | 
|  | } | 
|  |  | 
|  | // Returns the scroll offset of the given element. | 
|  | function getScroll(e) { | 
|  | if (e == htmlElem) { | 
|  | return new goog.dom.DomHelper(ownerDoc).getDocumentScroll(); | 
|  | } else { | 
|  | return new goog.math.Coordinate(e.scrollLeft, e.scrollTop); | 
|  | } | 
|  | } | 
|  |  | 
|  | // Check if the element overflows any ancestor element. | 
|  | for (var container = getOverflowParent(elem); | 
|  | !!container; | 
|  | container = getOverflowParent(container)) { | 
|  | var containerOverflow = getOverflowStyles(container); | 
|  |  | 
|  | // If the container has overflow:visible, the element cannot overflow it. | 
|  | if (containerOverflow.x == 'visible' && containerOverflow.y == 'visible') { | 
|  | continue; | 
|  | } | 
|  |  | 
|  | var containerRect = bot.dom.getClientRect(container); | 
|  |  | 
|  | // Zero-sized containers without overflow:visible hide all descendants. | 
|  | if (containerRect.width == 0 || containerRect.height == 0) { | 
|  | return bot.dom.OverflowState.HIDDEN; | 
|  | } | 
|  |  | 
|  | // Check "underflow": if an element is to the left or above the container | 
|  | var underflowsX = region.right < containerRect.left; | 
|  | var underflowsY = region.bottom < containerRect.top; | 
|  | if ((underflowsX && containerOverflow.x == 'hidden') || | 
|  | (underflowsY && containerOverflow.y == 'hidden')) { | 
|  | return bot.dom.OverflowState.HIDDEN; | 
|  | } else if ((underflowsX && containerOverflow.x != 'visible') || | 
|  | (underflowsY && containerOverflow.y != 'visible')) { | 
|  | // When the element is positioned to the left or above a container, we | 
|  | // have to distinguish between the element being completely outside the | 
|  | // container and merely scrolled out of view within the container. | 
|  | var containerScroll = getScroll(container); | 
|  | var unscrollableX = region.right < containerRect.left - containerScroll.x; | 
|  | var unscrollableY = region.bottom < containerRect.top - containerScroll.y; | 
|  | if ((unscrollableX && containerOverflow.x != 'visible') || | 
|  | (unscrollableY && containerOverflow.x != 'visible')) { | 
|  | return bot.dom.OverflowState.HIDDEN; | 
|  | } | 
|  | var containerState = bot.dom.getOverflowState(container); | 
|  | return containerState == bot.dom.OverflowState.HIDDEN ? | 
|  | bot.dom.OverflowState.HIDDEN : bot.dom.OverflowState.SCROLL; | 
|  | } | 
|  |  | 
|  | // Check "overflow": if an element is to the right or below a container | 
|  | var overflowsX = region.left >= containerRect.left + containerRect.width; | 
|  | var overflowsY = region.top >= containerRect.top + containerRect.height; | 
|  | if ((overflowsX && containerOverflow.x == 'hidden') || | 
|  | (overflowsY && containerOverflow.y == 'hidden')) { | 
|  | return bot.dom.OverflowState.HIDDEN; | 
|  | } else if ((overflowsX && containerOverflow.x != 'visible') || | 
|  | (overflowsY && containerOverflow.y != 'visible')) { | 
|  | // If the element has fixed position and falls outside the scrollable area | 
|  | // of the document, then it is hidden. | 
|  | if (treatAsFixedPosition) { | 
|  | var docScroll = getScroll(container); | 
|  | if ((region.left >= htmlElem.scrollWidth - docScroll.x) || | 
|  | (region.right >= htmlElem.scrollHeight - docScroll.y)) { | 
|  | return bot.dom.OverflowState.HIDDEN; | 
|  | } | 
|  | } | 
|  | // If the element can be scrolled into view of the parent, it has a scroll | 
|  | // state; unless the parent itself is entirely hidden by overflow, in | 
|  | // which it is also hidden by overflow. | 
|  | var containerState = bot.dom.getOverflowState(container); | 
|  | return containerState == bot.dom.OverflowState.HIDDEN ? | 
|  | bot.dom.OverflowState.HIDDEN : bot.dom.OverflowState.SCROLL; | 
|  | } | 
|  | } | 
|  |  | 
|  | // Does not overflow any ancestor. | 
|  | return bot.dom.OverflowState.NONE; | 
|  | }; | 
|  |  | 
|  |  | 
|  | /** | 
|  | * A regular expression to match the CSS transform matrix syntax. | 
|  | * @private {!RegExp} | 
|  | * @const | 
|  | */ | 
|  | bot.dom.CSS_TRANSFORM_MATRIX_REGEX_ = | 
|  | new RegExp('matrix\\(([\\d\\.\\-]+), ([\\d\\.\\-]+), ' + | 
|  | '([\\d\\.\\-]+), ([\\d\\.\\-]+), ' + | 
|  | '([\\d\\.\\-]+)(?:px)?, ([\\d\\.\\-]+)(?:px)?\\)'); | 
|  |  | 
|  |  | 
|  | /** | 
|  | * Gets the client rectangle of the DOM element. It often returns the same value | 
|  | * as Element.getBoundingClientRect, but is "fixed" for various scenarios: | 
|  | * 1. Like goog.style.getClientPosition, it adjusts for the inset border in IE. | 
|  | * 2. Gets a rect for <map>'s and <area>'s relative to the image using them. | 
|  | * 3. Gets a rect for SVG elements representing their true bounding box. | 
|  | * 4. Defines the client rect of the <html> element to be the window viewport. | 
|  | * | 
|  | * @param {!Element} elem The element to use. | 
|  | * @return {!goog.math.Rect} The interaction box of the element. | 
|  | */ | 
|  | bot.dom.getClientRect = function(elem) { | 
|  | var imageMap = bot.dom.maybeFindImageMap_(elem); | 
|  | if (imageMap) { | 
|  | return imageMap.rect; | 
|  | } else if (bot.dom.isElement(elem, goog.dom.TagName.HTML)) { | 
|  | // Define the client rect of the <html> element to be the viewport. | 
|  | var doc = goog.dom.getOwnerDocument(elem); | 
|  | var viewportSize = goog.dom.getViewportSize(goog.dom.getWindow(doc)); | 
|  | return new goog.math.Rect(0, 0, viewportSize.width, viewportSize.height); | 
|  | } else { | 
|  | var nativeRect; | 
|  | try { | 
|  | // TODO: in IE and Firefox, getBoundingClientRect includes stroke width, | 
|  | // but getBBox does not. | 
|  | nativeRect = elem.getBoundingClientRect(); | 
|  | } catch (e) { | 
|  | // On IE < 9, calling getBoundingClientRect on an orphan element raises | 
|  | // an "Unspecified Error". All other browsers return zeros. | 
|  | return new goog.math.Rect(0, 0, 0, 0); | 
|  | } | 
|  |  | 
|  | var rect = new goog.math.Rect(nativeRect.left, nativeRect.top, | 
|  | nativeRect.right - nativeRect.left, nativeRect.bottom - nativeRect.top); | 
|  |  | 
|  | // In IE, the element can additionally be offset by a border around the | 
|  | // documentElement or body element that we have to subtract. | 
|  | if (goog.userAgent.IE && elem.ownerDocument.body) { | 
|  | var doc = goog.dom.getOwnerDocument(elem); | 
|  | rect.left -= doc.documentElement.clientLeft + doc.body.clientLeft; | 
|  | rect.top -= doc.documentElement.clientTop + doc.body.clientTop; | 
|  | } | 
|  |  | 
|  | return rect; | 
|  | } | 
|  | }; | 
|  |  | 
|  |  | 
|  | /** | 
|  | * If given a <map> or <area> element, finds the corresponding image and client | 
|  | * rectangle of the element; otherwise returns null. The return value is an | 
|  | * object with 'image' and 'rect' properties. When no image uses the given | 
|  | * element, the returned rectangle is present but has zero size. | 
|  | * | 
|  | * @param {!Element} elem Element to test. | 
|  | * @return {?{image: Element, rect: !goog.math.Rect}} Image and rectangle. | 
|  | * @private | 
|  | */ | 
|  | bot.dom.maybeFindImageMap_ = function(elem) { | 
|  | // If not a <map> or <area>, return null indicating so. | 
|  | var isMap = bot.dom.isElement(elem, goog.dom.TagName.MAP); | 
|  | if (!isMap && !bot.dom.isElement(elem, goog.dom.TagName.AREA)) { | 
|  | return null; | 
|  | } | 
|  |  | 
|  | // Get the <map> associated with this element, or null if none. | 
|  | var map = isMap ? elem : | 
|  | (bot.dom.isElement(elem.parentNode, goog.dom.TagName.MAP) ? | 
|  | elem.parentNode : null); | 
|  |  | 
|  | var image = null, rect = null; | 
|  | if (map && map.name) { | 
|  | var mapDoc = goog.dom.getOwnerDocument(map); | 
|  |  | 
|  | // The "//*" XPath syntax can confuse the closure compiler, so we use | 
|  | // the "/descendant::*" syntax instead. | 
|  | // TODO: Try to find a reproducible case for the compiler bug. | 
|  | // TODO: Restrict to applet, img, input:image, and object nodes. | 
|  | var imageXpath = '/descendant::*[@usemap = "#' + map.name + '"]'; | 
|  |  | 
|  | // TODO: Break dependency of bot.locators on bot.dom, | 
|  | // so bot.locators.findElement can be called here instead. | 
|  | image = bot.locators.xpath.single(imageXpath, mapDoc); | 
|  |  | 
|  | if (image) { | 
|  | rect = bot.dom.getClientRect(image); | 
|  | if (!isMap && elem.shape.toLowerCase() != 'default') { | 
|  | // Shift and crop the relative area rectangle to the map. | 
|  | var relRect = bot.dom.getAreaRelativeRect_(elem); | 
|  | var relX = Math.min(Math.max(relRect.left, 0), rect.width); | 
|  | var relY = Math.min(Math.max(relRect.top, 0), rect.height); | 
|  | var w = Math.min(relRect.width, rect.width - relX); | 
|  | var h = Math.min(relRect.height, rect.height - relY); | 
|  | rect = new goog.math.Rect(relX + rect.left, relY + rect.top, w, h); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | return {image: image, rect: rect || new goog.math.Rect(0, 0, 0, 0)}; | 
|  | }; | 
|  |  | 
|  |  | 
|  | /** | 
|  | * Returns the bounding box around an <area> element relative to its enclosing | 
|  | * <map>. Does not apply to <area> elements with shape=='default'. | 
|  | * | 
|  | * @param {!Element} area Area element. | 
|  | * @return {!goog.math.Rect} Bounding box of the area element. | 
|  | * @private | 
|  | */ | 
|  | bot.dom.getAreaRelativeRect_ = function(area) { | 
|  | var shape = area.shape.toLowerCase(); | 
|  | var coords = area.coords.split(','); | 
|  | if (shape == 'rect' && coords.length == 4) { | 
|  | var x = coords[0], y = coords[1]; | 
|  | return new goog.math.Rect(x, y, coords[2] - x, coords[3] - y); | 
|  | } else if (shape == 'circle' && coords.length == 3) { | 
|  | var centerX = coords[0], centerY = coords[1], radius = coords[2]; | 
|  | return new goog.math.Rect(centerX - radius, centerY - radius, | 
|  | 2 * radius, 2 * radius); | 
|  | } else if (shape == 'poly' && coords.length > 2) { | 
|  | var minX = coords[0], minY = coords[1], maxX = minX, maxY = minY; | 
|  | for (var i = 2; i + 1 < coords.length; i += 2) { | 
|  | minX = Math.min(minX, coords[i]); | 
|  | maxX = Math.max(maxX, coords[i]); | 
|  | minY = Math.min(minY, coords[i + 1]); | 
|  | maxY = Math.max(maxY, coords[i + 1]); | 
|  | } | 
|  | return new goog.math.Rect(minX, minY, maxX - minX, maxY - minY); | 
|  | } | 
|  | return new goog.math.Rect(0, 0, 0, 0); | 
|  | }; | 
|  |  | 
|  |  | 
|  | /** | 
|  | * Gets the element's client rectangle as a box, optionally clipped to the | 
|  | * given coordinate or rectangle relative to the client's position. A coordinate | 
|  | * is treated as a 1x1 rectangle whose top-left corner is the coordinate. | 
|  | * | 
|  | * @param {!Element} elem The element. | 
|  | * @param {!(goog.math.Coordinate|goog.math.Rect)=} opt_region | 
|  | *     Coordinate or rectangle relative to the top-left corner of the element. | 
|  | * @return {!goog.math.Box} The client region box. | 
|  | */ | 
|  | bot.dom.getClientRegion = function(elem, opt_region) { | 
|  | var region = bot.dom.getClientRect(elem).toBox(); | 
|  |  | 
|  | if (opt_region) { | 
|  | var rect = opt_region instanceof goog.math.Rect ? opt_region : | 
|  | new goog.math.Rect(opt_region.x, opt_region.y, 1, 1); | 
|  | region.left = goog.math.clamp( | 
|  | region.left + rect.left, region.left, region.right); | 
|  | region.top = goog.math.clamp( | 
|  | region.top + rect.top, region.top, region.bottom); | 
|  | region.right = goog.math.clamp( | 
|  | region.left + rect.width, region.left, region.right); | 
|  | region.bottom = goog.math.clamp( | 
|  | region.top + rect.height, region.top, region.bottom); | 
|  | } | 
|  |  | 
|  | return region; | 
|  | }; | 
|  |  | 
|  |  | 
|  | /** | 
|  | * Trims leading and trailing whitespace from strings, leaving non-breaking | 
|  | * space characters in place. | 
|  | * | 
|  | * @param {string} str The string to trim. | 
|  | * @return {string} str without any leading or trailing whitespace characters | 
|  | *     except non-breaking spaces. | 
|  | * @private | 
|  | */ | 
|  | bot.dom.trimExcludingNonBreakingSpaceCharacters_ = function(str) { | 
|  | return str.replace(/^[^\S\xa0]+|[^\S\xa0]+$/g, ''); | 
|  | }; | 
|  |  | 
|  |  | 
|  | /** | 
|  | * Helper function for getVisibleText[InDisplayedDom]. | 
|  | * @param {!Array.<string>} lines Accumulated visible lines of text. | 
|  | * @return {string} cleaned up concatenated lines | 
|  | * @private | 
|  | */ | 
|  | bot.dom.concatenateCleanedLines_ = function(lines) { | 
|  | lines = goog.array.map( | 
|  | lines, | 
|  | bot.dom.trimExcludingNonBreakingSpaceCharacters_); | 
|  | var joined = lines.join('\n'); | 
|  | var trimmed = bot.dom.trimExcludingNonBreakingSpaceCharacters_(joined); | 
|  |  | 
|  | // Replace non-breakable spaces with regular ones. | 
|  | return trimmed.replace(/\xa0/g, ' '); | 
|  | }; | 
|  |  | 
|  |  | 
|  | /** | 
|  | * @param {!Element} elem The element to consider. | 
|  | * @return {string} visible text. | 
|  | */ | 
|  | bot.dom.getVisibleText = function(elem) { | 
|  | var lines = []; | 
|  |  | 
|  | if (bot.dom.IS_SHADOW_DOM_ENABLED) { | 
|  | bot.dom.appendVisibleTextLinesFromElementInComposedDom_(elem, lines); | 
|  | } else { | 
|  | bot.dom.appendVisibleTextLinesFromElement_(elem, lines); | 
|  | } | 
|  | return bot.dom.concatenateCleanedLines_(lines); | 
|  | }; | 
|  |  | 
|  |  | 
|  | /** | 
|  | * Helper function used by bot.dom.appendVisibleTextLinesFromElement_ and | 
|  | * bot.dom.appendVisibleTextLinesFromElementInComposedDom_ | 
|  | * @param {!Element} elem Element. | 
|  | * @param {!Array.<string>} lines Accumulated visible lines of text. | 
|  | * @param {function(!Element):boolean} isShownFn function to call to | 
|  | *     tell if an element is shown | 
|  | * @param {function(!Node, !Array.<string>, boolean, ?string, ?string):void} | 
|  | *     childNodeFn function to call to append lines from any child nodes | 
|  | * @private | 
|  | */ | 
|  | bot.dom.appendVisibleTextLinesFromElementCommon_ = function( | 
|  | elem, lines, isShownFn, childNodeFn) { | 
|  | function currLine() { | 
|  | return /** @type {string|undefined} */ (goog.array.peek(lines)) || ''; | 
|  | } | 
|  |  | 
|  | // TODO: Add case here for textual form elements. | 
|  | if (bot.dom.isElement(elem, goog.dom.TagName.BR)) { | 
|  | lines.push(''); | 
|  | } else { | 
|  | // TODO: properly handle display:run-in | 
|  | var isTD = bot.dom.isElement(elem, goog.dom.TagName.TD); | 
|  | var display = bot.dom.getEffectiveStyle(elem, 'display'); | 
|  | // On some browsers, table cells incorrectly show up with block styles. | 
|  | var isBlock = !isTD && | 
|  | !goog.array.contains(bot.dom.INLINE_DISPLAY_BOXES_, display); | 
|  |  | 
|  | // Add a newline before block elems when there is text on the current line, | 
|  | // except when the previous sibling has a display: run-in. | 
|  | // Also, do not run-in the previous sibling if this element is floated. | 
|  |  | 
|  | var previousElementSibling = goog.dom.getPreviousElementSibling(elem); | 
|  | var prevDisplay = (previousElementSibling) ? | 
|  | bot.dom.getEffectiveStyle(previousElementSibling, 'display') : ''; | 
|  | // TODO: getEffectiveStyle should mask this for us | 
|  | var thisFloat = bot.dom.getEffectiveStyle(elem, 'float') || | 
|  | bot.dom.getEffectiveStyle(elem, 'cssFloat') || | 
|  | bot.dom.getEffectiveStyle(elem, 'styleFloat'); | 
|  | var runIntoThis = prevDisplay == 'run-in' && thisFloat == 'none'; | 
|  | if (isBlock && !runIntoThis && | 
|  | !goog.string.isEmptyOrWhitespace(currLine())) { | 
|  | lines.push(''); | 
|  | } | 
|  |  | 
|  | // This element may be considered unshown, but have a child that is | 
|  | // explicitly shown (e.g. this element has "visibility:hidden"). | 
|  | // Nevertheless, any text nodes that are direct descendants of this | 
|  | // element will not contribute to the visible text. | 
|  | var shown = isShownFn(elem); | 
|  |  | 
|  | // All text nodes that are children of this element need to know the | 
|  | // effective "white-space" and "text-transform" styles to properly | 
|  | // compute their contribution to visible text. Compute these values once. | 
|  | var whitespace = null, textTransform = null; | 
|  | if (shown) { | 
|  | whitespace = bot.dom.getEffectiveStyle(elem, 'white-space'); | 
|  | textTransform = bot.dom.getEffectiveStyle(elem, 'text-transform'); | 
|  | } | 
|  |  | 
|  | goog.array.forEach(elem.childNodes, function(node) { | 
|  | childNodeFn(node, lines, shown, whitespace, textTransform); | 
|  | }); | 
|  |  | 
|  | var line = currLine(); | 
|  |  | 
|  | // Here we differ from standard innerText implementations (if there were | 
|  | // such a thing). Usually, table cells are separated by a tab, but we | 
|  | // normalize tabs into single spaces. | 
|  | if ((isTD || display == 'table-cell') && line && | 
|  | !goog.string.endsWith(line, ' ')) { | 
|  | lines[lines.length - 1] += ' '; | 
|  | } | 
|  |  | 
|  | // Add a newline after block elems when there is text on the current line, | 
|  | // and the current element isn't marked as run-in. | 
|  | if (isBlock && display != 'run-in' && | 
|  | !goog.string.isEmptyOrWhitespace(line)) { | 
|  | lines.push(''); | 
|  | } | 
|  | } | 
|  | }; | 
|  |  | 
|  |  | 
|  | /** | 
|  | * @param {!Element} elem Element. | 
|  | * @param {!Array.<string>} lines Accumulated visible lines of text. | 
|  | * @private | 
|  | */ | 
|  | bot.dom.appendVisibleTextLinesFromElement_ = function(elem, lines) { | 
|  | bot.dom.appendVisibleTextLinesFromElementCommon_( | 
|  | elem, lines, bot.dom.isShown, | 
|  | function(node, lines, shown, whitespace, textTransform) { | 
|  | if (node.nodeType == goog.dom.NodeType.TEXT && shown) { | 
|  | var textNode = /** @type {!Text} */ (node); | 
|  | bot.dom.appendVisibleTextLinesFromTextNode_(textNode, lines, | 
|  | whitespace, textTransform); | 
|  | } else if (bot.dom.isElement(node)) { | 
|  | var castElem = /** @type {!Element} */ (node); | 
|  | bot.dom.appendVisibleTextLinesFromElement_(castElem, lines); | 
|  | } | 
|  | }); | 
|  | }; | 
|  |  | 
|  |  | 
|  | /** | 
|  | * Elements with one of these effective "display" styles are treated as inline | 
|  | * display boxes and have their visible text appended to the current line. | 
|  | * @private {!Array.<string>} | 
|  | * @const | 
|  | */ | 
|  | bot.dom.INLINE_DISPLAY_BOXES_ = [ | 
|  | 'inline', | 
|  | 'inline-block', | 
|  | 'inline-table', | 
|  | 'none', | 
|  | 'table-cell', | 
|  | 'table-column', | 
|  | 'table-column-group' | 
|  | ]; | 
|  |  | 
|  |  | 
|  | /** | 
|  | * @param {!Text} textNode Text node. | 
|  | * @param {!Array.<string>} lines Accumulated visible lines of text. | 
|  | * @param {?string} whitespace Parent element's "white-space" style. | 
|  | * @param {?string} textTransform Parent element's "text-transform" style. | 
|  | * @private | 
|  | */ | 
|  | bot.dom.appendVisibleTextLinesFromTextNode_ = function(textNode, lines, | 
|  | whitespace, textTransform) { | 
|  |  | 
|  | // First, remove zero-width characters. Do this before regularizing spaces as | 
|  | // the zero-width space is both zero-width and a space, but we do not want to | 
|  | // make it visible by converting it to a regular space. | 
|  | // The replaced characters are: | 
|  | //   U+200B: Zero-width space | 
|  | //   U+200E: Left-to-right mark | 
|  | //   U+200F: Right-to-left mark | 
|  | var text = textNode.nodeValue.replace(/[\u200b\u200e\u200f]/g, ''); | 
|  |  | 
|  | // Canonicalize the new lines, and then collapse new lines | 
|  | // for the whitespace styles that collapse. See: | 
|  | // https://developer.mozilla.org/en/CSS/white-space | 
|  | text = goog.string.canonicalizeNewlines(text); | 
|  | if (whitespace == 'normal' || whitespace == 'nowrap') { | 
|  | text = text.replace(/\n/g, ' '); | 
|  | } | 
|  |  | 
|  | // For pre and pre-wrap whitespace styles, convert all breaking spaces to be | 
|  | // non-breaking, otherwise, collapse all breaking spaces. Breaking spaces are | 
|  | // converted to regular spaces by getVisibleText(). | 
|  | if (whitespace == 'pre' || whitespace == 'pre-wrap') { | 
|  | text = text.replace(/[ \f\t\v\u2028\u2029]/g, '\xa0'); | 
|  | } else { | 
|  | text = text.replace(/[\ \f\t\v\u2028\u2029]+/g, ' '); | 
|  | } | 
|  |  | 
|  | if (textTransform == 'capitalize') { | 
|  | // the unicode regex ending with /gu does not work in IE | 
|  | var re = goog.userAgent.IE ? /(^|\s|\b)(\S)/g : /(^|[^\d\p{L}\p{S}])([\p{Ll}|\p{S}])/gu; | 
|  | text = text.replace(re, function() { | 
|  | return arguments[1] + arguments[2].toUpperCase(); | 
|  | }); | 
|  | } else if (textTransform == 'uppercase') { | 
|  | text = text.toUpperCase(); | 
|  | } else if (textTransform == 'lowercase') { | 
|  | text = text.toLowerCase(); | 
|  | } | 
|  |  | 
|  | var currLine = lines.pop() || ''; | 
|  | if (goog.string.endsWith(currLine, ' ') && | 
|  | goog.string.startsWith(text, ' ')) { | 
|  | text = text.substr(1); | 
|  | } | 
|  | lines.push(currLine + text); | 
|  | }; | 
|  |  | 
|  |  | 
|  | /** | 
|  | * Gets the opacity of a node (x-browser). | 
|  | * This gets the inline style opacity of the node and takes into account the | 
|  | * cascaded or the computed style for this node. | 
|  | * | 
|  | * @param {!Element} elem Element whose opacity has to be found. | 
|  | * @return {number} Opacity between 0 and 1. | 
|  | */ | 
|  | bot.dom.getOpacity = function(elem) { | 
|  | // TODO: Does this need to deal with rgba colors? | 
|  | if (!bot.userAgent.IE_DOC_PRE9) { | 
|  | return bot.dom.getOpacityNonIE_(elem); | 
|  | } else { | 
|  | if (bot.dom.getEffectiveStyle(elem, 'position') == 'relative') { | 
|  | // Filter does not apply to non positioned elements. | 
|  | return 1; | 
|  | } | 
|  |  | 
|  | var opacityStyle = bot.dom.getEffectiveStyle(elem, 'filter'); | 
|  | var groups = opacityStyle.match(/^alpha\(opacity=(\d*)\)/) || | 
|  | opacityStyle.match( | 
|  | /^progid:DXImageTransform.Microsoft.Alpha\(Opacity=(\d*)\)/); | 
|  |  | 
|  | if (groups) { | 
|  | return Number(groups[1]) / 100; | 
|  | } else { | 
|  | return 1; // Opaque. | 
|  | } | 
|  | } | 
|  | }; | 
|  |  | 
|  |  | 
|  | /** | 
|  | * Implementation of getOpacity for browsers that do support | 
|  | * the "opacity" style. | 
|  | * | 
|  | * @param {!Element} elem Element whose opacity has to be found. | 
|  | * @return {number} Opacity between 0 and 1. | 
|  | * @private | 
|  | */ | 
|  | bot.dom.getOpacityNonIE_ = function(elem) { | 
|  | // By default the element is opaque. | 
|  | var elemOpacity = 1; | 
|  |  | 
|  | var opacityStyle = bot.dom.getEffectiveStyle(elem, 'opacity'); | 
|  | if (opacityStyle) { | 
|  | elemOpacity = Number(opacityStyle); | 
|  | } | 
|  |  | 
|  | // Let's apply the parent opacity to the element. | 
|  | var parentElement = bot.dom.getParentElement(elem); | 
|  | if (parentElement) { | 
|  | elemOpacity = elemOpacity * bot.dom.getOpacityNonIE_(parentElement); | 
|  | } | 
|  | return elemOpacity; | 
|  | }; | 
|  |  | 
|  |  | 
|  | /** | 
|  | * Returns the display parent element of the given node, or null. This method | 
|  | * differs from bot.dom.getParentElement in the presence of ShadowDOM and | 
|  | * <shadow> or <content> tags. For example if | 
|  | * <ul> | 
|  | * <li>div A contains div B | 
|  | * <li>div B has a css class .C | 
|  | * <li>div A contains a Shadow DOM with a div D | 
|  | * <li>div D contains a contents tag selecting all items of class .C | 
|  | * </ul> | 
|  | * then calling bot.dom.getParentElement on B will return A, but calling | 
|  | * getDisplayParentElement on B will return D. | 
|  | * | 
|  | * @param {!Node} node The node whose parent is desired. | 
|  | * @return {Node} The parent node, if available, null otherwise. | 
|  | */ | 
|  | bot.dom.getParentNodeInComposedDom = function(node) { | 
|  | var /**@type {Node}*/ parent = node.parentNode; | 
|  |  | 
|  | // Shadow DOM v1 | 
|  | if (parent && parent.shadowRoot && node.assignedSlot !== undefined) { | 
|  | // Can be null on purpose, meaning it has no parent as | 
|  | // it hasn't yet been slotted | 
|  | return node.assignedSlot ? node.assignedSlot.parentNode : null; | 
|  | } | 
|  |  | 
|  | // Shadow DOM V0 (deprecated) | 
|  | if (node.getDestinationInsertionPoints) { | 
|  | var destinations = node.getDestinationInsertionPoints(); | 
|  | if (destinations.length > 0) { | 
|  | return destinations[destinations.length - 1]; | 
|  | } | 
|  | } | 
|  |  | 
|  | return parent; | 
|  | }; | 
|  |  | 
|  |  | 
|  | /** | 
|  | * @param {!Node} node Node. | 
|  | * @param {!Array.<string>} lines Accumulated visible lines of text. | 
|  | * @param {boolean} shown whether the node is visible | 
|  | * @param {?string} whitespace the node's 'white-space' effectiveStyle | 
|  | * @param {?string} textTransform the node's 'text-transform' effectiveStyle | 
|  | * @private | 
|  | * @suppress {missingProperties} | 
|  | */ | 
|  | bot.dom.appendVisibleTextLinesFromNodeInComposedDom_ = function( | 
|  | node, lines, shown, whitespace, textTransform) { | 
|  |  | 
|  | if (node.nodeType == goog.dom.NodeType.TEXT && shown) { | 
|  | var textNode = /** @type {!Text} */ (node); | 
|  | bot.dom.appendVisibleTextLinesFromTextNode_(textNode, lines, | 
|  | whitespace, textTransform); | 
|  | } else if (bot.dom.isElement(node)) { | 
|  | var castElem = /** @type {!Element} */ (node); | 
|  |  | 
|  | if (bot.dom.isElement(node, 'CONTENT') || bot.dom.isElement(node, 'SLOT')) { | 
|  | var parentNode = node; | 
|  | while (parentNode.parentNode) { | 
|  | parentNode = parentNode.parentNode; | 
|  | } | 
|  | if (parentNode instanceof ShadowRoot) { | 
|  | // If the element is <content> and we're inside a shadow DOM then just | 
|  | // append the contents of the nodes that have been distributed into it. | 
|  | var contentElem = /** @type {!Object} */ (node); | 
|  | var shadowChildren; | 
|  | if (bot.dom.isElement(node, 'CONTENT')) { | 
|  | shadowChildren = contentElem.getDistributedNodes(); | 
|  | } else { | 
|  | shadowChildren = contentElem.assignedNodes(); | 
|  | } | 
|  | goog.array.forEach(shadowChildren, function(node) { | 
|  | bot.dom.appendVisibleTextLinesFromNodeInComposedDom_( | 
|  | node, lines, shown, whitespace, textTransform); | 
|  | }); | 
|  | } else { | 
|  | // if we're not inside a shadow DOM, then we just treat <content> | 
|  | // as an unknown element and use anything inside the tag | 
|  | bot.dom.appendVisibleTextLinesFromElementInComposedDom_( | 
|  | castElem, lines); | 
|  | } | 
|  | } else if (bot.dom.isElement(node, 'SHADOW')) { | 
|  | // if the element is <shadow> then find the owning shadowRoot | 
|  | var parentNode = node; | 
|  | while (parentNode.parentNode) { | 
|  | parentNode = parentNode.parentNode; | 
|  | } | 
|  | if (parentNode instanceof ShadowRoot) { | 
|  | var thisShadowRoot = /** @type {!ShadowRoot} */ (parentNode); | 
|  | if (thisShadowRoot) { | 
|  | // then go through the owning shadowRoots older siblings and append | 
|  | // their contents | 
|  | var olderShadowRoot = thisShadowRoot.olderShadowRoot; | 
|  | while (olderShadowRoot) { | 
|  | goog.array.forEach( | 
|  | olderShadowRoot.childNodes, function(childNode) { | 
|  | bot.dom.appendVisibleTextLinesFromNodeInComposedDom_( | 
|  | childNode, lines, shown, whitespace, textTransform); | 
|  | }); | 
|  | olderShadowRoot = olderShadowRoot.olderShadowRoot; | 
|  | } | 
|  | } | 
|  | } | 
|  | } else { | 
|  | // otherwise append the contents of an element as per normal. | 
|  | bot.dom.appendVisibleTextLinesFromElementInComposedDom_( | 
|  | castElem, lines); | 
|  | } | 
|  | } | 
|  | }; | 
|  |  | 
|  |  | 
|  | /** | 
|  | * Determines whether a given node has been distributed into a ShadowDOM | 
|  | * element somewhere. | 
|  | * @param {!Node} node The node to check | 
|  | * @return {boolean} True if the node has been distributed. | 
|  | */ | 
|  | bot.dom.isNodeDistributedIntoShadowDom = function(node) { | 
|  | var elemOrText = null; | 
|  | if (node.nodeType == goog.dom.NodeType.ELEMENT) { | 
|  | elemOrText = /** @type {!Element} */ (node); | 
|  | } else if (node.nodeType == goog.dom.NodeType.TEXT) { | 
|  | elemOrText = /** @type {!Text} */ (node); | 
|  | } | 
|  | return elemOrText != null && | 
|  | (elemOrText.assignedSlot != null || | 
|  | (elemOrText.getDestinationInsertionPoints && | 
|  | elemOrText.getDestinationInsertionPoints().length > 0) | 
|  | ); | 
|  | }; | 
|  |  | 
|  |  | 
|  | /** | 
|  | * @param {!Element} elem Element. | 
|  | * @param {!Array.<string>} lines Accumulated visible lines of text. | 
|  | * @private | 
|  | */ | 
|  | bot.dom.appendVisibleTextLinesFromElementInComposedDom_ = function( | 
|  | elem, lines) { | 
|  | if (elem.shadowRoot) { | 
|  | goog.array.forEach(elem.shadowRoot.childNodes, function(node) { | 
|  | bot.dom.appendVisibleTextLinesFromNodeInComposedDom_( | 
|  | node, lines, true, null, null); | 
|  | }); | 
|  | } | 
|  |  | 
|  | bot.dom.appendVisibleTextLinesFromElementCommon_( | 
|  | elem, lines, bot.dom.isShown, | 
|  | function(node, lines, shown, whitespace, textTransform) { | 
|  | // If the node has been distributed into a shadowDom element | 
|  | // to be displayed elsewhere, then we shouldn't append | 
|  | // its contents here). | 
|  | if (!bot.dom.isNodeDistributedIntoShadowDom(node)) { | 
|  | bot.dom.appendVisibleTextLinesFromNodeInComposedDom_( | 
|  | node, lines, shown, whitespace, textTransform); | 
|  | } | 
|  | }); | 
|  | }; |