// Licensed to the Software Freedom Conservancy (SFC) under one1// or more contributor license agreements. See the NOTICE file2// distributed with this work for additional information3// regarding copyright ownership. The SFC licenses this file4// to you under the Apache License, Version 2.0 (the5// "License"); you may not use this file except in compliance6// with the License. You may obtain a copy of the License at7//8// http://www.apache.org/licenses/LICENSE-2.09//10// Unless required by applicable law or agreed to in writing,11// software distributed under the License is distributed on an12// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY13// KIND, either express or implied. See the License for the14// specific language governing permissions and limitations15// under the License.1617/**18* @fileoverview DOM manipulation and querying routines.19*/2021goog.provide('bot.dom');2223goog.require('bot');24goog.require('bot.color');25goog.require('bot.dom.core');26goog.require('bot.locators.css');27goog.require('bot.userAgent');28goog.require('goog.array');29goog.require('goog.dom');30goog.require('goog.dom.DomHelper');31goog.require('goog.dom.NodeType');32goog.require('goog.dom.TagName');33goog.require('goog.math');34goog.require('goog.math.Coordinate');35goog.require('goog.math.Rect');36goog.require('goog.string');37goog.require('goog.style');38goog.require('goog.userAgent');394041/**42* Whether Shadow DOM operations are supported by the browser.43* @const {boolean}44*/45bot.dom.IS_SHADOW_DOM_ENABLED = (typeof ShadowRoot === 'function');464748/**49* Retrieves the active element for a node's owner document.50* @param {(!Node|!Window)} nodeOrWindow The node whose owner document to get51* the active element for.52* @return {?Element} The active element, if any.53*/54bot.dom.getActiveElement = function (nodeOrWindow) {55var active = goog.dom.getActiveElement(56goog.dom.getOwnerDocument(nodeOrWindow));57// IE has the habit of returning an empty object from58// goog.dom.getActiveElement instead of null.59if (goog.userAgent.IE &&60active &&61typeof active.nodeType === 'undefined') {62return null;63}64return active;65};666768/**69* @const70*/71bot.dom.isElement = bot.dom.core.isElement;727374/**75* Returns whether an element is in an interactable state: whether it is shown76* to the user, ignoring its opacity, and whether it is enabled.77*78* @param {!Element} element The element to check.79* @return {boolean} Whether the element is interactable.80* @see bot.dom.isShown.81* @see bot.dom.isEnabled82*/83bot.dom.isInteractable = function (element) {84return bot.dom.isShown(element, /*ignoreOpacity=*/true) &&85bot.dom.isEnabled(element) &&86!bot.dom.hasPointerEventsDisabled_(element);87};888990/**91* @param {!Element} element Element.92* @return {boolean} Whether element is set by the CSS pointer-events property93* not to be interactable.94* @private95*/96bot.dom.hasPointerEventsDisabled_ = function (element) {97if (goog.userAgent.IE ||98(goog.userAgent.GECKO && !bot.userAgent.isEngineVersion('1.9.2'))) {99// Don't support pointer events100return false;101}102return bot.dom.getEffectiveStyle(element, 'pointer-events') == 'none';103};104105106/**107* @const108*/109bot.dom.isSelectable = bot.dom.core.isSelectable;110111112/**113* @const114*/115bot.dom.isSelected = bot.dom.core.isSelected;116117118/**119* List of the focusable fields, according to120* http://www.w3.org/TR/html401/interact/scripts.html#adef-onfocus121* @private {!Array.<!goog.dom.TagName>}122* @const123*/124bot.dom.FOCUSABLE_FORM_FIELDS_ = [125goog.dom.TagName.A,126goog.dom.TagName.AREA,127goog.dom.TagName.BUTTON,128goog.dom.TagName.INPUT,129goog.dom.TagName.LABEL,130goog.dom.TagName.SELECT,131goog.dom.TagName.TEXTAREA132];133134135/**136* Returns whether a node is a focusable element. An element may receive focus137* if it is a form field, has a non-negative tabindex, or is editable.138* @param {!Element} element The node to test.139* @return {boolean} Whether the node is focusable.140*/141bot.dom.isFocusable = function (element) {142return goog.array.some(bot.dom.FOCUSABLE_FORM_FIELDS_, tagNameMatches) ||143(bot.dom.getAttribute(element, 'tabindex') != null &&144Number(bot.dom.getProperty(element, 'tabIndex')) >= 0) ||145bot.dom.isEditable(element);146147function tagNameMatches(tagName) {148return bot.dom.isElement(element, tagName);149}150};151152153/**154* @const155*/156bot.dom.getProperty = bot.dom.core.getProperty;157158159/**160* @const161*/162bot.dom.getAttribute = bot.dom.core.getAttribute;163164165/**166* List of elements that support the "disabled" attribute, as defined by the167* HTML 4.01 specification.168* @private {!Array.<!goog.dom.TagName>}169* @const170* @see http://www.w3.org/TR/html401/interact/forms.html#h-17.12.1171*/172bot.dom.DISABLED_ATTRIBUTE_SUPPORTED_ = [173goog.dom.TagName.BUTTON,174goog.dom.TagName.INPUT,175goog.dom.TagName.OPTGROUP,176goog.dom.TagName.OPTION,177goog.dom.TagName.SELECT,178goog.dom.TagName.TEXTAREA179];180181182/**183* Determines if an element is enabled. An element is considered enabled if it184* does not support the "disabled" attribute, or if it is not disabled.185* @param {!Element} el The element to test.186* @return {boolean} Whether the element is enabled.187*/188bot.dom.isEnabled = function (el) {189var isSupported = goog.array.some(190bot.dom.DISABLED_ATTRIBUTE_SUPPORTED_,191function (tagName) { return bot.dom.isElement(el, tagName); });192if (!isSupported) {193return true;194}195196if (bot.dom.getProperty(el, 'disabled')) {197return false;198}199200// The element is not explicitly disabled, but if it is an OPTION or OPTGROUP,201// we must test if it inherits its state from a parent.202if (el.parentNode &&203el.parentNode.nodeType == goog.dom.NodeType.ELEMENT &&204bot.dom.isElement(el, goog.dom.TagName.OPTGROUP) ||205bot.dom.isElement(el, goog.dom.TagName.OPTION)) {206return bot.dom.isEnabled(/**@type{!Element}*/(el.parentNode));207}208209// Is there an ancestor of the current element that is a disabled fieldset210// and whose child is also an ancestor-or-self of the current element but is211// not the first legend child of the fieldset. If so then the element is212// disabled.213return !goog.dom.getAncestor(el, function (e) {214var parent = e.parentNode;215216if (parent &&217bot.dom.isElement(parent, goog.dom.TagName.FIELDSET) &&218bot.dom.getProperty(/** @type {!Element} */(parent), 'disabled')) {219if (!bot.dom.isElement(e, goog.dom.TagName.LEGEND)) {220return true;221}222223var sibling = e;224// Are there any previous legend siblings? If so then we are not the225// first and the element is disabled226while (sibling = goog.dom.getPreviousElementSibling(sibling)) {227if (bot.dom.isElement(sibling, goog.dom.TagName.LEGEND)) {228return true;229}230}231}232return false;233}, true);234};235236237/**238* List of input types that create text fields.239* @private {!Array.<string>}240* @const241* @see http://www.whatwg.org/specs/web-apps/current-work/multipage/the-input-element.html#attr-input-type242*/243bot.dom.TEXTUAL_INPUT_TYPES_ = [244'text',245'search',246'tel',247'url',248'email',249'password',250'number'251];252253254/**255* TODO: Add support for designMode elements.256*257* @param {!Element} element The element to check.258* @return {boolean} Whether the element accepts user-typed text.259*/260bot.dom.isTextual = function (element) {261if (bot.dom.isElement(element, goog.dom.TagName.TEXTAREA)) {262return true;263}264265if (bot.dom.isElement(element, goog.dom.TagName.INPUT)) {266var type = element.type.toLowerCase();267return goog.array.contains(bot.dom.TEXTUAL_INPUT_TYPES_, type);268}269270if (bot.dom.isContentEditable(element)) {271return true;272}273274return false;275};276277278/**279* @param {!Element} element The element to check.280* @return {boolean} Whether the element is a file input.281*/282bot.dom.isFileInput = function (element) {283if (bot.dom.isElement(element, goog.dom.TagName.INPUT)) {284var type = element.type.toLowerCase();285return type == 'file';286}287288return false;289};290291292/**293* @param {!Element} element The element to check.294* @param {string} inputType The type of input to check.295* @return {boolean} Whether the element is an input with specified type.296*/297bot.dom.isInputType = function (element, inputType) {298if (bot.dom.isElement(element, goog.dom.TagName.INPUT)) {299var type = element.type.toLowerCase();300return type == inputType;301}302303return false;304};305306307/**308* @param {!Element} element The element to check.309* @return {boolean} Whether the element is contentEditable.310*/311bot.dom.isContentEditable = function (element) {312// Check if browser supports contentEditable.313if (!goog.isDef(element['contentEditable'])) {314return false;315}316317// Checking the element's isContentEditable property is preferred except for318// IE where that property is not reliable on IE versions 7, 8, and 9.319if (!goog.userAgent.IE && goog.isDef(element['isContentEditable'])) {320return element.isContentEditable;321}322323// For IE and for browsers where contentEditable is supported but324// isContentEditable is not, traverse up the ancestors:325function legacyIsContentEditable(e) {326if (e.contentEditable == 'inherit') {327var parent = bot.dom.getParentElement(e);328return parent ? legacyIsContentEditable(parent) : false;329} else {330return e.contentEditable == 'true';331}332}333return legacyIsContentEditable(element);334};335336337/**338* TODO: Merge isTextual into this function and move to bot.dom.339* For Puppet, requires adding support to getVisibleText for grabbing340* text from all textual elements.341*342* Whether the element may contain text the user can edit.343*344* @param {!Element} element The element to check.345* @return {boolean} Whether the element accepts user-typed text.346*/347bot.dom.isEditable = function (element) {348return (bot.dom.isTextual(element) ||349bot.dom.isFileInput(element) ||350bot.dom.isInputType(element, 'range') ||351bot.dom.isInputType(element, 'date') ||352bot.dom.isInputType(element, 'month') ||353bot.dom.isInputType(element, 'week') ||354bot.dom.isInputType(element, 'time') ||355bot.dom.isInputType(element, 'datetime-local') ||356bot.dom.isInputType(element, 'color')) &&357!bot.dom.getProperty(element, 'readOnly');358};359360361/**362* Returns the parent element of the given node, or null. This is required363* because the parent node may not be another element.364*365* @param {!Node} node The node who's parent is desired.366* @return {Element} The parent element, if available, null otherwise.367*/368bot.dom.getParentElement = function (node) {369var elem = node.parentNode;370371while (elem &&372elem.nodeType != goog.dom.NodeType.ELEMENT &&373elem.nodeType != goog.dom.NodeType.DOCUMENT &&374elem.nodeType != goog.dom.NodeType.DOCUMENT_FRAGMENT) {375elem = elem.parentNode;376}377return /** @type {Element} */ (bot.dom.isElement(elem) ? elem : null);378};379380381/**382* Retrieves an explicitly-set, inline style value of an element. This returns383* '' if there isn't a style attribute on the element or if this style property384* has not been explicitly set in script.385*386* @param {!Element} elem Element to get the style value from.387* @param {string} styleName Name of the style property in selector-case.388* @return {string} The value of the style property.389*/390bot.dom.getInlineStyle = function (elem, styleName) {391return goog.style.getStyle(elem, styleName);392};393394395/**396* Retrieves the implicitly-set, effective style of an element, or null if it is397* unknown. It returns the computed style where available; otherwise it looks398* up the DOM tree for the first style value not equal to 'inherit,' using the399* IE currentStyle of each node if available, and otherwise the inline style.400* Since the computed, current, and inline styles can be different, the return401* value of this function is not always consistent across browsers. See:402* http://code.google.com/p/doctype/wiki/ArticleComputedStyleVsCascadedStyle403*404* @param {!Element} elem Element to get the style value from.405* @param {string} propertyName Name of the CSS property.406* @return {?string} The value of the style property, or null.407*/408bot.dom.getEffectiveStyle = function (elem, propertyName) {409var styleName = goog.string.toCamelCase(propertyName);410if (styleName == 'float' ||411styleName == 'cssFloat' ||412styleName == 'styleFloat') {413styleName = bot.userAgent.IE_DOC_PRE9 ? 'styleFloat' : 'cssFloat';414}415var style = goog.style.getComputedStyle(elem, styleName) ||416bot.dom.getCascadedStyle_(elem, styleName);417if (style === null) {418return null;419}420return bot.color.standardizeColor(styleName, style);421};422423424/**425* Looks up the DOM tree for the first style value not equal to 'inherit,' using426* the currentStyle of each node if available, and otherwise the inline style.427*428* @param {!Element} elem Element to get the style value from.429* @param {string} styleName CSS style property in camelCase.430* @return {?string} The value of the style property, or null.431* @private432*/433bot.dom.getCascadedStyle_ = function (elem, styleName) {434var style = elem.currentStyle || elem.style;435var value = style[styleName];436if (!goog.isDef(value) && goog.isFunction(style.getPropertyValue)) {437value = style.getPropertyValue(styleName);438}439440if (value != 'inherit') {441return goog.isDef(value) ? value : null;442}443var parent = bot.dom.getParentElement(elem);444return parent ? bot.dom.getCascadedStyle_(parent, styleName) : null;445};446447448/**449* Extracted code from bot.dom.isShown.450*451* @param {!Element} elem The element to consider.452* @param {boolean} ignoreOpacity Whether to ignore the element's opacity453* when determining whether it is shown.454* @param {function(!Element):boolean} parentsDisplayedFn a function that's used455* to tell if the chain of ancestors are all shown.456* @return {boolean} Whether or not the element is visible.457* @private458*/459bot.dom.isShown_ = function (elem, ignoreOpacity, parentsDisplayedFn) {460if (!bot.dom.isElement(elem)) {461throw new Error('Argument to isShown must be of type Element');462}463464// By convention, BODY element is always shown: BODY represents the document465// and even if there's nothing rendered in there, user can always see there's466// the document.467if (bot.dom.isElement(elem, goog.dom.TagName.BODY)) {468return true;469}470471// Option or optgroup is shown iff enclosing select is shown (ignoring the472// select's opacity).473if (bot.dom.isElement(elem, goog.dom.TagName.OPTION) ||474bot.dom.isElement(elem, goog.dom.TagName.OPTGROUP)) {475var select = /**@type {Element}*/ (goog.dom.getAncestor(elem, function (e) {476return bot.dom.isElement(e, goog.dom.TagName.SELECT);477}));478return !!select && bot.dom.isShown_(select, true, parentsDisplayedFn);479}480481// Image map elements are shown if image that uses it is shown, and482// the area of the element is positive.483var imageMap = bot.dom.maybeFindImageMap_(elem);484if (imageMap) {485return !!imageMap.image &&486imageMap.rect.width > 0 && imageMap.rect.height > 0 &&487bot.dom.isShown_(488imageMap.image, ignoreOpacity, parentsDisplayedFn);489}490491// Any hidden input is not shown.492if (bot.dom.isElement(elem, goog.dom.TagName.INPUT) &&493elem.type.toLowerCase() == 'hidden') {494return false;495}496497// Any NOSCRIPT element is not shown.498if (bot.dom.isElement(elem, goog.dom.TagName.NOSCRIPT)) {499return false;500}501502// Any element with hidden/collapsed visibility is not shown.503var visibility = bot.dom.getEffectiveStyle(elem, 'visibility');504if (visibility == 'collapse' || visibility == 'hidden') {505return false;506}507508if (!parentsDisplayedFn(elem)) {509return false;510}511512// Any transparent element is not shown.513if (!ignoreOpacity && bot.dom.getOpacity(elem) == 0) {514return false;515}516517// Any element without positive size dimensions is not shown.518function positiveSize(e) {519var rect = bot.dom.getClientRect(e);520if (rect.height > 0 && rect.width > 0) {521return true;522}523// A vertical or horizontal SVG Path element will report zero width or524// height but is "shown" if it has a positive stroke-width.525if (bot.dom.isElement(e, 'PATH') && (rect.height > 0 || rect.width > 0)) {526var strokeWidth = bot.dom.getEffectiveStyle(e, 'stroke-width');527return !!strokeWidth && (parseInt(strokeWidth, 10) > 0);528}529// Zero-sized elements should still be considered to have positive size530// if they have a child element or text node with positive size, unless531// the element has an 'overflow' style of 'hidden'.532return bot.dom.getEffectiveStyle(e, 'overflow') != 'hidden' &&533goog.array.some(e.childNodes, function (n) {534return n.nodeType == goog.dom.NodeType.TEXT ||535(bot.dom.isElement(n) && positiveSize(n));536});537}538if (!positiveSize(elem)) {539return false;540}541542// Elements that are hidden by overflow are not shown.543function hiddenByOverflow(e) {544return bot.dom.getOverflowState(e) == bot.dom.OverflowState.HIDDEN &&545goog.array.every(e.childNodes, function (n) {546return !bot.dom.isElement(n) || hiddenByOverflow(n) ||547!positiveSize(n);548});549}550return !hiddenByOverflow(elem);551};552553554/**555* Determines whether an element is what a user would call "shown". This means556* that the element is shown in the viewport of the browser, and only has557* height and width greater than 0px, and that its visibility is not "hidden"558* and its display property is not "none".559* Options and Optgroup elements are treated as special cases: they are560* considered shown iff they have a enclosing select element that is shown.561*562* Elements in Shadow DOMs with younger shadow roots are not visible, and563* elements distributed into shadow DOMs check the visibility of the564* ancestors in the Composed DOM, rather than their ancestors in the logical565* DOM.566*567* @param {!Element} elem The element to consider.568* @param {boolean=} opt_ignoreOpacity Whether to ignore the element's opacity569* when determining whether it is shown; defaults to false.570* @return {boolean} Whether or not the element is visible.571*/572bot.dom.isShown = function (elem, opt_ignoreOpacity) {573/**574* Determines whether an element or its parents have `display: none` set575* @param {!Node} e the element576* @return {!boolean}577*/578function displayed(e) {579if (bot.dom.isElement(e)) {580var elem = /** @type {!Element} */ (e);581if ((bot.dom.getEffectiveStyle(elem, 'display') == 'none')582|| (bot.dom.getEffectiveStyle(elem, 'content-visibility') == 'hidden')) {583return false;584}585}586587var parent = bot.dom.getParentNodeInComposedDom(e);588589if (bot.dom.IS_SHADOW_DOM_ENABLED && (parent instanceof ShadowRoot)) {590if (parent.host.shadowRoot && parent.host.shadowRoot !== parent) {591// There is a younger shadow root, which will take precedence over592// the shadow this element is in, thus this element won't be593// displayed.594return false;595} else {596parent = parent.host;597}598}599600if (parent && (parent.nodeType == goog.dom.NodeType.DOCUMENT ||601parent.nodeType == goog.dom.NodeType.DOCUMENT_FRAGMENT)) {602return true;603}604605// Child of DETAILS element is not shown unless the DETAILS element is open606// or the child is a SUMMARY element.607if (parent && bot.dom.isElement(parent, goog.dom.TagName.DETAILS) &&608!parent.open && !bot.dom.isElement(e, goog.dom.TagName.SUMMARY)) {609return false;610}611612return !!parent && displayed(parent);613}614615return bot.dom.isShown_(elem, !!opt_ignoreOpacity, displayed);616};617618619/**620* The kind of overflow area in which an element may be located. NONE if it does621* not overflow any ancestor element; HIDDEN if it overflows and cannot be622* scrolled into view; SCROLL if it overflows but can be scrolled into view.623*624* @enum {string}625*/626bot.dom.OverflowState = {627NONE: 'none',628HIDDEN: 'hidden',629SCROLL: 'scroll'630};631632633/**634* Returns the overflow state of the given element.635*636* If an optional coordinate or rectangle region is provided, returns the637* overflow state of that region relative to the element. A coordinate is638* treated as a 1x1 rectangle whose top-left corner is the coordinate.639*640* @param {!Element} elem Element.641* @param {!(goog.math.Coordinate|goog.math.Rect)=} opt_region642* Coordinate or rectangle relative to the top-left corner of the element.643* @return {bot.dom.OverflowState} Overflow state of the element.644*/645bot.dom.getOverflowState = function (elem, opt_region) {646var region = bot.dom.getClientRegion(elem, opt_region);647var ownerDoc = goog.dom.getOwnerDocument(elem);648var htmlElem = ownerDoc.documentElement;649var bodyElem = ownerDoc.body;650var htmlOverflowStyle = bot.dom.getEffectiveStyle(htmlElem, 'overflow');651var treatAsFixedPosition;652653// Return the closest ancestor that the given element may overflow.654function getOverflowParent(e) {655var position = bot.dom.getEffectiveStyle(e, 'position');656if (position == 'fixed') {657treatAsFixedPosition = true;658// Fixed-position element may only overflow the viewport.659return e == htmlElem ? null : htmlElem;660} else {661var parent = bot.dom.getParentElement(e);662while (parent && !canBeOverflowed(parent)) {663parent = bot.dom.getParentElement(parent);664}665return parent;666}667668function canBeOverflowed(container) {669// The HTML element can always be overflowed.670if (container == htmlElem) {671return true;672}673// An element cannot overflow an element with an inline or contents display style.674var containerDisplay = /** @type {string} */ (675bot.dom.getEffectiveStyle(container, 'display'));676if (goog.string.startsWith(containerDisplay, 'inline') ||677(containerDisplay == 'contents')) {678return false;679}680// An absolute-positioned element cannot overflow a static-positioned one.681if (position == 'absolute' &&682bot.dom.getEffectiveStyle(container, 'position') == 'static') {683return false;684}685return true;686}687}688689// Return the x and y overflow styles for the given element.690function getOverflowStyles(e) {691// When the <html> element has an overflow style of 'visible', it assumes692// the overflow style of the body, and the body is really overflow:visible.693var overflowElem = e;694if (htmlOverflowStyle == 'visible') {695// Note: bodyElem will be null/undefined in SVG documents.696if (e == htmlElem && bodyElem) {697overflowElem = bodyElem;698} else if (e == bodyElem) {699return { x: 'visible', y: 'visible' };700}701}702var overflow = {703x: bot.dom.getEffectiveStyle(overflowElem, 'overflow-x'),704y: bot.dom.getEffectiveStyle(overflowElem, 'overflow-y')705};706// The <html> element cannot have a genuine 'visible' overflow style,707// because the viewport can't expand; 'visible' is really 'auto'.708if (e == htmlElem) {709overflow.x = overflow.x == 'visible' ? 'auto' : overflow.x;710overflow.y = overflow.y == 'visible' ? 'auto' : overflow.y;711}712return overflow;713}714715// Returns the scroll offset of the given element.716function getScroll(e) {717if (e == htmlElem) {718return new goog.dom.DomHelper(ownerDoc).getDocumentScroll();719} else {720return new goog.math.Coordinate(e.scrollLeft, e.scrollTop);721}722}723724// Check if the element overflows any ancestor element.725for (var container = getOverflowParent(elem);726!!container;727container = getOverflowParent(container)) {728var containerOverflow = getOverflowStyles(container);729730// If the container has overflow:visible, the element cannot overflow it.731if (containerOverflow.x == 'visible' && containerOverflow.y == 'visible') {732continue;733}734735var containerRect = bot.dom.getClientRect(container);736737// Zero-sized containers without overflow:visible hide all descendants.738if (containerRect.width == 0 || containerRect.height == 0) {739return bot.dom.OverflowState.HIDDEN;740}741742// Check "underflow": if an element is to the left or above the container743var underflowsX = region.right < containerRect.left;744var underflowsY = region.bottom < containerRect.top;745if ((underflowsX && containerOverflow.x == 'hidden') ||746(underflowsY && containerOverflow.y == 'hidden')) {747return bot.dom.OverflowState.HIDDEN;748} else if ((underflowsX && containerOverflow.x != 'visible') ||749(underflowsY && containerOverflow.y != 'visible')) {750// When the element is positioned to the left or above a container, we751// have to distinguish between the element being completely outside the752// container and merely scrolled out of view within the container.753var containerScroll = getScroll(container);754var unscrollableX = region.right < containerRect.left - containerScroll.x;755var unscrollableY = region.bottom < containerRect.top - containerScroll.y;756if ((unscrollableX && containerOverflow.x != 'visible') ||757(unscrollableY && containerOverflow.x != 'visible')) {758return bot.dom.OverflowState.HIDDEN;759}760var containerState = bot.dom.getOverflowState(container);761return containerState == bot.dom.OverflowState.HIDDEN ?762bot.dom.OverflowState.HIDDEN : bot.dom.OverflowState.SCROLL;763}764765// Check "overflow": if an element is to the right or below a container766var overflowsX = region.left >= containerRect.left + containerRect.width;767var overflowsY = region.top >= containerRect.top + containerRect.height;768if ((overflowsX && containerOverflow.x == 'hidden') ||769(overflowsY && containerOverflow.y == 'hidden')) {770return bot.dom.OverflowState.HIDDEN;771} else if ((overflowsX && containerOverflow.x != 'visible') ||772(overflowsY && containerOverflow.y != 'visible')) {773// If the element has fixed position and falls outside the scrollable area774// of the document, then it is hidden.775if (treatAsFixedPosition) {776var docScroll = getScroll(container);777if ((region.left >= htmlElem.scrollWidth - docScroll.x) ||778(region.right >= htmlElem.scrollHeight - docScroll.y)) {779return bot.dom.OverflowState.HIDDEN;780}781}782// If the element can be scrolled into view of the parent, it has a scroll783// state; unless the parent itself is entirely hidden by overflow, in784// which it is also hidden by overflow.785var containerState = bot.dom.getOverflowState(container);786return containerState == bot.dom.OverflowState.HIDDEN ?787bot.dom.OverflowState.HIDDEN : bot.dom.OverflowState.SCROLL;788}789}790791// Does not overflow any ancestor.792return bot.dom.OverflowState.NONE;793};794795796/**797* A regular expression to match the CSS transform matrix syntax.798* @private {!RegExp}799* @const800*/801bot.dom.CSS_TRANSFORM_MATRIX_REGEX_ =802new RegExp('matrix\\(([\\d\\.\\-]+), ([\\d\\.\\-]+), ' +803'([\\d\\.\\-]+), ([\\d\\.\\-]+), ' +804'([\\d\\.\\-]+)(?:px)?, ([\\d\\.\\-]+)(?:px)?\\)');805806807/**808* Gets the client rectangle of the DOM element. It often returns the same value809* as Element.getBoundingClientRect, but is "fixed" for various scenarios:810* 1. Like goog.style.getClientPosition, it adjusts for the inset border in IE.811* 2. Gets a rect for <map>'s and <area>'s relative to the image using them.812* 3. Gets a rect for SVG elements representing their true bounding box.813* 4. Defines the client rect of the <html> element to be the window viewport.814*815* @param {!Element} elem The element to use.816* @return {!goog.math.Rect} The interaction box of the element.817*/818bot.dom.getClientRect = function (elem) {819var imageMap = bot.dom.maybeFindImageMap_(elem);820if (imageMap) {821return imageMap.rect;822} else if (bot.dom.isElement(elem, goog.dom.TagName.HTML)) {823// Define the client rect of the <html> element to be the viewport.824var doc = goog.dom.getOwnerDocument(elem);825var viewportSize = goog.dom.getViewportSize(goog.dom.getWindow(doc));826return new goog.math.Rect(0, 0, viewportSize.width, viewportSize.height);827} else {828var nativeRect;829try {830// TODO: in IE and Firefox, getBoundingClientRect includes stroke width,831// but getBBox does not.832nativeRect = elem.getBoundingClientRect();833} catch (e) {834// On IE < 9, calling getBoundingClientRect on an orphan element raises835// an "Unspecified Error". All other browsers return zeros.836return new goog.math.Rect(0, 0, 0, 0);837}838839var rect = new goog.math.Rect(nativeRect.left, nativeRect.top,840nativeRect.right - nativeRect.left, nativeRect.bottom - nativeRect.top);841842// In IE, the element can additionally be offset by a border around the843// documentElement or body element that we have to subtract.844if (goog.userAgent.IE && elem.ownerDocument.body) {845var doc = goog.dom.getOwnerDocument(elem);846rect.left -= doc.documentElement.clientLeft + doc.body.clientLeft;847rect.top -= doc.documentElement.clientTop + doc.body.clientTop;848}849850return rect;851}852};853854855/**856* If given a <map> or <area> element, finds the corresponding image and client857* rectangle of the element; otherwise returns null. The return value is an858* object with 'image' and 'rect' properties. When no image uses the given859* element, the returned rectangle is present but has zero size.860*861* @param {!Element} elem Element to test.862* @return {?{image: Element, rect: !goog.math.Rect}} Image and rectangle.863* @private864*/865bot.dom.maybeFindImageMap_ = function (elem) {866// If not a <map> or <area>, return null indicating so.867var isMap = bot.dom.isElement(elem, goog.dom.TagName.MAP);868if (!isMap && !bot.dom.isElement(elem, goog.dom.TagName.AREA)) {869return null;870}871872// Get the <map> associated with this element, or null if none.873var map = isMap ? elem :874(bot.dom.isElement(elem.parentNode, goog.dom.TagName.MAP) ?875elem.parentNode : null);876877var image = null, rect = null;878if (map && map.name) {879var mapDoc = goog.dom.getOwnerDocument(map);880881// TODO: Restrict to applet, img, input:image, and object nodes.882var locator = '*[usemap="#' + map.name + '"]';883884// TODO: Break dependency of bot.locators on bot.dom,885// so bot.locators.findElement can be called here instead.886image = bot.locators.css.single(locator, mapDoc);887888if (image) {889rect = bot.dom.getClientRect(image);890if (!isMap && elem.shape.toLowerCase() != 'default') {891// Shift and crop the relative area rectangle to the map.892var relRect = bot.dom.getAreaRelativeRect_(elem);893var relX = Math.min(Math.max(relRect.left, 0), rect.width);894var relY = Math.min(Math.max(relRect.top, 0), rect.height);895var w = Math.min(relRect.width, rect.width - relX);896var h = Math.min(relRect.height, rect.height - relY);897rect = new goog.math.Rect(relX + rect.left, relY + rect.top, w, h);898}899}900}901902return { image: image, rect: rect || new goog.math.Rect(0, 0, 0, 0) };903};904905906/**907* Returns the bounding box around an <area> element relative to its enclosing908* <map>. Does not apply to <area> elements with shape=='default'.909*910* @param {!Element} area Area element.911* @return {!goog.math.Rect} Bounding box of the area element.912* @private913*/914bot.dom.getAreaRelativeRect_ = function (area) {915var shape = area.shape.toLowerCase();916var coords = area.coords.split(',');917if (shape == 'rect' && coords.length == 4) {918var x = coords[0], y = coords[1];919return new goog.math.Rect(x, y, coords[2] - x, coords[3] - y);920} else if (shape == 'circle' && coords.length == 3) {921var centerX = coords[0], centerY = coords[1], radius = coords[2];922return new goog.math.Rect(centerX - radius, centerY - radius,9232 * radius, 2 * radius);924} else if (shape == 'poly' && coords.length > 2) {925var minX = coords[0], minY = coords[1], maxX = minX, maxY = minY;926for (var i = 2; i + 1 < coords.length; i += 2) {927minX = Math.min(minX, coords[i]);928maxX = Math.max(maxX, coords[i]);929minY = Math.min(minY, coords[i + 1]);930maxY = Math.max(maxY, coords[i + 1]);931}932return new goog.math.Rect(minX, minY, maxX - minX, maxY - minY);933}934return new goog.math.Rect(0, 0, 0, 0);935};936937938/**939* Gets the element's client rectangle as a box, optionally clipped to the940* given coordinate or rectangle relative to the client's position. A coordinate941* is treated as a 1x1 rectangle whose top-left corner is the coordinate.942*943* @param {!Element} elem The element.944* @param {!(goog.math.Coordinate|goog.math.Rect)=} opt_region945* Coordinate or rectangle relative to the top-left corner of the element.946* @return {!goog.math.Box} The client region box.947*/948bot.dom.getClientRegion = function (elem, opt_region) {949var region = bot.dom.getClientRect(elem).toBox();950951if (opt_region) {952var rect = opt_region instanceof goog.math.Rect ? opt_region :953new goog.math.Rect(opt_region.x, opt_region.y, 1, 1);954region.left = goog.math.clamp(955region.left + rect.left, region.left, region.right);956region.top = goog.math.clamp(957region.top + rect.top, region.top, region.bottom);958region.right = goog.math.clamp(959region.left + rect.width, region.left, region.right);960region.bottom = goog.math.clamp(961region.top + rect.height, region.top, region.bottom);962}963964return region;965};966967968/**969* Trims leading and trailing whitespace from strings, leaving non-breaking970* space characters in place.971*972* @param {string} str The string to trim.973* @return {string} str without any leading or trailing whitespace characters974* except non-breaking spaces.975* @private976*/977bot.dom.trimExcludingNonBreakingSpaceCharacters_ = function (str) {978return str.replace(/^[^\S\xa0]+|[^\S\xa0]+$/g, '');979};980981982/**983* Helper function for getVisibleText[InDisplayedDom].984* @param {!Array.<string>} lines Accumulated visible lines of text.985* @return {string} cleaned up concatenated lines986* @private987*/988bot.dom.concatenateCleanedLines_ = function (lines) {989lines = goog.array.map(990lines,991bot.dom.trimExcludingNonBreakingSpaceCharacters_);992var joined = lines.join('\n');993var trimmed = bot.dom.trimExcludingNonBreakingSpaceCharacters_(joined);994995// Replace non-breakable spaces with regular ones.996return trimmed.replace(/\xa0/g, ' ');997};9989991000/**1001* @param {!Element} elem The element to consider.1002* @return {string} visible text.1003*/1004bot.dom.getVisibleText = function (elem) {1005var lines = [];10061007if (bot.dom.IS_SHADOW_DOM_ENABLED) {1008bot.dom.appendVisibleTextLinesFromElementInComposedDom_(elem, lines);1009} else {1010bot.dom.appendVisibleTextLinesFromElement_(elem, lines);1011}1012return bot.dom.concatenateCleanedLines_(lines);1013};101410151016/**1017* Helper function used by bot.dom.appendVisibleTextLinesFromElement_ and1018* bot.dom.appendVisibleTextLinesFromElementInComposedDom_1019* @param {!Element} elem Element.1020* @param {!Array.<string>} lines Accumulated visible lines of text.1021* @param {function(!Element):boolean} isShownFn function to call to1022* tell if an element is shown1023* @param {function(!Node, !Array.<string>, boolean, ?string, ?string):void}1024* childNodeFn function to call to append lines from any child nodes1025* @private1026*/1027bot.dom.appendVisibleTextLinesFromElementCommon_ = function (1028elem, lines, isShownFn, childNodeFn) {1029function currLine() {1030return /** @type {string|undefined} */ (goog.array.peek(lines)) || '';1031}10321033// TODO: Add case here for textual form elements.1034if (bot.dom.isElement(elem, goog.dom.TagName.BR)) {1035lines.push('');1036} else {1037// TODO: properly handle display:run-in1038var isTD = bot.dom.isElement(elem, goog.dom.TagName.TD);1039var display = bot.dom.getEffectiveStyle(elem, 'display');1040// On some browsers, table cells incorrectly show up with block styles.1041var isBlock = !isTD &&1042!goog.array.contains(bot.dom.INLINE_DISPLAY_BOXES_, display);10431044// Add a newline before block elems when there is text on the current line,1045// except when the previous sibling has a display: run-in.1046// Also, do not run-in the previous sibling if this element is floated.10471048var previousElementSibling = goog.dom.getPreviousElementSibling(elem);1049var prevDisplay = (previousElementSibling) ?1050bot.dom.getEffectiveStyle(previousElementSibling, 'display') : '';1051// TODO: getEffectiveStyle should mask this for us1052var thisFloat = bot.dom.getEffectiveStyle(elem, 'float') ||1053bot.dom.getEffectiveStyle(elem, 'cssFloat') ||1054bot.dom.getEffectiveStyle(elem, 'styleFloat');1055var runIntoThis = prevDisplay == 'run-in' && thisFloat == 'none';1056if (isBlock && !runIntoThis &&1057!goog.string.isEmptyOrWhitespace(currLine())) {1058lines.push('');1059}10601061// This element may be considered unshown, but have a child that is1062// explicitly shown (e.g. this element has "visibility:hidden").1063// Nevertheless, any text nodes that are direct descendants of this1064// element will not contribute to the visible text.1065var shown = isShownFn(elem);10661067// All text nodes that are children of this element need to know the1068// effective "white-space" and "text-transform" styles to properly1069// compute their contribution to visible text. Compute these values once.1070var whitespace = null, textTransform = null;1071if (shown) {1072whitespace = bot.dom.getEffectiveStyle(elem, 'white-space');1073textTransform = bot.dom.getEffectiveStyle(elem, 'text-transform');1074}10751076goog.array.forEach(elem.childNodes, function (node) {1077childNodeFn(node, lines, shown, whitespace, textTransform);1078});10791080var line = currLine();10811082// Here we differ from standard innerText implementations (if there were1083// such a thing). Usually, table cells are separated by a tab, but we1084// normalize tabs into single spaces.1085if ((isTD || display == 'table-cell') && line &&1086!goog.string.endsWith(line, ' ')) {1087lines[lines.length - 1] += ' ';1088}10891090// Add a newline after block elems when there is text on the current line,1091// and the current element isn't marked as run-in.1092if (isBlock && display != 'run-in' &&1093!goog.string.isEmptyOrWhitespace(line)) {1094lines.push('');1095}1096}1097};109810991100/**1101* @param {!Element} elem Element.1102* @param {!Array.<string>} lines Accumulated visible lines of text.1103* @private1104*/1105bot.dom.appendVisibleTextLinesFromElement_ = function (elem, lines) {1106bot.dom.appendVisibleTextLinesFromElementCommon_(1107elem, lines, bot.dom.isShown,1108function (node, lines, shown, whitespace, textTransform) {1109if (node.nodeType == goog.dom.NodeType.TEXT && shown) {1110var textNode = /** @type {!Text} */ (node);1111bot.dom.appendVisibleTextLinesFromTextNode_(textNode, lines,1112whitespace, textTransform);1113} else if (bot.dom.isElement(node)) {1114var castElem = /** @type {!Element} */ (node);1115bot.dom.appendVisibleTextLinesFromElement_(castElem, lines);1116}1117});1118};111911201121/**1122* Elements with one of these effective "display" styles are treated as inline1123* display boxes and have their visible text appended to the current line.1124* @private {!Array.<string>}1125* @const1126*/1127bot.dom.INLINE_DISPLAY_BOXES_ = [1128'inline',1129'inline-block',1130'inline-table',1131'none',1132'table-cell',1133'table-column',1134'table-column-group'1135];113611371138/**1139* @param {!Text} textNode Text node.1140* @param {!Array.<string>} lines Accumulated visible lines of text.1141* @param {?string} whitespace Parent element's "white-space" style.1142* @param {?string} textTransform Parent element's "text-transform" style.1143* @private1144*/1145bot.dom.appendVisibleTextLinesFromTextNode_ = function (textNode, lines,1146whitespace, textTransform) {11471148// First, remove zero-width characters. Do this before regularizing spaces as1149// the zero-width space is both zero-width and a space, but we do not want to1150// make it visible by converting it to a regular space.1151// The replaced characters are:1152// U+200B: Zero-width space1153// U+200E: Left-to-right mark1154// U+200F: Right-to-left mark1155var text = textNode.nodeValue.replace(/[\u200b\u200e\u200f]/g, '');11561157// Canonicalize the new lines, and then collapse new lines1158// for the whitespace styles that collapse. See:1159// https://developer.mozilla.org/en/CSS/white-space1160text = goog.string.canonicalizeNewlines(text);1161if (whitespace == 'normal' || whitespace == 'nowrap') {1162text = text.replace(/\n/g, ' ');1163}11641165// For pre and pre-wrap whitespace styles, convert all breaking spaces to be1166// non-breaking, otherwise, collapse all breaking spaces. Breaking spaces are1167// converted to regular spaces by getVisibleText().1168if (whitespace == 'pre' || whitespace == 'pre-wrap') {1169text = text.replace(/[ \f\t\v\u2028\u2029]/g, '\xa0');1170} else {1171text = text.replace(/[\ \f\t\v\u2028\u2029]+/g, ' ');1172}11731174if (textTransform == 'capitalize') {1175// the unicode regex ending with /gu does not work in IE1176var re = goog.userAgent.IE ? /(^|\s|\b)(\S)/g : /(^|\s|\b)(\S)/gu;1177text = text.replace(re, function () {1178return arguments[1] + arguments[2].toUpperCase();1179});1180} else if (textTransform == 'uppercase') {1181text = text.toUpperCase();1182} else if (textTransform == 'lowercase') {1183text = text.toLowerCase();1184}11851186var currLine = lines.pop() || '';1187if (goog.string.endsWith(currLine, ' ') &&1188goog.string.startsWith(text, ' ')) {1189text = text.substr(1);1190}1191lines.push(currLine + text);1192};119311941195/**1196* Gets the opacity of a node (x-browser).1197* This gets the inline style opacity of the node and takes into account the1198* cascaded or the computed style for this node.1199*1200* @param {!Element} elem Element whose opacity has to be found.1201* @return {number} Opacity between 0 and 1.1202*/1203bot.dom.getOpacity = function (elem) {1204// TODO: Does this need to deal with rgba colors?1205if (!bot.userAgent.IE_DOC_PRE9) {1206return bot.dom.getOpacityNonIE_(elem);1207} else {1208if (bot.dom.getEffectiveStyle(elem, 'position') == 'relative') {1209// Filter does not apply to non positioned elements.1210return 1;1211}12121213var opacityStyle = bot.dom.getEffectiveStyle(elem, 'filter');1214var groups = opacityStyle.match(/^alpha\(opacity=(\d*)\)/) ||1215opacityStyle.match(1216/^progid:DXImageTransform.Microsoft.Alpha\(Opacity=(\d*)\)/);12171218if (groups) {1219return Number(groups[1]) / 100;1220} else {1221return 1; // Opaque.1222}1223}1224};122512261227/**1228* Implementation of getOpacity for browsers that do support1229* the "opacity" style.1230*1231* @param {!Element} elem Element whose opacity has to be found.1232* @return {number} Opacity between 0 and 1.1233* @private1234*/1235bot.dom.getOpacityNonIE_ = function (elem) {1236// By default the element is opaque.1237var elemOpacity = 1;12381239var opacityStyle = bot.dom.getEffectiveStyle(elem, 'opacity');1240if (opacityStyle) {1241elemOpacity = Number(opacityStyle);1242}12431244// Let's apply the parent opacity to the element.1245var parentElement = bot.dom.getParentElement(elem);1246if (parentElement) {1247elemOpacity = elemOpacity * bot.dom.getOpacityNonIE_(parentElement);1248}1249return elemOpacity;1250};125112521253/**1254* Returns the display parent element of the given node, or null. This method1255* differs from bot.dom.getParentElement in the presence of ShadowDOM and1256* <shadow> or <content> tags. For example if1257* <ul>1258* <li>div A contains div B1259* <li>div B has a css class .C1260* <li>div A contains a Shadow DOM with a div D1261* <li>div D contains a contents tag selecting all items of class .C1262* </ul>1263* then calling bot.dom.getParentElement on B will return A, but calling1264* getDisplayParentElement on B will return D.1265*1266* @param {!Node} node The node whose parent is desired.1267* @return {Node} The parent node, if available, null otherwise.1268*/1269bot.dom.getParentNodeInComposedDom = function (node) {1270var /**@type {Node}*/ parent = node.parentNode;12711272// Shadow DOM v11273if (parent && parent.shadowRoot && node.assignedSlot !== undefined) {1274// Can be null on purpose, meaning it has no parent as1275// it hasn't yet been slotted1276return node.assignedSlot ? node.assignedSlot.parentNode : null;1277}12781279// Shadow DOM V0 (deprecated)1280if (node.getDestinationInsertionPoints) {1281var destinations = node.getDestinationInsertionPoints();1282if (destinations.length > 0) {1283return destinations[destinations.length - 1];1284}1285}12861287return parent;1288};128912901291/**1292* @param {!Node} node Node.1293* @param {!Array.<string>} lines Accumulated visible lines of text.1294* @param {boolean} shown whether the node is visible1295* @param {?string} whitespace the node's 'white-space' effectiveStyle1296* @param {?string} textTransform the node's 'text-transform' effectiveStyle1297* @private1298* @suppress {missingProperties}1299*/1300bot.dom.appendVisibleTextLinesFromNodeInComposedDom_ = function (1301node, lines, shown, whitespace, textTransform) {13021303if (node.nodeType == goog.dom.NodeType.TEXT && shown) {1304var textNode = /** @type {!Text} */ (node);1305bot.dom.appendVisibleTextLinesFromTextNode_(textNode, lines,1306whitespace, textTransform);1307} else if (bot.dom.isElement(node)) {1308var castElem = /** @type {!Element} */ (node);13091310if (bot.dom.isElement(node, 'CONTENT') || bot.dom.isElement(node, 'SLOT')) {1311var parentNode = node;1312while (parentNode.parentNode) {1313parentNode = parentNode.parentNode;1314}1315if (parentNode instanceof ShadowRoot) {1316// If the element is <content> and we're inside a shadow DOM then just1317// append the contents of the nodes that have been distributed into it.1318var contentElem = /** @type {!Object} */ (node);1319var shadowChildren;1320if (bot.dom.isElement(node, 'CONTENT')) {1321shadowChildren = contentElem.getDistributedNodes();1322} else {1323shadowChildren = contentElem.assignedNodes();1324}1325const childrenToTraverse =1326shadowChildren.length > 0 ? shadowChildren : contentElem.childNodes;1327goog.array.forEach(childrenToTraverse, function (node) {1328bot.dom.appendVisibleTextLinesFromNodeInComposedDom_(1329node, lines, shown, whitespace, textTransform);1330});1331} else {1332// if we're not inside a shadow DOM, then we just treat <content>1333// as an unknown element and use anything inside the tag1334bot.dom.appendVisibleTextLinesFromElementInComposedDom_(1335castElem, lines);1336}1337} else if (bot.dom.isElement(node, 'SHADOW')) {1338// if the element is <shadow> then find the owning shadowRoot1339var parentNode = node;1340while (parentNode.parentNode) {1341parentNode = parentNode.parentNode;1342}1343if (parentNode instanceof ShadowRoot) {1344var thisShadowRoot = /** @type {!ShadowRoot} */ (parentNode);1345if (thisShadowRoot) {1346// then go through the owning shadowRoots older siblings and append1347// their contents1348var olderShadowRoot = thisShadowRoot.olderShadowRoot;1349while (olderShadowRoot) {1350goog.array.forEach(1351olderShadowRoot.childNodes, function (childNode) {1352bot.dom.appendVisibleTextLinesFromNodeInComposedDom_(1353childNode, lines, shown, whitespace, textTransform);1354});1355olderShadowRoot = olderShadowRoot.olderShadowRoot;1356}1357}1358}1359} else {1360// otherwise append the contents of an element as per normal.1361bot.dom.appendVisibleTextLinesFromElementInComposedDom_(1362castElem, lines);1363}1364}1365};136613671368/**1369* Determines whether a given node has been distributed into a ShadowDOM1370* element somewhere.1371* @param {!Node} node The node to check1372* @return {boolean} True if the node has been distributed.1373*/1374bot.dom.isNodeDistributedIntoShadowDom = function (node) {1375var elemOrText = null;1376if (node.nodeType == goog.dom.NodeType.ELEMENT) {1377elemOrText = /** @type {!Element} */ (node);1378} else if (node.nodeType == goog.dom.NodeType.TEXT) {1379elemOrText = /** @type {!Text} */ (node);1380}1381return elemOrText != null &&1382(elemOrText.assignedSlot != null ||1383(elemOrText.getDestinationInsertionPoints &&1384elemOrText.getDestinationInsertionPoints().length > 0)1385);1386};138713881389/**1390* @param {!Element} elem Element.1391* @param {!Array.<string>} lines Accumulated visible lines of text.1392* @private1393*/1394bot.dom.appendVisibleTextLinesFromElementInComposedDom_ = function (1395elem, lines) {1396if (elem.shadowRoot) {1397goog.array.forEach(elem.shadowRoot.childNodes, function (node) {1398bot.dom.appendVisibleTextLinesFromNodeInComposedDom_(1399node, lines, true, null, null);1400});1401}14021403bot.dom.appendVisibleTextLinesFromElementCommon_(1404elem, lines, bot.dom.isShown,1405function (node, lines, shown, whitespace, textTransform) {1406// If the node has been distributed into a shadowDom element1407// to be displayed elsewhere, then we shouldn't append1408// its contents here).1409if (!bot.dom.isNodeDistributedIntoShadowDom(node)) {1410bot.dom.appendVisibleTextLinesFromNodeInComposedDom_(1411node, lines, shown, whitespace, textTransform);1412}1413});1414};141514161417