2015-04-04 09:53:59 -07:00
|
|
|
// 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
|
2014-11-12 19:33:37 +00:00
|
|
|
//
|
2015-04-04 09:53:59 -07:00
|
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
2014-11-12 19:33:37 +00:00
|
|
|
//
|
2015-04-04 09:53:59 -07:00
|
|
|
// 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.
|
2010-06-22 18:52:07 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @fileoverview DOM manipulation and querying routines.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
goog.provide('bot.dom');
|
|
|
|
|
|
|
|
|
|
goog.require('bot');
|
2012-05-24 15:48:45 +00:00
|
|
|
goog.require('bot.color');
|
2016-09-02 11:30:03 -07:00
|
|
|
goog.require('bot.dom.core');
|
2024-03-25 15:37:36 +01:00
|
|
|
goog.require('bot.locators.css');
|
2012-01-13 17:10:50 +00:00
|
|
|
goog.require('bot.userAgent');
|
2010-06-22 18:52:07 +00:00
|
|
|
goog.require('goog.array');
|
2011-03-10 01:43:20 +00:00
|
|
|
goog.require('goog.dom');
|
2013-08-16 11:53:17 -07:00
|
|
|
goog.require('goog.dom.DomHelper');
|
2010-07-19 11:24:30 +00:00
|
|
|
goog.require('goog.dom.NodeType');
|
|
|
|
|
goog.require('goog.dom.TagName');
|
2013-09-20 15:24:21 -07:00
|
|
|
goog.require('goog.math');
|
2011-08-02 20:45:02 +00:00
|
|
|
goog.require('goog.math.Coordinate');
|
2011-03-10 01:43:20 +00:00
|
|
|
goog.require('goog.math.Rect');
|
2010-07-19 11:24:30 +00:00
|
|
|
goog.require('goog.string');
|
2010-06-22 18:52:07 +00:00
|
|
|
goog.require('goog.style');
|
2012-09-13 10:52:38 +00:00
|
|
|
goog.require('goog.userAgent');
|
2010-06-22 18:52:07 +00:00
|
|
|
|
|
|
|
|
|
2015-06-18 14:28:03 -07:00
|
|
|
/**
|
|
|
|
|
* Whether Shadow DOM operations are supported by the browser.
|
|
|
|
|
* @const {boolean}
|
|
|
|
|
*/
|
|
|
|
|
bot.dom.IS_SHADOW_DOM_ENABLED = (typeof ShadowRoot === 'function');
|
|
|
|
|
|
|
|
|
|
|
2010-10-11 21:53:13 +00:00
|
|
|
/**
|
|
|
|
|
* Retrieves the active element for a node's owner document.
|
2018-11-25 13:21:40 +00:00
|
|
|
* @param {(!Node|!Window)} nodeOrWindow The node whose owner document to get
|
2010-10-11 21:53:13 +00:00
|
|
|
* the active element for.
|
2018-11-25 13:21:40 +00:00
|
|
|
* @return {?Element} The active element, if any.
|
2010-10-11 21:53:13 +00:00
|
|
|
*/
|
|
|
|
|
bot.dom.getActiveElement = function(nodeOrWindow) {
|
2013-08-09 17:20:30 -04:00
|
|
|
var active = goog.dom.getActiveElement(
|
2012-11-08 16:22:32 +00:00
|
|
|
goog.dom.getOwnerDocument(nodeOrWindow));
|
2013-08-09 17:20:30 -04:00
|
|
|
// IE has the habit of returning an empty object from
|
|
|
|
|
// goog.dom.getActiveElement instead of null.
|
|
|
|
|
if (goog.userAgent.IE &&
|
|
|
|
|
active &&
|
2013-08-12 18:35:19 -04:00
|
|
|
typeof active.nodeType === 'undefined') {
|
2013-08-09 17:20:30 -04:00
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
return active;
|
2010-10-11 21:53:13 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
2010-06-22 18:52:07 +00:00
|
|
|
/**
|
2016-09-02 11:30:03 -07:00
|
|
|
* @const
|
2010-07-02 13:13:29 +00:00
|
|
|
*/
|
2016-09-02 11:30:03 -07:00
|
|
|
bot.dom.isElement = bot.dom.core.isElement;
|
2010-07-02 13:13:29 +00:00
|
|
|
|
2010-07-19 11:24:30 +00:00
|
|
|
|
2011-08-24 16:03:31 +00:00
|
|
|
/**
|
|
|
|
|
* 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) &&
|
2012-09-13 10:52:38 +00:00
|
|
|
bot.dom.isEnabled(element) &&
|
2012-11-08 16:22:32 +00:00
|
|
|
!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) {
|
2015-02-28 21:21:52 +01:00
|
|
|
if (goog.userAgent.IE ||
|
2012-11-08 16:22:32 +00:00
|
|
|
(goog.userAgent.GECKO && !bot.userAgent.isEngineVersion('1.9.2'))) {
|
|
|
|
|
// Don't support pointer events
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return bot.dom.getEffectiveStyle(element, 'pointer-events') == 'none';
|
2011-08-24 16:03:31 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2016-09-02 11:30:03 -07:00
|
|
|
* @const
|
2011-08-24 16:03:31 +00:00
|
|
|
*/
|
2016-09-02 11:30:03 -07:00
|
|
|
bot.dom.isSelectable = bot.dom.core.isSelectable;
|
2011-08-24 16:03:31 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2016-09-02 11:30:03 -07:00
|
|
|
* @const
|
2011-08-24 16:03:31 +00:00
|
|
|
*/
|
2016-09-02 11:30:03 -07:00
|
|
|
bot.dom.isSelected = bot.dom.core.isSelected;
|
2011-08-24 16:03:31 +00:00
|
|
|
|
|
|
|
|
|
2011-04-14 20:25:37 +00:00
|
|
|
/**
|
|
|
|
|
* List of the focusable fields, according to
|
|
|
|
|
* http://www.w3.org/TR/html401/interact/scripts.html#adef-onfocus
|
2013-03-10 13:56:37 -07:00
|
|
|
* @private {!Array.<!goog.dom.TagName>}
|
2013-06-15 16:19:51 -07:00
|
|
|
* @const
|
2011-04-14 20:25:37 +00:00
|
|
|
*/
|
|
|
|
|
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
|
2016-06-23 13:58:54 -07:00
|
|
|
* if it is a form field, has a non-negative tabindex, or is editable.
|
2011-08-02 20:45:02 +00:00
|
|
|
* @param {!Element} element The node to test.
|
2011-04-14 20:25:37 +00:00
|
|
|
* @return {boolean} Whether the node is focusable.
|
|
|
|
|
*/
|
|
|
|
|
bot.dom.isFocusable = function(element) {
|
2016-06-23 13:58:54 -07:00
|
|
|
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) {
|
2017-07-17 17:19:01 -07:00
|
|
|
return bot.dom.isElement(element, tagName);
|
2016-06-23 13:58:54 -07:00
|
|
|
}
|
2011-04-14 20:25:37 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
2010-07-02 13:13:29 +00:00
|
|
|
/**
|
2013-06-15 16:19:51 -07:00
|
|
|
* @const
|
2012-04-24 13:19:38 +00:00
|
|
|
*/
|
2016-09-02 11:30:03 -07:00
|
|
|
bot.dom.getProperty = bot.dom.core.getProperty;
|
2012-04-24 13:19:38 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2016-09-02 11:30:03 -07:00
|
|
|
* @const
|
2010-06-22 18:52:07 +00:00
|
|
|
*/
|
2016-09-02 11:30:03 -07:00
|
|
|
bot.dom.getAttribute = bot.dom.core.getAttribute;
|
2012-01-18 23:20:57 +00:00
|
|
|
|
|
|
|
|
|
2010-07-19 11:24:30 +00:00
|
|
|
/**
|
|
|
|
|
* List of elements that support the "disabled" attribute, as defined by the
|
|
|
|
|
* HTML 4.01 specification.
|
2018-11-25 13:21:40 +00:00
|
|
|
* @private {!Array.<!goog.dom.TagName>}
|
2013-06-15 16:19:51 -07:00
|
|
|
* @const
|
2010-07-19 11:24:30 +00:00
|
|
|
* @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) {
|
2017-07-17 17:19:01 -07:00
|
|
|
var isSupported = goog.array.some(
|
|
|
|
|
bot.dom.DISABLED_ATTRIBUTE_SUPPORTED_,
|
|
|
|
|
function(tagName) { return bot.dom.isElement(el, tagName); });
|
|
|
|
|
if (!isSupported) {
|
2010-07-19 11:24:30 +00:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2011-08-02 20:45:02 +00:00
|
|
|
if (bot.dom.getProperty(el, 'disabled')) {
|
2010-07-19 11:24:30 +00:00
|
|
|
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 &&
|
2017-07-17 17:19:01 -07:00
|
|
|
bot.dom.isElement(el, goog.dom.TagName.OPTGROUP) ||
|
|
|
|
|
bot.dom.isElement(el, goog.dom.TagName.OPTION)) {
|
2013-06-13 17:16:23 -07:00
|
|
|
return bot.dom.isEnabled(/**@type{!Element}*/ (el.parentNode));
|
2010-07-19 11:24:30 +00:00
|
|
|
}
|
2013-03-15 16:34:27 -07:00
|
|
|
|
|
|
|
|
// 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.
|
2013-06-15 16:19:51 -07:00
|
|
|
return !goog.dom.getAncestor(el, function(e) {
|
2013-03-15 16:34:27 -07:00
|
|
|
var parent = e.parentNode;
|
|
|
|
|
|
|
|
|
|
if (parent &&
|
|
|
|
|
bot.dom.isElement(parent, goog.dom.TagName.FIELDSET) &&
|
2013-04-15 14:23:46 -07:00
|
|
|
bot.dom.getProperty(/** @type {!Element} */ (parent), 'disabled')) {
|
2013-03-15 16:34:27 -07:00
|
|
|
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;
|
2013-06-15 16:19:51 -07:00
|
|
|
}, true);
|
2010-07-19 11:24:30 +00:00
|
|
|
};
|
|
|
|
|
|
2011-12-06 11:44:44 +00:00
|
|
|
|
2011-08-18 19:22:56 +00:00
|
|
|
/**
|
|
|
|
|
* List of input types that create text fields.
|
2013-06-15 16:19:51 -07:00
|
|
|
* @private {!Array.<string>}
|
2011-08-18 19:22:56 +00:00
|
|
|
* @const
|
|
|
|
|
* @see http://www.whatwg.org/specs/web-apps/current-work/multipage/the-input-element.html#attr-input-type
|
|
|
|
|
*/
|
|
|
|
|
bot.dom.TEXTUAL_INPUT_TYPES_ = [
|
2011-12-06 11:44:44 +00:00
|
|
|
'text',
|
|
|
|
|
'search',
|
|
|
|
|
'tel',
|
|
|
|
|
'url',
|
|
|
|
|
'email',
|
|
|
|
|
'password',
|
|
|
|
|
'number'
|
2011-09-20 16:29:16 +00:00
|
|
|
];
|
2010-07-19 11:24:30 +00:00
|
|
|
|
2011-12-06 11:44:44 +00:00
|
|
|
|
2011-07-26 17:01:24 +00:00
|
|
|
/**
|
2013-08-16 11:53:17 -07:00
|
|
|
* TODO: Add support for designMode elements.
|
2011-07-26 17:01:24 +00:00
|
|
|
*
|
|
|
|
|
* @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();
|
2011-08-18 19:22:56 +00:00
|
|
|
return goog.array.contains(bot.dom.TEXTUAL_INPUT_TYPES_, type);
|
2011-07-26 17:01:24 +00:00
|
|
|
}
|
|
|
|
|
|
2011-11-08 14:22:40 +00:00
|
|
|
if (bot.dom.isContentEditable(element)) {
|
2011-11-08 11:36:06 +00:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2011-07-26 17:01:24 +00:00
|
|
|
return false;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
2015-05-21 14:44:52 +03:00
|
|
|
/**
|
|
|
|
|
* @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;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
2018-03-17 12:14:08 -07:00
|
|
|
/**
|
|
|
|
|
* @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;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
2011-11-08 14:22:40 +00:00
|
|
|
/**
|
|
|
|
|
* @param {!Element} element The element to check.
|
2011-11-08 14:35:48 +00:00
|
|
|
* @return {boolean} Whether the element is contentEditable.
|
2011-11-08 14:22:40 +00:00
|
|
|
*/
|
|
|
|
|
bot.dom.isContentEditable = function(element) {
|
2011-12-06 11:44:44 +00:00
|
|
|
// 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);
|
2011-11-08 20:34:18 +00:00
|
|
|
};
|
2011-11-08 14:22:40 +00:00
|
|
|
|
|
|
|
|
|
2011-07-26 17:01:24 +00:00
|
|
|
/**
|
2013-08-16 11:53:17 -07:00
|
|
|
* TODO: Merge isTextual into this function and move to bot.dom.
|
2011-07-26 17:01:24 +00:00
|
|
|
* 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) {
|
2018-03-17 12:14:08 -07:00
|
|
|
return (bot.dom.isTextual(element) ||
|
|
|
|
|
bot.dom.isFileInput(element) ||
|
2018-09-28 12:04:42 -07:00
|
|
|
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')) &&
|
2011-07-26 17:01:24 +00:00
|
|
|
!bot.dom.getProperty(element, 'readOnly');
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
2010-07-19 11:24:30 +00:00
|
|
|
/**
|
|
|
|
|
* 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.
|
|
|
|
|
*/
|
2011-06-27 03:26:12 +00:00
|
|
|
bot.dom.getParentElement = function(node) {
|
2010-07-19 11:24:30 +00:00
|
|
|
var elem = node.parentNode;
|
|
|
|
|
|
2010-08-19 13:39:26 +00:00
|
|
|
while (elem &&
|
|
|
|
|
elem.nodeType != goog.dom.NodeType.ELEMENT &&
|
2010-07-19 11:24:30 +00:00
|
|
|
elem.nodeType != goog.dom.NodeType.DOCUMENT &&
|
|
|
|
|
elem.nodeType != goog.dom.NodeType.DOCUMENT_FRAGMENT) {
|
|
|
|
|
elem = elem.parentNode;
|
|
|
|
|
}
|
2013-06-13 17:16:23 -07:00
|
|
|
return /** @type {Element} */ (bot.dom.isElement(elem) ? elem : null);
|
2010-08-19 13:39:26 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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
|
2010-11-19 16:05:03 +00:00
|
|
|
* has not been explicitly set in script.
|
2010-08-19 13:39:26 +00:00
|
|
|
*
|
|
|
|
|
* @param {!Element} elem Element to get the style value from.
|
|
|
|
|
* @param {string} styleName Name of the style property in selector-case.
|
2010-11-19 16:05:03 +00:00
|
|
|
* @return {string} The value of the style property.
|
2010-08-19 13:39:26 +00:00
|
|
|
*/
|
|
|
|
|
bot.dom.getInlineStyle = function(elem, styleName) {
|
2010-11-19 16:05:03 +00:00
|
|
|
return goog.style.getStyle(elem, styleName);
|
2010-08-19 13:39:26 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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.
|
2013-11-13 12:56:36 -08:00
|
|
|
* @param {string} propertyName Name of the CSS property.
|
2010-08-19 13:39:26 +00:00
|
|
|
* @return {?string} The value of the style property, or null.
|
|
|
|
|
*/
|
2012-05-24 15:48:45 +00:00
|
|
|
bot.dom.getEffectiveStyle = function(elem, propertyName) {
|
|
|
|
|
var styleName = goog.string.toCamelCase(propertyName);
|
2012-09-13 10:52:38 +00:00
|
|
|
if (styleName == 'float' ||
|
|
|
|
|
styleName == 'cssFloat' ||
|
|
|
|
|
styleName == 'styleFloat') {
|
|
|
|
|
styleName = bot.userAgent.IE_DOC_PRE9 ? 'styleFloat' : 'cssFloat';
|
|
|
|
|
}
|
2012-05-24 15:48:45 +00:00
|
|
|
var style = goog.style.getComputedStyle(elem, styleName) ||
|
2010-08-19 13:39:26 +00:00
|
|
|
bot.dom.getCascadedStyle_(elem, styleName);
|
2012-05-24 15:48:45 +00:00
|
|
|
if (style === null) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2013-11-13 12:56:36 -08:00
|
|
|
return bot.color.standardizeColor(styleName, style);
|
2010-08-19 13:39:26 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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) {
|
2011-11-25 15:17:49 +00:00
|
|
|
var style = elem.currentStyle || elem.style;
|
|
|
|
|
var value = style[styleName];
|
2017-02-28 12:35:12 -08:00
|
|
|
if (!goog.isDef(value) && goog.isFunction(style.getPropertyValue)) {
|
|
|
|
|
value = style.getPropertyValue(styleName);
|
2011-11-25 15:17:49 +00:00
|
|
|
}
|
2011-12-06 11:44:44 +00:00
|
|
|
|
2010-08-19 13:39:26 +00:00
|
|
|
if (value != 'inherit') {
|
|
|
|
|
return goog.isDef(value) ? value : null;
|
|
|
|
|
}
|
2011-06-27 03:26:12 +00:00
|
|
|
var parent = bot.dom.getParentElement(elem);
|
2010-08-19 13:39:26 +00:00
|
|
|
return parent ? bot.dom.getCascadedStyle_(parent, styleName) : null;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2017-01-17 20:18:07 +00:00
|
|
|
* Extracted code from bot.dom.isShown.
|
2010-08-19 13:39:26 +00:00
|
|
|
*
|
|
|
|
|
* @param {!Element} elem The element to consider.
|
2015-06-18 14:28:03 -07:00
|
|
|
* @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.
|
2011-12-06 11:44:44 +00:00
|
|
|
* @return {boolean} Whether or not the element is visible.
|
2015-06-18 14:28:03 -07:00
|
|
|
* @private
|
2010-08-19 13:39:26 +00:00
|
|
|
*/
|
2015-06-18 14:28:03 -07:00
|
|
|
bot.dom.isShown_ = function(elem, ignoreOpacity, parentsDisplayedFn) {
|
2011-01-26 13:28:56 +00:00
|
|
|
if (!bot.dom.isElement(elem)) {
|
2010-08-19 13:39:26 +00:00
|
|
|
throw new Error('Argument to isShown must be of type Element');
|
|
|
|
|
}
|
|
|
|
|
|
2014-05-16 14:46:29 -07:00
|
|
|
// By convention, BODY element is always shown: BODY represents the document
|
2015-06-18 14:28:03 -07:00
|
|
|
// and even if there's nothing rendered in there, user can always see there's
|
|
|
|
|
// the document.
|
2014-05-16 14:46:29 -07:00
|
|
|
if (bot.dom.isElement(elem, goog.dom.TagName.BODY)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2011-11-28 18:02:14 +00:00
|
|
|
// Option or optgroup is shown iff enclosing select is shown (ignoring the
|
|
|
|
|
// select's opacity).
|
2011-01-26 13:28:56 +00:00
|
|
|
if (bot.dom.isElement(elem, goog.dom.TagName.OPTION) ||
|
|
|
|
|
bot.dom.isElement(elem, goog.dom.TagName.OPTGROUP)) {
|
2010-08-19 13:39:26 +00:00
|
|
|
var select = /**@type {Element}*/ (goog.dom.getAncestor(elem, function(e) {
|
2011-01-26 13:28:56 +00:00
|
|
|
return bot.dom.isElement(e, goog.dom.TagName.SELECT);
|
2010-08-19 13:39:26 +00:00
|
|
|
}));
|
2015-06-18 14:28:03 -07:00
|
|
|
return !!select && bot.dom.isShown_(select, true, parentsDisplayedFn);
|
2010-08-19 13:39:26 +00:00
|
|
|
}
|
|
|
|
|
|
2013-06-15 16:19:51 -07:00
|
|
|
// 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 &&
|
2015-06-18 14:28:03 -07:00
|
|
|
bot.dom.isShown_(
|
|
|
|
|
imageMap.image, ignoreOpacity, parentsDisplayedFn);
|
2010-12-22 17:04:29 +00:00
|
|
|
}
|
|
|
|
|
|
2010-08-19 13:39:26 +00:00
|
|
|
// Any hidden input is not shown.
|
2011-01-26 13:28:56 +00:00
|
|
|
if (bot.dom.isElement(elem, goog.dom.TagName.INPUT) &&
|
2010-08-19 13:39:26 +00:00
|
|
|
elem.type.toLowerCase() == 'hidden') {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2011-12-19 14:41:17 +00:00
|
|
|
// Any NOSCRIPT element is not shown.
|
|
|
|
|
if (bot.dom.isElement(elem, goog.dom.TagName.NOSCRIPT)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2015-05-06 13:47:56 -07:00
|
|
|
// Any element with hidden/collapsed visibility is not shown.
|
|
|
|
|
var visibility = bot.dom.getEffectiveStyle(elem, 'visibility');
|
|
|
|
|
if (visibility == 'collapse' || visibility == 'hidden') {
|
2010-08-19 13:39:26 +00:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2015-06-18 14:28:03 -07:00
|
|
|
if (!parentsDisplayedFn(elem)) {
|
2010-08-19 13:39:26 +00:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2010-12-22 17:04:29 +00:00
|
|
|
// Any transparent element is not shown.
|
2015-06-18 14:28:03 -07:00
|
|
|
if (!ignoreOpacity && bot.dom.getOpacity(elem) == 0) {
|
2010-12-22 17:04:29 +00:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2010-08-19 13:39:26 +00:00
|
|
|
// Any element without positive size dimensions is not shown.
|
2010-10-07 13:10:58 +00:00
|
|
|
function positiveSize(e) {
|
2013-06-15 16:19:51 -07:00
|
|
|
var rect = bot.dom.getClientRect(e);
|
|
|
|
|
if (rect.height > 0 && rect.width > 0) {
|
2010-10-07 13:10:58 +00:00
|
|
|
return true;
|
|
|
|
|
}
|
2012-11-08 16:22:32 +00:00
|
|
|
// A vertical or horizontal SVG Path element will report zero width or
|
|
|
|
|
// height but is "shown" if it has a positive stroke-width.
|
2013-06-15 16:19:51 -07:00
|
|
|
if (bot.dom.isElement(e, 'PATH') && (rect.height > 0 || rect.width > 0)) {
|
2012-11-08 16:22:32 +00:00
|
|
|
var strokeWidth = bot.dom.getEffectiveStyle(e, 'stroke-width');
|
|
|
|
|
return !!strokeWidth && (parseInt(strokeWidth, 10) > 0);
|
|
|
|
|
}
|
2011-08-02 20:45:02 +00:00
|
|
|
// Zero-sized elements should still be considered to have positive size
|
2013-06-15 16:19:51 -07:00
|
|
|
// 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));
|
|
|
|
|
});
|
2010-10-07 13:10:58 +00:00
|
|
|
}
|
|
|
|
|
if (!positiveSize(elem)) {
|
2010-08-19 13:39:26 +00:00
|
|
|
return false;
|
|
|
|
|
}
|
2012-03-19 11:27:07 +00:00
|
|
|
|
2013-06-15 16:19:51 -07:00
|
|
|
// Elements that are hidden by overflow are not shown.
|
2013-08-16 11:53:17 -07:00
|
|
|
function hiddenByOverflow(e) {
|
|
|
|
|
return bot.dom.getOverflowState(e) == bot.dom.OverflowState.HIDDEN &&
|
|
|
|
|
goog.array.every(e.childNodes, function(n) {
|
2015-06-18 19:00:49 -07:00
|
|
|
return !bot.dom.isElement(n) || hiddenByOverflow(n) ||
|
|
|
|
|
!positiveSize(n);
|
2013-08-16 11:53:17 -07:00
|
|
|
});
|
2012-11-06 15:47:32 +00:00
|
|
|
}
|
2013-08-16 11:53:17 -07:00
|
|
|
return !hiddenByOverflow(elem);
|
2010-08-19 13:39:26 +00:00
|
|
|
};
|
|
|
|
|
|
2013-06-15 16:19:51 -07:00
|
|
|
|
2015-06-18 14:28:03 -07:00
|
|
|
/**
|
|
|
|
|
* 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.
|
|
|
|
|
*
|
2017-01-17 20:18:07 +00:00
|
|
|
* 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.
|
|
|
|
|
*
|
2015-06-18 14:28:03 -07:00
|
|
|
* @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) {
|
2018-04-23 20:39:22 +01:00
|
|
|
/**
|
|
|
|
|
* Determines whether an element or its parents have `display: none` set
|
2018-04-24 21:58:21 +01:00
|
|
|
* @param {!Node} e the element
|
2020-11-10 12:28:55 +00:00
|
|
|
* @return {!boolean}
|
2018-04-23 20:39:22 +01:00
|
|
|
*/
|
|
|
|
|
function displayed(e) {
|
2018-04-24 21:58:21 +01:00
|
|
|
if (bot.dom.isElement(e)) {
|
|
|
|
|
var elem = /** @type {!Element} */ (e);
|
|
|
|
|
if (bot.dom.getEffectiveStyle(elem, 'display') == 'none') {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2018-04-23 19:23:16 +01:00
|
|
|
}
|
2018-04-18 14:20:37 +01:00
|
|
|
|
2018-04-23 19:23:16 +01:00
|
|
|
var parent = bot.dom.getParentNodeInComposedDom(e);
|
2018-04-18 14:20:37 +01:00
|
|
|
|
2018-04-23 19:23:16 +01:00
|
|
|
if (bot.dom.IS_SHADOW_DOM_ENABLED && (parent instanceof ShadowRoot)) {
|
2023-11-29 14:57:16 +01:00
|
|
|
if (parent.host.shadowRoot && parent.host.shadowRoot !== parent) {
|
2018-04-23 19:23:16 +01:00
|
|
|
// 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;
|
2018-04-18 14:20:37 +01:00
|
|
|
}
|
2018-04-23 19:23:16 +01:00
|
|
|
}
|
2018-04-18 14:20:37 +01:00
|
|
|
|
2018-04-23 19:23:16 +01:00
|
|
|
if (parent && (parent.nodeType == goog.dom.NodeType.DOCUMENT ||
|
|
|
|
|
parent.nodeType == goog.dom.NodeType.DOCUMENT_FRAGMENT)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-19 04:56:16 -07:00
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-10 12:28:55 +00:00
|
|
|
return !!parent && displayed(parent);
|
2018-04-23 20:39:22 +01:00
|
|
|
}
|
2018-04-18 14:20:37 +01:00
|
|
|
|
2015-06-18 14:28:03 -07:00
|
|
|
return bot.dom.isShown_(elem, !!opt_ignoreOpacity, displayed);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
2013-06-15 16:19:51 -07:00
|
|
|
/**
|
|
|
|
|
* 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'
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2013-09-20 15:24:21 -07:00
|
|
|
* 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.
|
2013-06-15 16:19:51 -07:00
|
|
|
*
|
|
|
|
|
* @param {!Element} elem Element.
|
2013-09-20 15:24:21 -07:00
|
|
|
* @param {!(goog.math.Coordinate|goog.math.Rect)=} opt_region
|
|
|
|
|
* Coordinate or rectangle relative to the top-left corner of the element.
|
2013-06-15 16:19:51 -07:00
|
|
|
* @return {bot.dom.OverflowState} Overflow state of the element.
|
|
|
|
|
*/
|
2013-09-20 15:24:21 -07:00
|
|
|
bot.dom.getOverflowState = function(elem, opt_region) {
|
|
|
|
|
var region = bot.dom.getClientRegion(elem, opt_region);
|
2013-06-15 16:19:51 -07:00
|
|
|
var ownerDoc = goog.dom.getOwnerDocument(elem);
|
|
|
|
|
var htmlElem = ownerDoc.documentElement;
|
2013-08-16 11:53:17 -07:00
|
|
|
var bodyElem = ownerDoc.body;
|
|
|
|
|
var htmlOverflowStyle = bot.dom.getEffectiveStyle(htmlElem, 'overflow');
|
|
|
|
|
var treatAsFixedPosition;
|
|
|
|
|
|
2013-06-15 16:19:51 -07:00
|
|
|
// Return the closest ancestor that the given element may overflow.
|
|
|
|
|
function getOverflowParent(e) {
|
|
|
|
|
var position = bot.dom.getEffectiveStyle(e, 'position');
|
|
|
|
|
if (position == 'fixed') {
|
2013-08-16 11:53:17 -07:00
|
|
|
treatAsFixedPosition = true;
|
2013-06-15 16:19:51 -07:00
|
|
|
// Fixed-position element may only overflow the viewport.
|
2013-12-03 15:43:11 -08:00
|
|
|
return e == htmlElem ? null : htmlElem;
|
2013-06-15 16:19:51 -07:00
|
|
|
} 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;
|
|
|
|
|
}
|
2019-03-08 14:59:41 -08:00
|
|
|
// An element cannot overflow an element with an inline or contents display style.
|
2013-06-15 16:19:51 -07:00
|
|
|
var containerDisplay = /** @type {string} */ (
|
|
|
|
|
bot.dom.getEffectiveStyle(container, 'display'));
|
2019-03-08 14:59:41 -08:00
|
|
|
if (goog.string.startsWith(containerDisplay, 'inline') ||
|
|
|
|
|
(containerDisplay == 'contents')) {
|
2013-06-15 16:19:51 -07:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2013-08-16 11:53:17 -07:00
|
|
|
// 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') {
|
2013-09-05 18:58:57 -07:00
|
|
|
// Note: bodyElem will be null/undefined in SVG documents.
|
|
|
|
|
if (e == htmlElem && bodyElem) {
|
2013-08-16 11:53:17 -07:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2013-06-15 16:19:51 -07:00
|
|
|
// Check if the element overflows any ancestor element.
|
|
|
|
|
for (var container = getOverflowParent(elem);
|
|
|
|
|
!!container;
|
|
|
|
|
container = getOverflowParent(container)) {
|
|
|
|
|
var containerOverflow = getOverflowStyles(container);
|
2013-09-20 15:24:21 -07:00
|
|
|
|
|
|
|
|
// If the container has overflow:visible, the element cannot overflow it.
|
2013-08-16 11:53:17 -07:00
|
|
|
if (containerOverflow.x == 'visible' && containerOverflow.y == 'visible') {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2013-09-20 15:24:21 -07:00
|
|
|
|
2013-08-16 11:53:17 -07:00
|
|
|
var containerRect = bot.dom.getClientRect(container);
|
|
|
|
|
|
2013-09-20 15:24:21 -07:00
|
|
|
// Zero-sized containers without overflow:visible hide all descendants.
|
|
|
|
|
if (containerRect.width == 0 || containerRect.height == 0) {
|
|
|
|
|
return bot.dom.OverflowState.HIDDEN;
|
|
|
|
|
}
|
|
|
|
|
|
2013-08-16 11:53:17 -07:00
|
|
|
// Check "underflow": if an element is to the left or above the container
|
2013-09-20 15:24:21 -07:00
|
|
|
var underflowsX = region.right < containerRect.left;
|
|
|
|
|
var underflowsY = region.bottom < containerRect.top;
|
2013-08-16 11:53:17 -07:00
|
|
|
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);
|
2013-09-20 15:24:21 -07:00
|
|
|
var unscrollableX = region.right < containerRect.left - containerScroll.x;
|
|
|
|
|
var unscrollableY = region.bottom < containerRect.top - containerScroll.y;
|
2013-08-16 11:53:17 -07:00
|
|
|
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;
|
|
|
|
|
}
|
2013-06-15 16:19:51 -07:00
|
|
|
|
|
|
|
|
// Check "overflow": if an element is to the right or below a container
|
2013-09-20 15:24:21 -07:00
|
|
|
var overflowsX = region.left >= containerRect.left + containerRect.width;
|
|
|
|
|
var overflowsY = region.top >= containerRect.top + containerRect.height;
|
2013-06-15 16:19:51 -07:00
|
|
|
if ((overflowsX && containerOverflow.x == 'hidden') ||
|
|
|
|
|
(overflowsY && containerOverflow.y == 'hidden')) {
|
|
|
|
|
return bot.dom.OverflowState.HIDDEN;
|
|
|
|
|
} else if ((overflowsX && containerOverflow.x != 'visible') ||
|
|
|
|
|
(overflowsY && containerOverflow.y != 'visible')) {
|
2013-08-16 11:53:17 -07:00
|
|
|
// 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);
|
2013-09-20 15:24:21 -07:00
|
|
|
if ((region.left >= htmlElem.scrollWidth - docScroll.x) ||
|
|
|
|
|
(region.right >= htmlElem.scrollHeight - docScroll.y)) {
|
2013-08-16 11:53:17 -07:00
|
|
|
return bot.dom.OverflowState.HIDDEN;
|
|
|
|
|
}
|
|
|
|
|
}
|
2013-06-15 16:19:51 -07:00
|
|
|
// 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;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
2013-08-16 11:53:17 -07:00
|
|
|
/**
|
|
|
|
|
* 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)?\\)');
|
|
|
|
|
|
|
|
|
|
|
2013-06-15 16:19:51 -07:00
|
|
|
/**
|
|
|
|
|
* 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.
|
2013-08-16 11:53:17 -07:00
|
|
|
* 4. Defines the client rect of the <html> element to be the window viewport.
|
2013-06-15 16:19:51 -07:00
|
|
|
*
|
|
|
|
|
* @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 {
|
2013-08-16 11:53:17 -07:00
|
|
|
var nativeRect;
|
|
|
|
|
try {
|
2013-09-05 18:58:57 -07:00
|
|
|
// TODO: in IE and Firefox, getBoundingClientRect includes stroke width,
|
|
|
|
|
// but getBBox does not.
|
2013-08-16 11:53:17 -07:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2013-11-13 12:56:36 -08:00
|
|
|
var rect = new goog.math.Rect(nativeRect.left, nativeRect.top,
|
|
|
|
|
nativeRect.right - nativeRect.left, nativeRect.bottom - nativeRect.top);
|
2013-08-16 11:53:17 -07:00
|
|
|
|
|
|
|
|
// In IE, the element can additionally be offset by a border around the
|
|
|
|
|
// documentElement or body element that we have to subtract.
|
2013-09-20 15:24:21 -07:00
|
|
|
if (goog.userAgent.IE && elem.ownerDocument.body) {
|
2013-08-16 11:53:17 -07:00
|
|
|
var doc = goog.dom.getOwnerDocument(elem);
|
|
|
|
|
rect.left -= doc.documentElement.clientLeft + doc.body.clientLeft;
|
|
|
|
|
rect.top -= doc.documentElement.clientTop + doc.body.clientTop;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return rect;
|
|
|
|
|
}
|
2013-06-15 16:19:51 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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);
|
|
|
|
|
|
2013-08-16 11:53:17 -07:00
|
|
|
// TODO: Restrict to applet, img, input:image, and object nodes.
|
2024-03-25 15:37:36 +01:00
|
|
|
var locator = '*[usemap="#' + map.name + '"]';
|
2013-06-15 16:19:51 -07:00
|
|
|
|
2013-08-16 11:53:17 -07:00
|
|
|
// TODO: Break dependency of bot.locators on bot.dom,
|
2013-06-15 16:19:51 -07:00
|
|
|
// so bot.locators.findElement can be called here instead.
|
2024-03-25 15:37:36 +01:00
|
|
|
image = bot.locators.css.single(locator, mapDoc);
|
2013-06-15 16:19:51 -07:00
|
|
|
|
|
|
|
|
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)};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
2013-02-22 12:31:30 -05:00
|
|
|
/**
|
2013-06-15 16:19:51 -07:00
|
|
|
* 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);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
2013-09-20 15:24:21 -07:00
|
|
|
/**
|
|
|
|
|
* 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;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
2011-08-08 16:03:16 +00:00
|
|
|
/**
|
|
|
|
|
* 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, '');
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
2010-08-19 13:39:26 +00:00
|
|
|
/**
|
2015-06-18 14:28:03 -07:00
|
|
|
* Helper function for getVisibleText[InDisplayedDom].
|
|
|
|
|
* @param {!Array.<string>} lines Accumulated visible lines of text.
|
|
|
|
|
* @return {string} cleaned up concatenated lines
|
|
|
|
|
* @private
|
2010-08-19 13:39:26 +00:00
|
|
|
*/
|
2015-06-18 14:28:03 -07:00
|
|
|
bot.dom.concatenateCleanedLines_ = function(lines) {
|
2011-08-08 16:03:16 +00:00
|
|
|
lines = goog.array.map(
|
2015-06-18 14:28:03 -07:00
|
|
|
lines,
|
|
|
|
|
bot.dom.trimExcludingNonBreakingSpaceCharacters_);
|
2011-08-08 16:03:16 +00:00
|
|
|
var joined = lines.join('\n');
|
|
|
|
|
var trimmed = bot.dom.trimExcludingNonBreakingSpaceCharacters_(joined);
|
2011-12-06 11:44:44 +00:00
|
|
|
|
|
|
|
|
// Replace non-breakable spaces with regular ones.
|
|
|
|
|
return trimmed.replace(/\xa0/g, ' ');
|
2010-08-19 13:39:26 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
2017-01-17 16:20:54 +00:00
|
|
|
/**
|
|
|
|
|
* @param {!Element} elem The element to consider.
|
|
|
|
|
* @return {string} visible text.
|
|
|
|
|
*/
|
|
|
|
|
bot.dom.getVisibleText = function(elem) {
|
|
|
|
|
var lines = [];
|
2017-01-17 20:52:02 +00:00
|
|
|
|
|
|
|
|
if (bot.dom.IS_SHADOW_DOM_ENABLED) {
|
|
|
|
|
bot.dom.appendVisibleTextLinesFromElementInComposedDom_(elem, lines);
|
|
|
|
|
} else {
|
|
|
|
|
bot.dom.appendVisibleTextLinesFromElement_(elem, lines);
|
|
|
|
|
}
|
2017-01-17 16:20:54 +00:00
|
|
|
return bot.dom.concatenateCleanedLines_(lines);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
2015-06-18 14:28:03 -07:00
|
|
|
/**
|
|
|
|
|
* Helper function used by bot.dom.appendVisibleTextLinesFromElement_ and
|
|
|
|
|
* bot.dom.appendVisibleTextLinesFromElementInComposedDom_
|
2011-02-09 22:07:47 +00:00
|
|
|
* @param {!Element} elem Element.
|
|
|
|
|
* @param {!Array.<string>} lines Accumulated visible lines of text.
|
2015-06-18 14:28:03 -07:00
|
|
|
* @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
|
2010-08-19 13:39:26 +00:00
|
|
|
* @private
|
|
|
|
|
*/
|
2015-06-18 14:28:03 -07:00
|
|
|
bot.dom.appendVisibleTextLinesFromElementCommon_ = function(
|
|
|
|
|
elem, lines, isShownFn, childNodeFn) {
|
2011-08-02 20:45:02 +00:00
|
|
|
function currLine() {
|
2013-06-13 17:16:23 -07:00
|
|
|
return /** @type {string|undefined} */ (goog.array.peek(lines)) || '';
|
2011-08-02 20:45:02 +00:00
|
|
|
}
|
|
|
|
|
|
2013-08-16 11:53:17 -07:00
|
|
|
// TODO: Add case here for textual form elements.
|
2011-02-09 22:07:47 +00:00
|
|
|
if (bot.dom.isElement(elem, goog.dom.TagName.BR)) {
|
|
|
|
|
lines.push('');
|
|
|
|
|
} else {
|
2012-09-13 10:52:38 +00:00
|
|
|
// TODO: properly handle display:run-in
|
2011-08-02 20:45:02 +00:00
|
|
|
var isTD = bot.dom.isElement(elem, goog.dom.TagName.TD);
|
2011-04-26 22:23:55 +00:00
|
|
|
var display = bot.dom.getEffectiveStyle(elem, 'display');
|
2011-08-02 20:45:02 +00:00
|
|
|
// On some browsers, table cells incorrectly show up with block styles.
|
|
|
|
|
var isBlock = !isTD &&
|
|
|
|
|
!goog.array.contains(bot.dom.INLINE_DISPLAY_BOXES_, display);
|
2011-04-26 22:23:55 +00:00
|
|
|
|
2012-06-18 17:45:59 +00:00
|
|
|
// 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.
|
|
|
|
|
|
2012-06-19 16:40:34 +00:00
|
|
|
var previousElementSibling = goog.dom.getPreviousElementSibling(elem);
|
|
|
|
|
var prevDisplay = (previousElementSibling) ?
|
|
|
|
|
bot.dom.getEffectiveStyle(previousElementSibling, 'display') : '';
|
2013-08-16 11:53:17 -07:00
|
|
|
// TODO: getEffectiveStyle should mask this for us
|
2012-06-19 16:40:34 +00:00
|
|
|
var thisFloat = bot.dom.getEffectiveStyle(elem, 'float') ||
|
|
|
|
|
bot.dom.getEffectiveStyle(elem, 'cssFloat') ||
|
|
|
|
|
bot.dom.getEffectiveStyle(elem, 'styleFloat');
|
|
|
|
|
var runIntoThis = prevDisplay == 'run-in' && thisFloat == 'none';
|
2017-07-17 18:05:42 -07:00
|
|
|
if (isBlock && !runIntoThis &&
|
|
|
|
|
!goog.string.isEmptyOrWhitespace(currLine())) {
|
2011-02-09 22:07:47 +00:00
|
|
|
lines.push('');
|
|
|
|
|
}
|
2011-04-26 22:23:55 +00:00
|
|
|
|
|
|
|
|
// 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.
|
2015-06-18 14:28:03 -07:00
|
|
|
var shown = isShownFn(elem);
|
2011-04-26 22:23:55 +00:00
|
|
|
|
|
|
|
|
// 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.
|
2011-08-02 20:45:02 +00:00
|
|
|
var whitespace = null, textTransform = null;
|
2011-04-26 22:23:55 +00:00
|
|
|
if (shown) {
|
2011-08-02 20:45:02 +00:00
|
|
|
whitespace = bot.dom.getEffectiveStyle(elem, 'white-space');
|
|
|
|
|
textTransform = bot.dom.getEffectiveStyle(elem, 'text-transform');
|
2011-04-26 22:23:55 +00:00
|
|
|
}
|
|
|
|
|
|
2011-02-09 22:07:47 +00:00
|
|
|
goog.array.forEach(elem.childNodes, function(node) {
|
2015-06-18 14:28:03 -07:00
|
|
|
childNodeFn(node, lines, shown, whitespace, textTransform);
|
2011-02-09 22:07:47 +00:00
|
|
|
});
|
2011-04-26 22:23:55 +00:00
|
|
|
|
2011-08-02 20:45:02 +00:00
|
|
|
var line = currLine();
|
2011-04-26 23:48:16 +00:00
|
|
|
|
2011-04-26 22:23:55 +00:00
|
|
|
// 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.
|
2011-08-02 20:45:02 +00:00
|
|
|
if ((isTD || display == 'table-cell') && line &&
|
|
|
|
|
!goog.string.endsWith(line, ' ')) {
|
2011-04-26 22:23:55 +00:00
|
|
|
lines[lines.length - 1] += ' ';
|
|
|
|
|
}
|
|
|
|
|
|
2012-06-18 17:45:59 +00:00
|
|
|
// Add a newline after block elems when there is text on the current line,
|
|
|
|
|
// and the current element isn't marked as run-in.
|
2017-07-17 18:05:42 -07:00
|
|
|
if (isBlock && display != 'run-in' &&
|
|
|
|
|
!goog.string.isEmptyOrWhitespace(line)) {
|
2011-02-09 22:07:47 +00:00
|
|
|
lines.push('');
|
|
|
|
|
}
|
2010-08-19 13:39:26 +00:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
2015-06-18 14:28:03 -07:00
|
|
|
/**
|
|
|
|
|
* @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);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
2011-04-27 19:34:45 +00:00
|
|
|
/**
|
2011-08-02 20:45:02 +00:00
|
|
|
* Elements with one of these effective "display" styles are treated as inline
|
|
|
|
|
* display boxes and have their visible text appended to the current line.
|
2013-03-10 13:56:37 -07:00
|
|
|
* @private {!Array.<string>}
|
2011-04-26 22:23:55 +00:00
|
|
|
* @const
|
2010-08-19 13:39:26 +00:00
|
|
|
*/
|
2011-04-26 22:23:55 +00:00
|
|
|
bot.dom.INLINE_DISPLAY_BOXES_ = [
|
|
|
|
|
'inline',
|
|
|
|
|
'inline-block',
|
|
|
|
|
'inline-table',
|
|
|
|
|
'none',
|
|
|
|
|
'table-cell',
|
|
|
|
|
'table-column',
|
|
|
|
|
'table-column-group'
|
|
|
|
|
];
|
2010-08-19 13:39:26 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2011-02-09 22:07:47 +00:00
|
|
|
* @param {!Text} textNode Text node.
|
|
|
|
|
* @param {!Array.<string>} lines Accumulated visible lines of text.
|
2011-08-02 20:45:02 +00:00
|
|
|
* @param {?string} whitespace Parent element's "white-space" style.
|
|
|
|
|
* @param {?string} textTransform Parent element's "text-transform" style.
|
2010-08-19 13:39:26 +00:00
|
|
|
* @private
|
|
|
|
|
*/
|
2011-04-26 22:23:55 +00:00
|
|
|
bot.dom.appendVisibleTextLinesFromTextNode_ = function(textNode, lines,
|
|
|
|
|
whitespace, textTransform) {
|
2015-06-18 14:28:03 -07:00
|
|
|
|
2013-08-16 11:53:17 -07:00
|
|
|
// 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, '');
|
2011-08-02 20:45:02 +00:00
|
|
|
|
|
|
|
|
// 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);
|
2011-04-26 22:23:55 +00:00
|
|
|
if (whitespace == 'normal' || whitespace == 'nowrap') {
|
2011-08-02 20:45:02 +00:00
|
|
|
text = text.replace(/\n/g, ' ');
|
|
|
|
|
}
|
|
|
|
|
|
2011-12-06 11:44:44 +00:00
|
|
|
// 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().
|
2011-08-02 20:45:02 +00:00
|
|
|
if (whitespace == 'pre' || whitespace == 'pre-wrap') {
|
2011-12-06 11:44:44 +00:00
|
|
|
text = text.replace(/[ \f\t\v\u2028\u2029]/g, '\xa0');
|
2011-08-02 20:45:02 +00:00
|
|
|
} else {
|
2011-08-08 16:03:16 +00:00
|
|
|
text = text.replace(/[\ \f\t\v\u2028\u2029]+/g, ' ');
|
2011-08-02 20:45:02 +00:00
|
|
|
}
|
2011-04-26 22:23:55 +00:00
|
|
|
|
|
|
|
|
if (textTransform == 'capitalize') {
|
2021-10-18 08:36:50 -07:00
|
|
|
// the unicode regex ending with /gu does not work in IE
|
2024-04-10 14:02:39 +02:00
|
|
|
var re = goog.userAgent.IE ? /(^|\s|\b)(\S)/g : /(^|\s|\b)(\S)/gu;
|
2021-10-18 08:36:50 -07:00
|
|
|
text = text.replace(re, function() {
|
2020-04-12 12:50:49 -07:00
|
|
|
return arguments[1] + arguments[2].toUpperCase();
|
2011-04-26 22:23:55 +00:00
|
|
|
});
|
|
|
|
|
} else if (textTransform == 'uppercase') {
|
|
|
|
|
text = text.toUpperCase();
|
|
|
|
|
} else if (textTransform == 'lowercase') {
|
|
|
|
|
text = text.toLowerCase();
|
2010-08-19 13:39:26 +00:00
|
|
|
}
|
2011-04-26 22:23:55 +00:00
|
|
|
|
2011-04-23 01:25:08 +00:00
|
|
|
var currLine = lines.pop() || '';
|
2011-02-09 22:07:47 +00:00
|
|
|
if (goog.string.endsWith(currLine, ' ') &&
|
|
|
|
|
goog.string.startsWith(text, ' ')) {
|
|
|
|
|
text = text.substr(1);
|
2010-08-19 13:39:26 +00:00
|
|
|
}
|
2011-12-06 11:44:44 +00:00
|
|
|
lines.push(currLine + text);
|
2010-07-19 11:24:30 +00:00
|
|
|
};
|
2010-11-19 16:05:03 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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) {
|
2013-08-16 11:53:17 -07:00
|
|
|
// TODO: Does this need to deal with rgba colors?
|
2014-01-08 10:52:32 -08:00
|
|
|
if (!bot.userAgent.IE_DOC_PRE9) {
|
2010-11-19 16:05:03 +00:00
|
|
|
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.
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2010-12-22 17:04:29 +00:00
|
|
|
|
2011-10-21 00:42:29 +00:00
|
|
|
/**
|
|
|
|
|
* 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;
|
|
|
|
|
};
|
2015-06-18 14:28:03 -07:00
|
|
|
|
2017-01-17 20:52:02 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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;
|
2018-04-23 19:28:52 +01:00
|
|
|
|
2018-04-22 21:31:23 +01:00
|
|
|
// Shadow DOM v1
|
2018-06-11 22:48:57 +03:00
|
|
|
if (parent && parent.shadowRoot && node.assignedSlot !== undefined) {
|
2018-04-22 21:31:23 +01:00
|
|
|
// Can be null on purpose, meaning it has no parent as
|
|
|
|
|
// it hasn't yet been slotted
|
2018-04-23 19:28:52 +01:00
|
|
|
return node.assignedSlot ? node.assignedSlot.parentNode : null;
|
2018-04-22 21:31:23 +01:00
|
|
|
}
|
2018-04-23 19:28:52 +01:00
|
|
|
|
2018-04-18 14:20:37 +01:00
|
|
|
// Shadow DOM V0 (deprecated)
|
2017-01-17 20:52:02 +00:00
|
|
|
if (node.getDestinationInsertionPoints) {
|
|
|
|
|
var destinations = node.getDestinationInsertionPoints();
|
|
|
|
|
if (destinations.length > 0) {
|
2018-04-23 19:28:52 +01:00
|
|
|
return destinations[destinations.length - 1];
|
2015-06-18 14:28:03 -07:00
|
|
|
}
|
2017-01-17 20:52:02 +00:00
|
|
|
}
|
2018-04-23 19:28:52 +01:00
|
|
|
|
2017-01-17 20:52:02 +00:00
|
|
|
return parent;
|
|
|
|
|
};
|
2015-06-18 14:28:03 -07:00
|
|
|
|
|
|
|
|
|
2017-01-17 20:52:02 +00:00
|
|
|
/**
|
|
|
|
|
* @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);
|
|
|
|
|
|
2018-04-11 13:37:37 +01:00
|
|
|
if (bot.dom.isElement(node, 'CONTENT') || bot.dom.isElement(node, 'SLOT')) {
|
2017-03-17 00:37:07 +00:00
|
|
|
var parentNode = node;
|
|
|
|
|
while (parentNode.parentNode) {
|
|
|
|
|
parentNode = parentNode.parentNode;
|
|
|
|
|
}
|
|
|
|
|
if (parentNode instanceof ShadowRoot) {
|
2018-04-11 13:37:37 +01:00
|
|
|
// If the element is <content> and we're inside a shadow DOM then just
|
2017-03-17 00:37:07 +00:00
|
|
|
// append the contents of the nodes that have been distributed into it.
|
|
|
|
|
var contentElem = /** @type {!Object} */ (node);
|
2018-04-11 13:37:37 +01:00
|
|
|
var shadowChildren;
|
|
|
|
|
if (bot.dom.isElement(node, 'CONTENT')) {
|
|
|
|
|
shadowChildren = contentElem.getDistributedNodes();
|
|
|
|
|
} else {
|
|
|
|
|
shadowChildren = contentElem.assignedNodes();
|
|
|
|
|
}
|
2024-01-20 23:23:01 +01:00
|
|
|
const childrenToTraverse =
|
|
|
|
|
shadowChildren.length > 0 ? shadowChildren : contentElem.childNodes;
|
|
|
|
|
goog.array.forEach(childrenToTraverse, function (node) {
|
2017-03-17 00:37:07 +00:00
|
|
|
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);
|
|
|
|
|
}
|
2017-01-17 20:52:02 +00:00
|
|
|
} 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;
|
2015-06-18 14:28:03 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-01-17 20:52:02 +00:00
|
|
|
} else {
|
|
|
|
|
// otherwise append the contents of an element as per normal.
|
|
|
|
|
bot.dom.appendVisibleTextLinesFromElementInComposedDom_(
|
|
|
|
|
castElem, lines);
|
2015-06-18 14:28:03 -07:00
|
|
|
}
|
2017-01-17 20:52:02 +00:00
|
|
|
}
|
|
|
|
|
};
|
2015-06-18 14:28:03 -07:00
|
|
|
|
2017-01-17 20:52:02 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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 &&
|
2018-04-18 14:20:37 +01:00
|
|
|
(elemOrText.assignedSlot != null ||
|
|
|
|
|
(elemOrText.getDestinationInsertionPoints &&
|
|
|
|
|
elemOrText.getDestinationInsertionPoints().length > 0)
|
|
|
|
|
);
|
2017-01-17 20:52:02 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @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);
|
2015-06-18 14:28:03 -07:00
|
|
|
});
|
2017-01-17 20:52:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
};
|