3

I need to find a very performant way to find out if a custom element or any of its parent elements has display: none;

First approach:

checkVisible() {
  let parentNodes = [];
  let el = this;
  while (!!(el = el.parentNode)) {
    parentNodes.push(el);
  }
  return [this, ...parentNodes].some(el => getComputedStyle(el).display === 'none') 
}

Is there anything that runs faster than this? Is this even a safe method?

The reason I need this: We have a <data-table> custom element (native webcomponent) which does very heavy lifting in its connectedCallback(). We have an application that has like 20-30 of those custom elements in a single page, which leads to IE 11 taking like 15 seconds until the page is rendered.

I need to delay initialisation of those <data-table> components which are initially not even visible, so I need a way to test inside the connectedCallback() if the element is visible (which it is not if it is in one of the 18 tabs initially not shown).

connexo
  • 53,704
  • 14
  • 91
  • 128
  • Could you use a MutationObserver instead? – Jonathan Rys Oct 29 '18 at 14:14
  • only thing I can see to be improved is to check to see if the current element is visible before looping over the parents.... And not building all of the parents up in a list.... – epascarello Oct 29 '18 at 14:14
  • I'd use recursion. Also, shouldn't this be in the code review section? –  Oct 29 '18 at 14:15
  • 2
    Why not check the parent visibility inside of the while? that way you save one loop. Further, I think inside of that while is missing `el = el.parentNode;` – Ele Oct 29 '18 at 14:16
  • 2
    Are you not interested in cases where the element might be hidden by a rule coming from the stylesheet? (Because `el.style.display === 'none'` would only be true for style set inline.) – misorude Oct 29 '18 at 14:23
  • @Ele thx for pointing out, ofc that was missing. Added. – connexo Oct 29 '18 at 14:29
  • @misorude Ofc I'd like to catch those elements as well. – connexo Oct 29 '18 at 14:35
  • Then you'll have to go with getComputedStyle as in Chris G’s answer, because `el.style` doesn't cover that. – misorude Oct 29 '18 at 14:37
  • @JonathanRys How would you configure such a mutation observer, what would it listen to? – connexo Oct 29 '18 at 15:16
  • @connexo Not really sure. I don't know exactly what your use case is so I was honestly asking. It seems like a good pattern, unsure how you would adapt it or your use case to make it work. – Jonathan Rys Oct 29 '18 at 15:22

3 Answers3

7

The easiest way to see if an element or its parent has display:none is to use el.offsetParent.

const p1 = document.getElementById('parent1');
const p2 = document.getElementById('parent2');
const c1 = document.getElementById('child1');
const c2 = document.getElementById('child2');
const btn = document.getElementById('btn');
const output = document.getElementById('output');

function renderVisibility() {
  const p1state = isElementVisible(p1) ? 'is visible' : 'is not visible';
  const p2state = isElementVisible(p2) ? 'is visible' : 'is not visible';
  const c1state = isElementVisible(c1) ? 'is visible' : 'is not visible';
  const c2state = isElementVisible(c2) ? 'is visible' : 'is not visible';
  
  output.innerHTML = `Parent 1 ${p1state}<br>Parent 2 ${p2state}<br/>Child 1 ${c1state}<br/>Child 2 ${c2state}`;
}

function isElementVisible(el) {
  return !!el.offsetParent;
}

function toggle() {
  p1.style.display = (p1.style.display ? '' : 'none');
  p2.style.display = (p2.style.display ? '' : 'none');
  renderVisibility();
}

btn.addEventListener('click', toggle),
renderVisibility();
<div id="parent1" style="display:none">
  <div id="child1">child 1</div>
</div>
<div id="parent2">
  <div id="child2">second child</div>
</div>
<button id="btn">Toggle</button>
<hr>
<div id="output"></div>

This code converts el.offsetParent into a boolean that indicates if the element is showing or not.

This only works for display:none

Intervalia
  • 10,248
  • 2
  • 30
  • 60
  • Will also report false for elements that are not in the doc. (should be correct for most cases, but just saying.) If it is a problem, then one can just add `&& !!el.parentNode;` – Kaiido Nov 09 '18 at 14:56
  • :) If it isn't in the DOM then isn't it hidden?? ;) Just kidding. Yeah your addition would account for that. – Intervalia Nov 09 '18 at 17:58
  • Yes it is hidden, but its computed display css rule is not necessarily set to `"none"`. – Kaiido Nov 10 '18 at 04:23
  • 1
    @Intervalia, beware of the case when the element CSS `position` property is set to `fixed`, in which case its `offsetParent` is `null` and you need to assess the element `clientWidth` and `clientHeight` properties, cf. https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent. – Édouard Mercier Sep 14 '20 at 10:25
3

Not sure about performance, but it should be faster than your approach at least:

HTMLElement.prototype.isInvisible = function() {
  if (this.style.display == 'none') return true;
  if (getComputedStyle(this).display === 'none') return true;
  if (this.parentNode.isInvisible) return this.parentNode.isInvisible();
  return false;
};
  • Please elaborate on why you expect that would be faster (afaik `window.getComputedStyle` is a very expensive operation). – connexo Oct 30 '18 at 17:54
  • 1
    @connexo I added the basic check to my code so `getComputedStyle` only runs when it needs to. Still: a) using `getComputedStyle` is required if the element is styled via CSS rule / class and b) my version doesn't needlessly check the parents further up the tree –  Oct 30 '18 at 22:43
0

For a pure function that:

  • returns TRUE if and only if neither the element itself nor any of its parents up to the document itself have style display === 'none'
  • returns FALSE if and only if either the element itself or any parent element up to the document itself have style display === 'none'

You can define the below function and then call it on the element you wish to validate:

function isVisible(element) {
    // Start with the element itself and move up the DOM tree
    for (let el = element; el && el !== document; el = el.parentNode) {
        // If current element has display property 'none', return false
        if (getComputedStyle(el).display === "none") {
            return false;
        }
    }
    // Neither element itself nor any parents have display 'none', so return true
    return true;
}