| // 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.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(); |
| } |
| const childrenToTraverse = |
| shadowChildren.length > 0 ? shadowChildren : contentElem.childNodes; |
| goog.array.forEach(childrenToTraverse, 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); |
| } |
| }); |
| }; |