blob: 58c873c55e3b3c9fcb361fe828bf64dfd21e0820 [file] [log] [blame] [edit]
// Copyright 2012 Software Freedom Conservancy
// Copyright 2010 WebDriver committers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview DOM manipulation and querying routines.
*/
goog.provide('bot.dom');
goog.require('bot');
goog.require('bot.color');
goog.require('bot.locators.xpath');
goog.require('bot.userAgent');
goog.require('bot.window');
goog.require('goog.array');
goog.require('goog.dom');
goog.require('goog.dom.NodeType');
goog.require('goog.dom.TagName');
goog.require('goog.math.Box');
goog.require('goog.math.Coordinate');
goog.require('goog.math.Rect');
goog.require('goog.string');
goog.require('goog.style');
goog.require('goog.userAgent');
/**
* 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) {
return goog.dom.getActiveElement(
goog.dom.getOwnerDocument(nodeOrWindow));
};
/**
* Returns whether the given node is an element and, optionally, whether it has
* the given tag name. If the tag name is not provided, returns true if the node
* is an element, regardless of the tag name.h
*
* @param {Node} node The node to test.
* @param {string=} opt_tagName Tag name to test the node for.
* @return {boolean} Whether the node is an element with the given tag name.
*/
bot.dom.isElement = function(node, opt_tagName) {
return !!node && node.nodeType == goog.dom.NodeType.ELEMENT &&
(!opt_tagName || node.tagName.toUpperCase() == opt_tagName);
};
/**
* 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.OPERA ||
(goog.userAgent.GECKO && !bot.userAgent.isEngineVersion('1.9.2'))) {
// Don't support pointer events
return false;
}
return bot.dom.getEffectiveStyle(element, 'pointer-events') == 'none';
};
/**
* Returns whether the element can be checked or selected.
*
* @param {!Element} element The element to check.
* @return {boolean} Whether the element could be checked or selected.
*/
bot.dom.isSelectable = function(element) {
if (bot.dom.isElement(element, goog.dom.TagName.OPTION)) {
return true;
}
if (bot.dom.isElement(element, goog.dom.TagName.INPUT)) {
var type = element.type.toLowerCase();
return type == 'checkbox' || type == 'radio';
}
return false;
};
/**
* Returns whether the element is checked or selected.
*
* @param {!Element} element The element to check.
* @return {boolean} Whether the element is checked or selected.
*/
bot.dom.isSelected = function(element) {
if (!bot.dom.isSelectable(element)) {
throw new bot.Error(bot.ErrorCode.ELEMENT_NOT_SELECTABLE,
'Element is not selectable');
}
var propertyName = 'selected';
var type = element.type && element.type.toLowerCase();
if ('checkbox' == type || 'radio' == type) {
propertyName = 'checked';
}
return !!bot.dom.getProperty(element, propertyName);
};
/**
* List of the focusable fields, according to
* http://www.w3.org/TR/html401/interact/scripts.html#adef-onfocus
* @type {!Array.<!goog.dom.TagName>}
* @const
* @private
*/
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 or has a positive tabindex.
* @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_, function(tagName) {
return element.tagName.toUpperCase() == tagName;
}) || (bot.dom.getAttribute(element, 'tabindex') != null &&
Number(bot.dom.getProperty(element, 'tabIndex')) >= 0);
};
/**
* Looks up the given property (not to be confused with an attribute) on the
* given element.
*
* @param {!Element} element The element to use.
* @param {string} propertyName The name of the property.
* @return {*} The value of the property.
*/
bot.dom.getProperty = function(element, propertyName) {
// When an <option>'s value attribute is not set, its value property should be
// its text content, but IE < 8 does not adhere to that behavior, so fix it.
// http://www.w3.org/TR/1999/REC-html401-19991224/interact/forms.html#adef-value-OPTION
if (bot.userAgent.IE_DOC_PRE8 && propertyName == 'value' &&
bot.dom.isElement(element, goog.dom.TagName.OPTION) &&
goog.isNull(bot.dom.getAttribute(element, 'value'))) {
return goog.dom.getRawTextContent(element);
}
return element[propertyName];
};
/**
* Regex to split on semicolons, but not when enclosed in parens or quotes.
* Helper for {@link bot.dom.standardizeStyleAttribute_}.
* If the style attribute ends with a semicolon this will include an empty
* string at the end of the array
* @type {!RegExp}
* @const
* @private
*/
bot.dom.SPLIT_STYLE_ATTRIBUTE_ON_SEMICOLONS_REGEXP_ =
new RegExp('[;]+' +
'(?=(?:(?:[^"]*"){2})*[^"]*$)' +
'(?=(?:(?:[^\']*\'){2})*[^\']*$)' +
'(?=(?:[^()]*\\([^()]*\\))*[^()]*$)');
/**
* Standardize a style attribute value, which includes:
* (1) converting all property names lowercase
* (2) ensuring it ends in a trailing semi-colon
* (3) removing empty style values (which only appear on Opera).
* @param {string} value The style attribute value.
* @return {string} The identical value, with the formatting rules described
* above applied.
* @private
*/
bot.dom.standardizeStyleAttribute_ = function(value) {
var styleArray = value.split(
bot.dom.SPLIT_STYLE_ATTRIBUTE_ON_SEMICOLONS_REGEXP_);
var css = [];
goog.array.forEach(styleArray, function(pair) {
var i = pair.indexOf(':');
if (i > 0) {
var keyValue = [pair.slice(0, i), pair.slice(i + 1)];
if (keyValue.length == 2) {
css.push(keyValue[0].toLowerCase(), ':', keyValue[1], ';');
}
}
});
css = css.join('');
css = css.charAt(css.length - 1) == ';' ? css : css + ';';
return goog.userAgent.OPERA ? css.replace(/\w+:;/g, '') : css;
};
/**
* Get the user-specified value of the given attribute of the element, or null
* if the attribute is not present.
*
* <p>For boolean attributes such as "selected" or "checked", this method
* returns the value of element.getAttribute(attributeName) cast to a String
* when attribute is present. For modern browsers, this will be the string the
* attribute is given in the HTML, but for IE8 it will be the name of the
* attribute, and for IE7, it will be the string "true". To test whether a
* boolean attribute is present, test whether the return value is non-null, the
* same as one would for non-boolean attributes. Specifically, do *not* test
* whether the boolean evaluation of the return value is true, because the value
* of a boolean attribute that is present will often be the empty string.
*
* <p>For the style attribute, it standardizes the value by lower-casing the
* property names and always including a trailing semi-colon.
*
* @param {!Element} element The element to use.
* @param {string} attributeName The name of the attribute to return.
* @return {?string} The value of the attribute or "null" if entirely missing.
*/
bot.dom.getAttribute = function(element, attributeName) {
attributeName = attributeName.toLowerCase();
// The style attribute should be a css text string that includes only what
// the HTML element specifies itself (excluding what is inherited from parent
// elements or style sheets). We standardize the format of this string via the
// standardizeStyleAttribute method.
if (attributeName == 'style') {
return bot.dom.standardizeStyleAttribute_(element.style.cssText);
}
// In IE doc mode < 8, the "value" attribute of an <input> is only accessible
// as a property.
if (bot.userAgent.IE_DOC_PRE8 && attributeName == 'value' &&
bot.dom.isElement(element, goog.dom.TagName.INPUT)) {
return element['value'];
}
// In IE < 9, element.getAttributeNode will return null for some boolean
// attributes that are present, such as the selected attribute on <option>
// elements. This if-statement is sufficient if these cases are restricted
// to boolean attributes whose reflected property names are all lowercase
// (as attributeName is by this point), like "selected". We have not
// found a boolean attribute for which this does not work.
if (bot.userAgent.IE_DOC_PRE9 && element[attributeName] === true) {
return String(element.getAttribute(attributeName));
}
// When the attribute is not present, either attr will be null or
// attr.specified will be false.
var attr = element.getAttributeNode(attributeName);
return (attr && attr.specified) ? attr.value : null;
};
/**
* List of elements that support the "disabled" attribute, as defined by the
* HTML 4.01 specification.
* @type {!Array.<goog.dom.TagName>}
* @const
* @private
* @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 tagName = el.tagName.toUpperCase();
if (!goog.array.contains(bot.dom.DISABLED_ATTRIBUTE_SUPPORTED_, tagName)) {
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 &&
goog.dom.TagName.OPTGROUP == tagName ||
goog.dom.TagName.OPTION == tagName) {
return bot.dom.isEnabled((/**@type{!Element}*/el.parentNode));
}
return true;
};
/**
* List of input types that create text fields.
* @type {!Array.<String>}
* @const
* @private
* @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(gdennis): 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 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(gdennis): 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.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 in selector-case.
* @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(propertyName, 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;
};
/**
* Would a user see scroll bars on the BODY element? In the case where the BODY
* has "overflow: hidden", and HTML has "overflow: auto" or "overflow: scroll"
* set, there's a scroll bar, so it's as if the BODY has "overflow: auto" set.
* In all other cases where BODY has "overflow: hidden", there are no
* scrollbars. http://www.w3.org/TR/CSS21/visufx.html#overflow
*
* @param {!Element} bodyElement The element, which must be a BODY element.
* @return {boolean} Whether scrollbars would be visible to a user.
* @private
*/
bot.dom.isBodyScrollBarShown_ = function(bodyElement) {
if (!bot.dom.isElement(bodyElement, goog.dom.TagName.BODY)) {
// bail
}
var bodyOverflow = bot.dom.getEffectiveStyle(bodyElement, 'overflow');
if (bodyOverflow != 'hidden') {
return true;
}
var html = bot.dom.getParentElement(bodyElement);
if (!html || !bot.dom.isElement(html, goog.dom.TagName.HTML)) {
return true; // Seems like a reasonable default.
}
var viewportOverflow = bot.dom.getEffectiveStyle(html, 'overflow');
return viewportOverflow == 'auto' || viewportOverflow == 'scroll';
};
/**
* @param {!Element} element The element to use.
* @return {!goog.math.Size} The dimensions of the element.
*/
bot.dom.getElementSize = function(element) {
if (goog.isFunction(element['getBBox'])) {
try {
var bb = element['getBBox']();
if (bb) {
// Opera will return an undefined bounding box for SVG elements.
// Which makes sense, but isn't useful.
return bb;
}
} catch (e) {
// Firefox will always throw for certain SVG elements,
// even if the function exists.
}
}
// If the element is the BODY, then get the visible size.
if (bot.dom.isElement(element, goog.dom.TagName.BODY)) {
var doc = goog.dom.getOwnerDocument(element);
var win = goog.dom.getWindow(doc) || undefined;
if (!bot.dom.isBodyScrollBarShown_(element)) {
return goog.dom.getViewportSize(win);
}
return bot.window.getInteractableSize(win);
}
return goog.style.getSize(element);
};
/**
* 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.
*
* @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) {
if (!bot.dom.isElement(elem)) {
throw new Error('Argument to isShown must be of type Element');
}
// 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, /*ignoreOpacity=*/true);
}
// Map is shown iff image that uses it is shown.
if (bot.dom.isElement(elem, goog.dom.TagName.MAP)) {
if (!elem.name) {
return false;
}
var mapDoc = goog.dom.getOwnerDocument(elem);
var mapImage;
// TODO(gdennis): Avoid brute-force search once a cross-browser xpath
// locator is available.
if (mapDoc['evaluate']) {
// The "//*" XPath syntax can confuse the closure compiler, so we use
// the "/descendant::*" syntax instead.
// TODO(jleyba): Try to find a reproducible case for the compiler bug.
// TODO(jleyba): Restrict to applet, img, input:image, and object nodes.
var imageXpath = '/descendant::*[@usemap = "#' + elem.name + '"]';
// TODO(gdennis): Break dependency of bot.locators on bot.dom,
// so bot.locators.findElement can be called here instead.
mapImage = bot.locators.xpath.single(imageXpath, mapDoc);
} else {
mapImage = goog.dom.findNode(mapDoc, function(n) {
return bot.dom.isElement(n) &&
bot.dom.getAttribute(
/** @type {!Element} */ (n), 'usemap') == '#' + elem.name;
});
}
return !!mapImage && bot.dom.isShown((/** @type {!Element} */ mapImage),
opt_ignoreOpacity);
}
// Area is shown iff enclosing map is shown.
if (bot.dom.isElement(elem, goog.dom.TagName.AREA)) {
var map = /**@type {Element}*/ (goog.dom.getAncestor(elem, function(e) {
return bot.dom.isElement(e, goog.dom.TagName.MAP);
}));
return !!map && bot.dom.isShown(map, opt_ignoreOpacity);
}
// 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 visibility is not shown.
if (bot.dom.getEffectiveStyle(elem, 'visibility') == 'hidden') {
return false;
}
// Any element with a display style equal to 'none' or that has an ancestor
// with display style equal to 'none' is not shown.
function displayed(e) {
if (bot.dom.getEffectiveStyle(e, 'display') == 'none') {
return false;
}
var parent = bot.dom.getParentElement(e);
return !parent || displayed(parent);
}
if (!displayed(elem)) {
return false;
}
// Any transparent element is not shown.
if (!opt_ignoreOpacity && bot.dom.getOpacity(elem) == 0) {
return false;
}
// Any element without positive size dimensions is not shown.
function positiveSize(e) {
var size = bot.dom.getElementSize(e);
if (size.height > 0 && size.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') && (size.height > 0 || size.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.
return 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 should be hidden if their parent has a fixed size AND has the
// style overflow:hidden AND the element's location is not within the fixed
// size of the parent
function isOverflowHiding(e) {
var parent = goog.style.getOffsetParent(e);
var parentNode = goog.userAgent.GECKO || goog.userAgent.IE ||
goog.userAgent.OPERA ? bot.dom.getParentElement(e) : parent;
// Gecko will skip the BODY tag when calling getOffsetParent. However, the
// combination of the overflow values on the BODY _and_ HTML tags determine
// whether scroll bars are shown, so we need to guarantee that both values
// are checked.
if ((goog.userAgent.GECKO || goog.userAgent.IE || goog.userAgent.OPERA) &&
bot.dom.isElement(parentNode, goog.dom.TagName.BODY)) {
parent = parentNode;
}
if (parent && bot.dom.getEffectiveStyle(parent, 'overflow') == 'hidden') {
var sizeOfParent = bot.dom.getElementSize(parent);
var locOfParent = goog.style.getClientPosition(parent);
var locOfElement = goog.style.getClientPosition(e);
if (locOfParent.x + sizeOfParent.width < locOfElement.x) {
return false;
}
if (locOfParent.y + sizeOfParent.height < locOfElement.y) {
return false;
}
return isOverflowHiding(parent);
}
return true;
}
if (!isOverflowHiding(elem)) {
return false;
}
function isTransformHiding(e) {
var transform = bot.dom.getEffectiveStyle(e, '-o-transform') ||
bot.dom.getEffectiveStyle(e, '-webkit-transform') ||
bot.dom.getEffectiveStyle(e, '-ms-transform') ||
bot.dom.getEffectiveStyle(e, '-moz-transform') ||
bot.dom.getEffectiveStyle(e, 'transform');
// Not all browsers know what a transform is so if we have a returned value
// lets carry on checking up the tree just in case. If we ask for the
// transform matrix and look at the details there it will return the centre
// of the element
if (transform && transform !== "none") {
var locOfElement = goog.style.getClientPosition(e);
if ((locOfElement.x) >= 0 &&
(locOfElement.y) >= 0){
return true;
} else {
return false;
}
} else {
var parent = bot.dom.getParentElement(e);
return !parent || isTransformHiding(parent);
}
}
return isTransformHiding(elem);
};
/**
* 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, '');
};
/**
* @param {!Element} elem The element to consider.
* @return {string} visible text.
*/
bot.dom.getVisibleText = function(elem) {
var lines = [];
bot.dom.appendVisibleTextLinesFromElement_(elem, 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 Element.
* @param {!Array.<string>} lines Accumulated visible lines of text.
* @private
*/
bot.dom.appendVisibleTextLinesFromElement_ = function(elem, lines) {
function currLine() {
return (/** @type {string|undefined} */ goog.array.peek(lines)) || '';
}
// TODO(gdennis): 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(dawagner): 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.isEmpty(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 = bot.dom.isShown(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) {
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);
}
});
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.isEmpty(line)) {
lines.push('');
}
}
};
/**
* Elements with one of these effective "display" styles are treated as inline
* display boxes and have their visible text appended to the current line.
* @type {!Array.<string>}
* @private
* @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, replace all zero-width spaces. Do this before regularizing spaces
// as the zero-width space is, by definition, a space.
var text = textNode.nodeValue.replace(/\u200b/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') {
text = text.replace(/(^|\s)(\S)/g, 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(bsilverberg): Does this need to deal with rgba colors?
if (!bot.userAgent.IE_DOC_PRE10) {
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;
};
/**
* This function calculates the amount of scrolling necessary to bring the
* target location into view.
*
* @param {number} targetLocation The target location relative to the current
* viewport.
* @param {number} viewportDimension The size of the current viewport.
* @return {number} Returns the scroll offset necessary to bring the given
* target location into view.
* @private
*/
bot.dom.calculateViewportScrolling_ =
function(targetLocation, viewportDimension) {
if (targetLocation >= viewportDimension) {
// Scroll until the target location appears on the right/bottom side of
// the viewport.
return targetLocation - (viewportDimension - 1);
}
if (targetLocation < 0) {
// Scroll until the target location appears on the left/top side of the
// viewport.
return targetLocation;
}
// The location is already within the viewport. No scrolling necessary.
return 0;
};
/**
* This function takes a relative location according to the current viewport. If
* this location is not visible in the viewport, it scrolls the location into
* view. The function returns the new relative location after scrolling.
*
* @param {!goog.math.Coordinate} targetLocation The target location relative
* to (0, 0) coordinate of the viewport.
* @param {Window=} opt_currentWindow The current browser window.
* @return {!goog.math.Coordinate} The target location within the viewport
* after scrolling.
*/
bot.dom.getInViewLocation =
function(targetLocation, opt_currentWindow) {
var currentWindow = opt_currentWindow || bot.getWindow();
var viewportSize = goog.dom.getViewportSize(currentWindow);
var xScrolling = bot.dom.calculateViewportScrolling_(
targetLocation.x,
viewportSize.width);
var yScrolling = bot.dom.calculateViewportScrolling_(
targetLocation.y,
viewportSize.height);
var scrollOffset =
goog.dom.getDomHelper(currentWindow.document).getDocumentScroll();
if (xScrolling != 0 || yScrolling != 0) {
currentWindow.scrollBy(xScrolling, yScrolling);
}
// It is difficult to determine the size of the web page in some browsers.
// We check if the scrolling we intended to do really happened. If not we
// assume that the target location is not on the web page.
var newScrollOffset =
goog.dom.getDomHelper(currentWindow.document).getDocumentScroll();
if ((scrollOffset.x + xScrolling != newScrollOffset.x) ||
(scrollOffset.y + yScrolling != newScrollOffset.y)) {
throw new bot.Error(bot.ErrorCode.MOVE_TARGET_OUT_OF_BOUNDS,
'The target location (' + (targetLocation.x + scrollOffset.x) +
', ' + (targetLocation.y + scrollOffset.y) + ') is not on the ' +
'webpage.');
}
var inViewLocation = new goog.math.Coordinate(
targetLocation.x - xScrolling,
targetLocation.y - yScrolling);
// The target location should be within the viewport after scrolling.
// This is assertion code. We do not expect them ever to become true.
if (0 > inViewLocation.x || inViewLocation.x >= viewportSize.width) {
throw new bot.Error(bot.ErrorCode.MOVE_TARGET_OUT_OF_BOUNDS,
'The target location (' +
inViewLocation.x + ', ' + inViewLocation.y +
') should be within the viewport (' +
viewportSize.width + ':' + viewportSize.height +
') after scrolling.');
}
if (0 > inViewLocation.y || inViewLocation.y >= viewportSize.height) {
throw new bot.Error(bot.ErrorCode.MOVE_TARGET_OUT_OF_BOUNDS,
'The target location (' +
inViewLocation.x + ', ' + inViewLocation.y +
') should be within the viewport (' +
viewportSize.width + ':' + viewportSize.height +
') after scrolling.');
}
return inViewLocation;
};
/**
* Scrolls the scrollable element so that the region is fully visible.
* If the region is too large, it will be aligned to the top-left of the
* scrollable element. The region should be relative to the scrollable
* element's current scroll position.
*
* @param {!goog.math.Rect} region The region to use.
* @param {!Element} scrollable The scrollable element to scroll.
* @private
*/
bot.dom.scrollRegionIntoView_ = function(region, scrollable) {
scrollable.scrollLeft += Math.min(
region.left, Math.max(region.left - region.width, 0));
scrollable.scrollTop += Math.min(
region.top, Math.max(region.top - region.height, 0));
};
/**
* Scrolls the region of an element into the container's view. If the
* region is too large to fit in the view, it will be aligned to the
* top-left of the container.
*
* The element and container should be attached to the current document.
*
* @param {!Element} elem The element to use.
* @param {!goog.math.Rect} elemRegion The region relative to the element to be
* scrolled into view.
* @param {!Element} container A container of the given element.
* @private
*/
bot.dom.scrollElementRegionIntoContainerView_ = function(elem, elemRegion,
container) {
// Based largely from goog.style.scrollIntoContainerView.
var elemPos = goog.style.getPageOffset(elem);
var containerPos = goog.style.getPageOffset(container);
var containerBorder = goog.style.getBorderBox(container);
// Relative pos. of the element's border box to the container's content box.
var relX = elemPos.x + elemRegion.left - containerPos.x -
containerBorder.left;
var relY = elemPos.y + elemRegion.top - containerPos.y - containerBorder.top;
// How much the element can move in the container.
var spaceX = container.clientWidth - elemRegion.width;
var spaceY = container.clientHeight - elemRegion.height;
bot.dom.scrollRegionIntoView_(new goog.math.Rect(relX, relY, spaceX, spaceY),
container);
};
/**
* Scrolls the element into the client's view. If the element or region is
* too large to fit in the view, it will be aligned to the top-left of the
* container.
*
* The element should be attached to the current document.
*
* @param {!Element} elem The element to use.
* @param {!goog.math.Rect} elemRegion The region relative to the element to be
* scrolled into view.
*/
bot.dom.scrollElementRegionIntoClientView = function(elem, elemRegion) {
var doc = goog.dom.getOwnerDocument(elem);
// Scroll the containers.
for (var container = bot.dom.getParentElement(elem);
container && container != doc.body && container != doc.documentElement;
container = bot.dom.getParentElement(container)) {
bot.dom.scrollElementRegionIntoContainerView_(elem, elemRegion, container);
}
// Scroll the actual window.
var elemPageOffset = goog.style.getPageOffset(elem);
var viewportSize = goog.dom.getDomHelper(doc).getViewportSize();
var region = new goog.math.Rect(
elemPageOffset.x + elemRegion.left - doc.body.scrollLeft,
elemPageOffset.y + elemRegion.top - doc.body.scrollTop,
viewportSize.width - elemRegion.width,
viewportSize.height - elemRegion.height);
bot.dom.scrollRegionIntoView_(region, doc.body || doc.documentElement);
};
/**
* Scrolls the element into the client's view and returns its position
* relative to the client viewport. If the element or region is too
* large to fit in the view, it will be aligned to the top-left of the
* container.
*
* The element should be attached to the current document.
*
* @param {!Element} elem The element to use.
* @param {!goog.math.Rect=} opt_elemRegion The region relative to the element
* to be scrolled into view.
* @return {!goog.math.Coordinate} The coordinate of the element in client
* space.
*/
bot.dom.getLocationInView = function(elem, opt_elemRegion) {
var elemRegion;
if (opt_elemRegion) {
elemRegion = new goog.math.Rect(
opt_elemRegion.left, opt_elemRegion.top,
opt_elemRegion.width, opt_elemRegion.height);
} else {
elemRegion = new goog.math.Rect(0, 0, elem.offsetWidth, elem.offsetHeight);
}
bot.dom.scrollElementRegionIntoClientView(elem, elemRegion);
// This is needed for elements that are split across multiple lines.
var rect = elem.getClientRects ? elem.getClientRects()[0] : null;
var elemClientPos = rect ?
new goog.math.Coordinate(rect.left, rect.top) :
goog.style.getClientPosition(elem);
return new goog.math.Coordinate(elemClientPos.x + elemRegion.left,
elemClientPos.y + elemRegion.top);
};
/**
* Checks whether the element is currently scrolled in to view, such that the
* offset given, relative to the top-left corner of the element, is currently
* displayed in the viewport.
*
* @param {!Element} element The element to check.
* @param {!goog.math.Coordinate=} opt_coords Coordinate in the element,
* relative to the top-left corner of the element, to check. If none are
* specified, checks that any part of the element is in view.
* @return {boolean} Whether the coordinates specified, relative to the element,
* are scrolled in to view.
*/
bot.dom.isScrolledIntoView = function(element, opt_coords) {
var ownerWindow = goog.dom.getWindow(goog.dom.getOwnerDocument(element));
var topWindow = ownerWindow.top;
var elSize = goog.style.getSize(element);
for (var win = ownerWindow;; win = win.parent) {
var scroll = goog.dom.getDomHelper(win.document).getDocumentScroll();
var size = goog.dom.getViewportSize(win);
var viewportRect = new goog.math.Rect(scroll.x,
scroll.y,
size.width,
size.height);
var elCoords = goog.style.getFramedPageOffset(element, win);
var elementRect = new goog.math.Rect(elCoords.x,
elCoords.y,
elSize.width,
elSize.height);
if (!goog.math.Rect.intersects(viewportRect, elementRect)) {
return false;
}
if (win == topWindow) {
break;
}
}
var visibleBox = goog.style.getVisibleRectForElement(element);
if (!visibleBox) {
return false;
}
if (opt_coords) {
var elementOffset = goog.style.getPageOffset(element);
var desiredPoint = goog.math.Coordinate.sum(elementOffset, opt_coords);
return visibleBox.contains(desiredPoint);
} else {
var elementBox = goog.style.getBounds(element).toBox();
return goog.math.Box.intersects(visibleBox, elementBox);
}
};