How can I tell if an element is in a shadow DOM?

2019-01-31 19:07发布

问题:

I have a project where I'm using the shadow DOM natively (not through a polyfill). I'd like to detect if a given element is contained within a shadow DOM or a light DOM.

I've looked through all of the properties on the elements, but there don't seem to be any which vary based on the type of DOM an element is in.

How can I determine if an element is part of a shadow DOM or a light DOM?


Here is an example of what is considered "shadow DOM" and "light DOM" for the purpose of this question.

 (light root) • Document
      (light)   • HTML
      (light)   | • BODY
      (light)   |   • DIV
(shadow root)   |     • ShadowRoot
     (shadow)   |       • DIV 
     (shadow)   |         • IFRAME 
 (light root)   |           • Document
      (light)   |             • HTML
      (light)   |             | • BODY
      (light)   |             |   • DIV
(shadow root)   |             |     • ShadowRoot
     (shadow)   |             |       • DIV
       (none)   |             • [Unattached DIV of second Document]
       (none)   • [Unattached DIV of first Document]

<!doctype html>
<title>
  isInShadow() test document - can not run in Stack Exchange's sandbox
</title>
<iframe src="about:blank"></iframe>
<script>

function isInShadow(element) {
  // TODO
}

function test() {
  //  (light root) • Document
  //       (light)   • HTML
  var html = document.documentElement;

  console.assert(isInShadow(html) === false);

  //       (light)   | • BODY
  var body = document.body;

  console.assert(isInShadow(body) === false);

  //       (light)   |   • DIV
  var div = document.createElement('div');
  body.appendChild(div);

  console.assert(isInShadow(div) === false);

  // (shadow root)   |     • ShadowRoot
  var divShadow = div.createShadowRoot();

  var shadowDiv = document.createElement('div');
  divShadow.appendChild(shadowDiv);

  //      (shadow)   |       • DIV 
  console.assert(isInShadow(shadowDiv) === true);

  //      (shadow)   |         • IFRAME 
  var iframe = document.querySelector('iframe');
  shadowDiv.appendChild(iframe);

  console.assert(isInShadow(iframe) === true);

  //  (light root)   |           • Document
  var iframeDocument = iframe.contentWindow.document;

  //       (light)   |             • HTML
  var iframeHtml = iframeDocument.documentElement;

  console.assert(isInShadow(iframeHtml) === false);

  //       (light)   |             | • BODY
  var iframeBody = iframeDocument.body;

  //
  console.assert(isInShadow(iframeHtml) === false);

  //       (light)   |             |   • DIV
  var iframeDiv = iframeDocument.createElement('div');
  iframeBody.appendChild(iframeDiv);
   
  console.assert(isInShadow(iframeDiv) === false);
   
  // (shadow root)   |             |     • ShadowRoot
  var iframeDivShadow = iframeDiv.createShadowRoot();

  //      (shadow)   |             |       • DIV
  var iframeDivShadowDiv = iframeDocument.createElement('div');
  iframeDivShadow.appendChild(iframeDivShadowDiv);
    
  console.assert(isInShadow(iframeDivShadowDiv) === true);
     
  //        (none)   |             • [Unattached DIV of second Document]
  var iframeUnattached = iframeDocument.createElement('div');
    
  console.assert(Boolean(isInShadow(iframeUnattached)) === false);

  //        (none)   • [Unattached DIV of first Document]
  var rootUnattached = document.createElement('div');
    
  console.assert(Boolean(isInShadow(rootUnattached)) === false);
}

onload = function main() {
  console.group('Testing');
  try {
    test();
    console.log('Testing complete.');
  } finally {
    console.groupEnd();
  }
}

</script>

回答1:

If you call a ShadowRoot's toString() method, it will return "[object ShadowRoot]". According to this fact, here's my approach:

function isInShadow(node) {
    var parent = (node && node.parentNode);
    while(parent) {
        if(parent.toString() === "[object ShadowRoot]") {
            return true;
        }
        parent = parent.parentNode;
    }
    return false;
}

EDIT

Jeremy Banks suggests an approach in another style of looping. This approach is a little different from mine: it also checks the passed node itself, which I didn't do.

function isInShadow(node) {
    for (; node; node = node.parentNode) {
        if (node.toString() === "[object ShadowRoot]") {
            return true;
        }
    }
    return false;
}

function isInShadow(node) {
    for (; node; node = node.parentNode) {
        if (node.toString() === "[object ShadowRoot]") {
            return true;
        }
    }
    return false;
}

console.group('Testing');

