Skip to content

Commit b400b48

Browse files
committed
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.
1 parent 014d1c3 commit b400b48

File tree

1 file changed

+219
-69
lines changed

1 file changed

+219
-69
lines changed

javascript/atoms/dom.js

Lines changed: 219 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -457,98 +457,247 @@ bot.dom.getCascadedStyle_ = function(elem, styleName) {
457457
* @return {boolean} Whether or not the element is visible.
458458
* @private
459459
*/
460-
bot.dom.isShown_ = function(elem, ignoreOpacity, parentsDisplayedFn) {
461-
if (!bot.dom.isElement(elem)) {
462-
throw new Error('Argument to isShown must be of type Element');
460+
bot.dom.isShown_ = function (elem, ignoreOpacity, parentsDisplayedFn) {
461+
462+
const isMap = (elem) => {
463+
const getAreaRelativeRect = (area) => {
464+
const shape = area.shape.toLowerCase();
465+
const coords = area.coords.split(',');
466+
if (shape == 'rect' && coords.length == 4) {
467+
const [x, y] = coords;
468+
return new DOMRect(x, y,coords[2] - x, coords[3] - y);
469+
} else if (shape == 'circle' && coords.length == 3) {
470+
const [centerX, centerY, radius] = coords;
471+
return new DOMRect(centerX - radius, centerY - radius, 2 * radius, 2 * radius);
472+
} else if (shape == 'poly' && coords.length > 2) {
473+
let [minX, minY] = coords, maxX = minX, maxY = minY;
474+
for (var i = 2; i + 1 < coords.length; i += 2) {
475+
minX = Math.min(minX, coords[i]);
476+
maxX = Math.max(maxX, coords[i]);
477+
minY = Math.min(minY, coords[i + 1]);
478+
maxY = Math.max(maxY, coords[i + 1]);
479+
}
480+
return new DOMRect(minX, minY, maxX - minX, maxY - minY);
481+
}
482+
return new DOMRect();
483+
};
484+
485+
// If not a <map> or <area>, return null indicating so.
486+
const isMap = elem instanceof HTMLMapElement;
487+
if (!isMap && !(elem instanceof HTMLAreaElement)) {
488+
return null;
489+
}
490+
491+
// Get the <map> associated with this element, or null if none.
492+
const map = isMap ? elem : elem.closest('map');
493+
494+
let image = null, rect = null;
495+
if (map && map.name) {
496+
const mapDoc = map.ownerDocument;
497+
image = mapDoc.querySelector(`*[usemap="#${map.name}"]`);
498+
499+
if (image) {
500+
rect = image.getBoundingClientRect();
501+
if (!isMap && elem.shape.toLowerCase() !== 'default') {
502+
// Shift and crop the relative area rectangle to the map.
503+
const relRect = getAreaRelativeRect(elem);
504+
const relX = Math.min(Math.max(relRect.left, 0), rect.width);
505+
const relY = Math.min(Math.max(relRect.top, 0), rect.height);
506+
const width = Math.min(relRect.width, rect.width - relX);
507+
const height = Math.min(relRect.height, rect.height - relY);
508+
rect = new DOMRect(relX + rect.left, relY + rect.top, width, height);
509+
}
510+
}
511+
}
512+
513+
return {image: image, rect: rect || new DOMRect()};
514+
};
515+
516+
const checkIsHiddenByOverflow = (elem, style) => {
517+
const htmlElement = elem.ownerDocument.documentElement;
518+
519+
const getNearestOverflowAncestor = (e, style) => {
520+
const elementPosition = style.getPropertyValue('position');
521+
const canBeOverflowed = (container) => {
522+
const containerStyle = getComputedStyle(container);
523+
if (container === htmlElement) {
524+
return true;
525+
}
526+
const containerDisplay = containerStyle.getPropertyValue('display');
527+
if (containerDisplay.startsWith('inline') || containerDisplay === 'contents') {
528+
return false;
529+
}
530+
const containerPosition = containerStyle.getPropertyValue('position');
531+
if (elementPosition === 'absolute' && containerPosition === 'static') {
532+
return false;
533+
}
534+
return true;
535+
};
536+
if (elementPosition === 'fixed') {
537+
return e === htmlElement ? null : htmlElement;
538+
}
539+
let container = e.parentElement;
540+
while (container && !canBeOverflowed(container)) {
541+
container = container.parentElement;
542+
}
543+
return container;
544+
};
545+
546+
// Walk up the tree, examining each ancestor capable of displaying
547+
// overflow.
548+
let parentElement = getNearestOverflowAncestor(elem, style);
549+
while (parentElement) {
550+
const parentStyle = getComputedStyle(parentElement);
551+
const parentOverflowX = parentStyle.getPropertyValue('overflow-x');
552+
const parentOverflowY = parentStyle.getPropertyValue('overflow-y');
553+
554+
// If the container has overflow:visible, the element cannot be hidden in its overflow.
555+
if (parentOverflowX !== 'visible' || parentOverflowY !== 'visible') {
556+
const parentRect = parentElement.getBoundingClientRect();
557+
558+
// Zero-sized containers without overflow:visible hide all descendants.
559+
if (parentRect.width === 0 || parentRect.height === 0) {
560+
return true;
561+
}
562+
563+
const elementRect = elem.getBoundingClientRect();
564+
565+
// Check "underflow": if an element is to the left or above the container
566+
// and overflow is "hidden" in the proper direction, the element is hidden.
567+
const isLeftOf = elementRect.x + elementRect.width < parentRect.x;
568+
const isAbove = elementRect.y + elementRect.height < parentRect.y;
569+
if ((isLeftOf && parentOverflowX === 'hidden') ||
570+
(isAbove && parentOverflowY === 'hidden')) {
571+
return true;
572+
}
573+
574+
// Check "overflow": if an element is to the right or below a container
575+
// and overflow is "hidden" in the proper direction, the element is hidden.
576+
const isRightOf = elementRect.x >= parentRect.x + parentRect.width;
577+
const isBelow = elementRect.y >= parentRect.y + parentRect.height;
578+
if ((isRightOf && parentOverflowX === 'hidden') ||
579+
(isBelow && parentOverflowY === 'hidden')) {
580+
return true;
581+
} else if ((isRightOf && parentOverflowX !== 'visible') ||
582+
(isBelow && parentOverflowY !== 'visible')) {
583+
// Special case for "fixed" elements: whether it is hidden by
584+
// overflow depends on the scroll position of the parent element
585+
if (style.getPropertyValue('position') === 'fixed') {
586+
const scrollPosition = parentElement.tagName === 'HTML' ?
587+
{
588+
x: parentElement.ownerDocument.scrollingElement.scrollLeft,
589+
y: parentElement.ownerDocument.scrollingElement.scrollTop
590+
} :
591+
{
592+
x: parentElement.scrollLeft,
593+
y: parentElement.scrollTop
594+
};
595+
if ((elementRect.x >= htmlElement.scrollWidth - scrollPosition.x) ||
596+
(elementRect.y >= htmlElement.scrollHeight - scrollPosition.y)) {
597+
return true;
598+
}
599+
}
600+
}
601+
}
602+
parentElement = getNearestOverflowAncestor(parentElement, parentStyle);
603+
}
604+
return false;
605+
};
606+
607+
if (elem.nodeType !== Node.ELEMENT_NODE) {
608+
throw new Error(`Argument to isShown must be of type Element`);
463609
}
464610

465-
// By convention, BODY element is always shown: BODY represents the document
466-
// and even if there's nothing rendered in there, user can always see there's
467-
// the document.
468-
if (bot.dom.isElement(elem, goog.dom.TagName.BODY)) {
611+
// The <body> element is always visible
612+
if (elem.tagName === 'BODY') {
469613
return true;
470614
}
471615

472-
// Option or optgroup is shown iff enclosing select is shown (ignoring the
473-
// select's opacity).
474-
if (bot.dom.isElement(elem, goog.dom.TagName.OPTION) ||
475-
bot.dom.isElement(elem, goog.dom.TagName.OPTGROUP)) {
476-
var select = /**@type {Element}*/ (goog.dom.getAncestor(elem, function(e) {
477-
return bot.dom.isElement(e, goog.dom.TagName.SELECT);
478-
}));
479-
return !!select && bot.dom.isShown_(select, true, parentsDisplayedFn);
616+
// <option> and <optgroup> elements are visible if their enclosing <select>
617+
// is visible.
618+
if (elem instanceof HTMLOptionElement || elem instanceof HTMLOptGroupElement) {
619+
const select = elem.closest('select');
620+
if (!select) {
621+
return false;
622+
}
623+
if (select instanceof HTMLSelectElement) {
624+
return bot.dom.isShown_(select, true, parentsDisplayedFn);
625+
}
480626
}
481627

482-
// Image map elements are shown if image that uses it is shown, and
483-
// the area of the element is positive.
484-
var imageMap = bot.dom.maybeFindImageMap_(elem);
628+
// <map> and <area> elements are visible if the images used by them are
629+
// visible.
630+
const imageMap = isMap(elem);
485631
if (imageMap) {
486632
return !!imageMap.image &&
487633
imageMap.rect.width > 0 && imageMap.rect.height > 0 &&
488634
bot.dom.isShown_(
489635
imageMap.image, ignoreOpacity, parentsDisplayedFn);
490636
}
491637

492-
// Any hidden input is not shown.
493-
if (bot.dom.isElement(elem, goog.dom.TagName.INPUT) &&
494-
elem.type.toLowerCase() == 'hidden') {
495-
return false;
496-
}
497-
498-
// Any NOSCRIPT element is not shown.
499-
if (bot.dom.isElement(elem, goog.dom.TagName.NOSCRIPT)) {
500-
return false;
501-
}
502-
503-
// Any element with hidden/collapsed visibility is not shown.
504-
var visibility = bot.dom.getEffectiveStyle(elem, 'visibility');
505-
if (visibility == 'collapse' || visibility == 'hidden') {
506-
return false;
507-
}
638+
// Defined as a function because the Closure compiler does not understand
639+
// the checkVisibility method on Element.
640+
const checkElementVisibility = (elem) => {
641+
return elem.checkVisibility({
642+
visibilityProperty: true,
643+
opacityProperty: !ignoreOpacity,
644+
contentVisibilityAuto: true
645+
});
646+
};
508647

509-
if (!parentsDisplayedFn(elem)) {
648+
const visibility = checkElementVisibility(elem);
649+
if (!visibility) {
510650
return false;
511651
}
512652

513-
// Any transparent element is not shown.
514-
if (!ignoreOpacity && bot.dom.getOpacity(elem) == 0) {
515-
return false;
653+
const style = getComputedStyle(elem);
654+
if (!style) {
655+
return true;
516656
}
517657

518-
// Any element without positive size dimensions is not shown.
519-
function positiveSize(e) {
520-
var rect = bot.dom.getClientRect(e);
521-
if (rect.height > 0 && rect.width > 0) {
658+
const hasPositiveSize = (elem, style) => {
659+
const rect = elem.getBoundingClientRect();
660+
if (rect.width > 0 && rect.height > 0) {
522661
return true;
523662
}
663+
524664
// A vertical or horizontal SVG Path element will report zero width or
525665
// height but is "shown" if it has a positive stroke-width.
526-
if (bot.dom.isElement(e, 'PATH') && (rect.height > 0 || rect.width > 0)) {
527-
var strokeWidth = bot.dom.getEffectiveStyle(e, 'stroke-width');
666+
if (elem.tagName.toUpperCase() === 'PATH' && (rect.height > 0 || rect.width > 0)) {
667+
const strokeWidth = style.getPropertyValue('stroke-width');
528668
return !!strokeWidth && (parseInt(strokeWidth, 10) > 0);
529669
}
670+
530671
// Zero-sized elements should still be considered to have positive size
531672
// if they have a child element or text node with positive size, unless
532673
// the element has an 'overflow' style of 'hidden'.
533-
return bot.dom.getEffectiveStyle(e, 'overflow') != 'hidden' &&
534-
goog.array.some(e.childNodes, function(n) {
535-
return n.nodeType == goog.dom.NodeType.TEXT ||
536-
(bot.dom.isElement(n) && positiveSize(n));
537-
});
538-
}
539-
if (!positiveSize(elem)) {
674+
return (style.getPropertyValue('overflow-x') !== 'hidden' || style.getPropertyValue('overflow-y') !== 'hidden') &&
675+
[...elem.childNodes].filter(child => {
676+
return child.nodeType === Node.TEXT_NODE ||
677+
(child.nodeType === Node.ELEMENT_NODE && hasPositiveSize(child, getComputedStyle(child)));
678+
}).length;
679+
};
680+
681+
if (!hasPositiveSize(elem, style)) {
540682
return false;
541683
}
542684

543-
// Elements that are hidden by overflow are not shown.
544-
function hiddenByOverflow(e) {
545-
return bot.dom.getOverflowState(e) == bot.dom.OverflowState.HIDDEN &&
546-
goog.array.every(e.childNodes, function(n) {
547-
return !bot.dom.isElement(n) || hiddenByOverflow(n) ||
548-
!positiveSize(n);
549-
});
550-
}
551-
return !hiddenByOverflow(elem);
685+
// Elements are hidden by overflow if their an ancestor container has
686+
// overflow hidden and all children are also hidden because the child node
687+
// is not an element node, zero size, or are themselves hidden by overflow.
688+
const hiddenByOverflow = (elem, style) => {
689+
const children = [...elem.childNodes];
690+
return checkIsHiddenByOverflow(elem, style) &&
691+
children.filter(child => {
692+
const isElement = child.nodeType === Node.ELEMENT_NODE;
693+
const childStyle = isElement ? getComputedStyle(child) : null;
694+
return !isElement ||
695+
hiddenByOverflow(child, childStyle) ||
696+
!hasPositiveSize(child, childStyle);
697+
}).length === children.length;
698+
};
699+
700+
return !hiddenByOverflow(elem, style);
552701
};
553702

554703

@@ -577,16 +726,17 @@ bot.dom.isShown = function(elem, opt_ignoreOpacity) {
577726
* @return {!boolean}
578727
*/
579728
function displayed(e) {
580-
if (bot.dom.isElement(e)) {
581-
var elem = /** @type {!Element} */ (e);
582-
if (bot.dom.getEffectiveStyle(elem, 'display') == 'none') {
729+
if (e.nodeType === Node.ELEMENT_NODE) {
730+
const elem = /** @type {!Element} */ (e);
731+
const elemStyle = getComputedStyle(elem);
732+
if (elemStyle && elemStyle.getPropertyValue('display') === 'none') {
583733
return false;
584734
}
585735
}
586736

587-
var parent = bot.dom.getParentNodeInComposedDom(e);
737+
let parent = bot.dom.getParentNodeInComposedDom(e);
588738

589-
if (bot.dom.IS_SHADOW_DOM_ENABLED && (parent instanceof ShadowRoot)) {
739+
if ((typeof ShadowRoot === 'function') && (parent instanceof ShadowRoot)) {
590740
if (parent.host.shadowRoot && parent.host.shadowRoot !== parent) {
591741
// There is a younger shadow root, which will take precedence over
592742
// the shadow this element is in, thus this element won't be
@@ -597,15 +747,15 @@ bot.dom.isShown = function(elem, opt_ignoreOpacity) {
597747
}
598748
}
599749

600-
if (parent && (parent.nodeType == goog.dom.NodeType.DOCUMENT ||
601-
parent.nodeType == goog.dom.NodeType.DOCUMENT_FRAGMENT)) {
750+
if (parent && (parent.nodeType === Node.DOCUMENT_NODE ||
751+
parent.nodeType === Node.DOCUMENT_FRAGMENT_NODE)) {
602752
return true;
603753
}
604754

605755
// Child of DETAILS element is not shown unless the DETAILS element is open
606756
// or the child is a SUMMARY element.
607-
if (parent && bot.dom.isElement(parent, goog.dom.TagName.DETAILS) &&
608-
!parent.open && !bot.dom.isElement(e, goog.dom.TagName.SUMMARY)) {
757+
if (parent && parent.nodeType === Node.ELEMENT_NODE && parent.tagName.toUpperCase() === 'DETAILS' &&
758+
!parent.open && !(e.nodeType === Node.ELEMENT_NODE && e.tagName.toUpperCase() === 'SUMMARY')) {
609759
return false;
610760
}
611761

0 commit comments

Comments
 (0)