diff --git a/javascript/atoms/dom.js b/javascript/atoms/dom.js index 1f521f06a99a5..85aa7dddb6739 100644 --- a/javascript/atoms/dom.js +++ b/javascript/atoms/dom.js @@ -457,31 +457,177 @@ bot.dom.getCascadedStyle_ = function(elem, styleName) { * @return {boolean} Whether or not the element is visible. * @private */ -bot.dom.isShown_ = function(elem, ignoreOpacity, parentsDisplayedFn) { - if (!bot.dom.isElement(elem)) { - throw new Error('Argument to isShown must be of type Element'); +bot.dom.isShown_ = function (elem, ignoreOpacity, parentsDisplayedFn) { + + const isMap = (elem) => { + const getAreaRelativeRect = (area) => { + const shape = area.shape.toLowerCase(); + const coords = area.coords.split(','); + if (shape == 'rect' && coords.length == 4) { + const [x, y] = coords; + return new DOMRect(x, y,coords[2] - x, coords[3] - y); + } else if (shape == 'circle' && coords.length == 3) { + const [centerX, centerY, radius] = coords; + return new DOMRect(centerX - radius, centerY - radius, 2 * radius, 2 * radius); + } else if (shape == 'poly' && coords.length > 2) { + let [minX, minY] = coords, 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 DOMRect(minX, minY, maxX - minX, maxY - minY); + } + return new DOMRect(); + }; + + // If not a or , return null indicating so. + const isMap = elem instanceof HTMLMapElement; + if (!isMap && !(elem instanceof HTMLAreaElement)) { + return null; + } + + // Get the associated with this element, or null if none. + const map = isMap ? elem : elem.closest('map'); + + let image = null, rect = null; + if (map && map.name) { + const mapDoc = map.ownerDocument; + image = mapDoc.querySelector(`*[usemap="#${map.name}"]`); + + if (image) { + rect = image.getBoundingClientRect(); + if (!isMap && elem.shape.toLowerCase() !== 'default') { + // Shift and crop the relative area rectangle to the map. + const relRect = getAreaRelativeRect(elem); + const relX = Math.min(Math.max(relRect.left, 0), rect.width); + const relY = Math.min(Math.max(relRect.top, 0), rect.height); + const width = Math.min(relRect.width, rect.width - relX); + const height = Math.min(relRect.height, rect.height - relY); + rect = new DOMRect(relX + rect.left, relY + rect.top, width, height); + } + } + } + + return {image: image, rect: rect || new DOMRect()}; + }; + + const checkIsHiddenByOverflow = (elem, style) => { + const htmlElement = elem.ownerDocument.documentElement; + + const getNearestOverflowAncestor = (e, style) => { + const elementPosition = style.getPropertyValue('position'); + const canBeOverflowed = (container) => { + const containerStyle = getComputedStyle(container); + if (container === htmlElement) { + return true; + } + const containerDisplay = containerStyle.getPropertyValue('display'); + if (containerDisplay.startsWith('inline') || containerDisplay === 'contents') { + return false; + } + const containerPosition = containerStyle.getPropertyValue('position'); + if (elementPosition === 'absolute' && containerPosition === 'static') { + return false; + } + return true; + }; + if (elementPosition === 'fixed') { + return e === htmlElement ? null : htmlElement; + } + let container = e.parentElement; + while (container && !canBeOverflowed(container)) { + container = container.parentElement; + } + return container; + }; + + // Walk up the tree, examining each ancestor capable of displaying + // overflow. + let parentElement = getNearestOverflowAncestor(elem, style); + while (parentElement) { + const parentStyle = getComputedStyle(parentElement); + const parentOverflowX = parentStyle.getPropertyValue('overflow-x'); + const parentOverflowY = parentStyle.getPropertyValue('overflow-y'); + + // If the container has overflow:visible, the element cannot be hidden in its overflow. + if (parentOverflowX !== 'visible' || parentOverflowY !== 'visible') { + const parentRect = parentElement.getBoundingClientRect(); + + // Zero-sized containers without overflow:visible hide all descendants. + if (parentRect.width === 0 || parentRect.height === 0) { + return true; + } + + const elementRect = elem.getBoundingClientRect(); + + // Check "underflow": if an element is to the left or above the container + // and overflow is "hidden" in the proper direction, the element is hidden. + const isLeftOf = elementRect.x + elementRect.width < parentRect.x; + const isAbove = elementRect.y + elementRect.height < parentRect.y; + if ((isLeftOf && parentOverflowX === 'hidden') || + (isAbove && parentOverflowY === 'hidden')) { + return true; + } + + // Check "overflow": if an element is to the right or below a container + // and overflow is "hidden" in the proper direction, the element is hidden. + const isRightOf = elementRect.x >= parentRect.x + parentRect.width; + const isBelow = elementRect.y >= parentRect.y + parentRect.height; + if ((isRightOf && parentOverflowX === 'hidden') || + (isBelow && parentOverflowY === 'hidden')) { + return true; + } else if ((isRightOf && parentOverflowX !== 'visible') || + (isBelow && parentOverflowY !== 'visible')) { + // Special case for "fixed" elements: whether it is hidden by + // overflow depends on the scroll position of the parent element + if (style.getPropertyValue('position') === 'fixed') { + const scrollPosition = parentElement.tagName === 'HTML' ? + { + x: parentElement.ownerDocument.scrollingElement.scrollLeft, + y: parentElement.ownerDocument.scrollingElement.scrollTop + } : + { + x: parentElement.scrollLeft, + y: parentElement.scrollTop + }; + if ((elementRect.x >= htmlElement.scrollWidth - scrollPosition.x) || + (elementRect.y >= htmlElement.scrollHeight - scrollPosition.y)) { + return true; + } + } + } + } + parentElement = getNearestOverflowAncestor(parentElement, parentStyle); + } + return false; + }; + + if (elem.nodeType !== Node.ELEMENT_NODE) { + throw new Error(`Argument to isShown must be of type Element`); } - // By convention, BODY element is always shown: BODY represents the document - // and even if there's nothing rendered in there, user can always see there's - // the document. - if (bot.dom.isElement(elem, goog.dom.TagName.BODY)) { + // The element is always visible + if (elem.tagName === 'BODY') { return true; } - // Option or optgroup is shown iff enclosing select is shown (ignoring the - // select's opacity). - if (bot.dom.isElement(elem, goog.dom.TagName.OPTION) || - bot.dom.isElement(elem, goog.dom.TagName.OPTGROUP)) { - var select = /**@type {Element}*/ (goog.dom.getAncestor(elem, function(e) { - return bot.dom.isElement(e, goog.dom.TagName.SELECT); - })); - return !!select && bot.dom.isShown_(select, true, parentsDisplayedFn); + // elements are visible if their enclosing