var lightElement = document.querySelector('div');    

console.assert(isInShadow(lightElement) === false);

var shadowChild = document.createElement('div');
lightElement.createShadowRoot().appendChild(shadowChild);

console.assert(isInShadow(shadowChild) === true);

var orphanedElement = document.createElement('div');

console.assert(isInShadow(orphanedElement) === false);

var orphanedShadowChild = document.createElement('div');
orphanedElement.createShadowRoot().appendChild(orphanedShadowChild);

console.assert(isInShadow(orphanedShadowChild) === true);

var fragmentChild = document.createElement('div');
document.createDocumentFragment().appendChild(fragmentChild);

console.assert(isInShadow(fragmentChild) === false);

console.log('Complete.');
console.groupEnd();
<div></div>



回答2:

⚠️ Warning: Deprecation Risk

The ::shadow pseudo-element is deprecated in and being removed from from the dynamic selector profile. The approach below only requires that it remain in the static selector profile, but it may also be deprecated and removed there in the future. Discussions are ongoing.

We can use Element's .matches() method to determine if an element is attached to a shadow DOM.

If and only if the element is in a shadow DOM, we will be able to match it by using the selector :host to identify elements that have a Shadow DOM, ::shadow to look in those shadow DOMs, and * and to match any descendant.

function isInShadow(element) {
  return element.matches(':host::shadow *');
}

function isInShadow(element) {
  return element.matches(':host::shadow *');
}

console.group('Testing');

var lightElement = document.querySelector('div');    

console.assert(isInShadow(lightElement) === false);

var shadowChild = document.createElement('div');
lightElement.createShadowRoot().appendChild(shadowChild);

console.assert(isInShadow(shadowChild) === true);

var orphanedElement = document.createElement('div');

console.assert(isInShadow(orphanedElement) === false);

var orphanedShadowChild = document.createElement('div');
orphanedElement.createShadowRoot().appendChild(orphanedShadowChild);

console.assert(isInShadow(orphanedShadowChild) === true);

var fragmentChild = document.createElement('div');
document.createDocumentFragment().appendChild(fragmentChild);

console.assert(isInShadow(fragmentChild) === false);

console.log('Complete.');
console.groupEnd();
<div></div>



回答3:

You can check if an element has a shadow parent like this:

function hasShadowParent(element) {
    while(element.parentNode && (element = element.parentNode)){
        if(element instanceof ShadowRoot){
            return true;
        }
    }
    return false;
}

This uses instanceof over .toString().



回答4:

Lets understand Light Dom:

The Light DOM is the user supplied DOM of an element that hosts a shadow root. For more info read at polymer-project.

https://www.polymer-project.org/platform/shadow-dom.html#shadow-dom-subtrees

This means: Light DOM is always relative to the next ancestor which hosts a shadow root.

An Element can be a part of the light dom of a custom element while it can be a part of the shadow root of another custom element at same time.

Example:

<my-custom-element>
    <shadowRoot>

        <custom-element>
            <div>I'm in Light DOM of "custom-element" and 
                    in Shadow Root of "my-custom-element" at same time</div>
        </custom-element>

    </shadowRoot>

    <div id="LDofMCE"> Im in Light DOM of "my-custom-element"</div>

<my-custom-element>

According to your question:

If you want to know if an element is in a shadow root, you just need to grab the element out of the document.

var isInLD = document.contains(NodeRef);
if(isInLD){
    console.alert('Element is in the only available "global Light DOM"(document)');
} else {
    console.log('Element is hidden in the shadow dom of some element');
}

The only Light DOM which is not preceeded by a shadow Root is part of the document, because Light DOM is relative as shown above.

It doesnt work backwards: if its part of the document its not in a Light DOM at all. You need to check if one of the ancestors is hosting a Shadow Root like suggested from Leo.

You can use this approach with other elements to. Just replace the "document" with e.g. "my-custom-element" and test if div#LDofMCE is in Light DOM relative to "my-custom-element".

Because of the lack of information about why you need this information i cant get closer...

EDIT:

It doesnt work backwards should be understand as follows:

Is this element in a Shadow Root?: document.contains() or the isInShadow(node) method from Leo deliver the answer.

"backwards" Question: Is this element in a Light DOM (In case you start searching relative to document)?: domcument.contains() does not deliver the answer because to be in a Light Dom - one of the elements ancestors needs to be a shadow host.

Come to the Point

  • Light DOM is relative.
  • An element can take part in a shadow root and in a light dom at the same time. there is no "is part of a shadow DOM OR a light DOM?"