From b400b48ed2403c4a3a56fdc8b5c09a4138867b70 Mon Sep 17 00:00:00 2001 From: Jim Evans Date: Thu, 27 Mar 2025 15:49:37 +0100 Subject: [PATCH] Refactor isDisplayed atom to remove Closure library code The Closure library adds a lot of complexity to the compiled and minified atoms, including for isDisplayed. Additionally, the web platform has evolved in the time since the atoms were originally written, with additional methods added that help simplify the atom. This commit removes (nearly) all of the Closure code from the specific atom used by the isDisplayed method in the various language bindings. --- javascript/atoms/dom.js | 288 ++++++++++++++++++++++++++++++---------- 1 file changed, 219 insertions(+), 69 deletions(-) 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