* Copyright (C) 2017 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
* check if element is visible
* @param {HTMLElement} elem element to check
* @return {Boolean} true if element is within viewport
*/
export default function isElementDisplayed (element: Element): boolean {
function nodeIsElement(node?: Element) {
if (!node) {
return false
}
switch (node.nodeType) {
case Node.ELEMENT_NODE:
case Node.DOCUMENT_NODE:
case Node.DOCUMENT_FRAGMENT_NODE:
return true
default:
return false
}
}
function parentElementForElement(element?: Element) {
if (!element) {
return null
}
return enclosingNodeOrSelfMatchingPredicate(element.parentNode as ParentNode, nodeIsElement)
}
function enclosingNodeOrSelfMatchingPredicate(targetNode: ParentNode | HTMLElement, predicate: Function) {
for (
let node: ParentNode = targetNode;
node && node !== (targetNode as Node).ownerDocument;
node = (node as HTMLElement).parentNode as ParentNode
) {
if (predicate(node)) {
return node
}
}
return null
}
function enclosingElementOrSelfMatchingPredicate(targetElement: HTMLElement | Document, predicate: Function) {
for (
let element: HTMLElement | ParentNode = targetElement;
element && element !== targetElement.ownerDocument;
element = parentElementForElement(element as HTMLElement) as HTMLElement
) {
if (predicate(element)) {
return element
}
}
return null
}
function cascadedStylePropertyForElement(
element?: Element | ParentNode | ShadowRoot,
property?: string
): string | null {
if (!element || !property) {
return null
}
if ('ShadowRoot' in window && element instanceof window.ShadowRoot) {
element = element.host
}
const computedStyle = window.getComputedStyle(element as Element)
const computedStyleProperty = computedStyle.getPropertyValue(property)
if (computedStyleProperty && computedStyleProperty !== 'inherit') {
return computedStyleProperty
}
const parentElement = parentElementForElement(element as Element) as ParentNode
return cascadedStylePropertyForElement(parentElement, property)
}
function elementHasBoundingBox(element: Element): boolean {
const boundingBox = element.getBoundingClientRect()
return boundingBox.width > 0 && boundingBox.height > 0
}
function elementSubtreeHasNonZeroDimensions(element: Element): boolean {
if (elementHasBoundingBox(element)) {
return true
}
const boundingBox = element.getBoundingClientRect()
if (element.tagName.toUpperCase() === 'PATH' && boundingBox.width + boundingBox.height > 0) {
const strokeWidth = cascadedStylePropertyForElement(element, 'stroke-width')
return !!strokeWidth && (parseInt(strokeWidth, 10) > 0)
}
const cascadedOverflow = cascadedStylePropertyForElement(element, 'overflow')
if (cascadedOverflow === 'hidden') {
return false
}
return [].some.call(element.childNodes, function (childNode: Element) {
if (childNode.nodeType === Node.TEXT_NODE) {
return true
}
if (nodeIsElement(childNode)) {
return elementSubtreeHasNonZeroDimensions(childNode)
}
return false
})
}
function elementOverflowsContainer(element: Element) {
const cascadedOverflow = cascadedStylePropertyForElement(element, 'overflow')
if (cascadedOverflow !== 'hidden') {
return false
}
return true
}
function isElementSubtreeHiddenByOverflow (element: Element): boolean {
if (!element) {
return false
}
if (!elementOverflowsContainer(element)) {
return false
}
if (!element.childNodes.length) {
return false
}
return [].every.call(element.childNodes, function (childNode: Element) {
if (childNode.nodeType === Node.TEXT_NODE) {
return false
}
if (!nodeIsElement(childNode)) {
return true
}
if (!elementSubtreeHasNonZeroDimensions(childNode)) {
return true
}
return isElementSubtreeHiddenByOverflow(childNode)
})
}
function isElementInsideShadowRoot (element: Element): boolean {
if (!element) {
return false
}
if (element.parentNode && (element.parentNode as ShadowRoot).host) {
return true
}
return isElementInsideShadowRoot(element.parentNode as Element)
}
if (
!isElementInsideShadowRoot(element) &&
(
typeof document.contains === 'function'
? !document.contains(element)
: !document.body.contains(element)
)
) {
return false
}
switch (element.tagName.toUpperCase()) {
case 'BODY':
return true
case 'SCRIPT':
case 'NOSCRIPT':
return false
case 'OPTGROUP':
case 'OPTION': {
const enclosingSelectElement = enclosingNodeOrSelfMatchingPredicate(element, function (e: Element) {
return e.tagName.toUpperCase() === 'SELECT'
})
return isElementDisplayed(enclosingSelectElement as Element)
}
case 'INPUT':
if ((element as HTMLInputElement).type === 'hidden') {
return false
}
break
default:
break
}
if (cascadedStylePropertyForElement(element, 'visibility') !== 'visible') {
return false
}
const hasAncestorWithZeroOpacity = !!enclosingElementOrSelfMatchingPredicate(element as HTMLElement, function (e: Element) {
return Number(cascadedStylePropertyForElement(e, 'opacity')) === 0
})
const hasAncestorWithDisplayNone = !!enclosingElementOrSelfMatchingPredicate(element as HTMLElement, function (e: Element) {
return cascadedStylePropertyForElement(e, 'display') === 'none'
})
if (hasAncestorWithZeroOpacity || hasAncestorWithDisplayNone) {
return false
}
if (!elementSubtreeHasNonZeroDimensions(element)) {
return false
}
if (isElementSubtreeHiddenByOverflow(element) && !elementHasBoundingBox(element)) {
return false
}
return true
}