如何判断元素是否在Shadow DOM中?

36

我有一个项目,其中我正在原生使用影子DOM(而不是通过polyfill)。我想检测给定的element是否包含在影子DOM或轻DOM中。

我已经查看了所有元素的属性,但似乎没有任何属性基于元素所在的DOM类型而变化。

我该如何确定元素是属于影子DOM还是轻DOM?


这里是为了这个问题而考虑的“影子DOM”和“轻DOM”的例子。

 (轻根)• 文档
     (轻)    • HTML
     (轻)    | • BODY
     (轻)    |   • DIV
(影子根)|     • ShadowRoot
     (影子)  |       • DIV 
     (影子)  |         • IFRAME 
(轻根)    |           • 文档
     (轻)    |             • HTML
     (轻)    |             | • BODY
     (轻)    |             |   • DIV
(影子根)|             |     • ShadowRoot
     (影子)  |             |       • DIV
       (无)  |             • [第二个文档的未连接DIV]
       (无)  • [第一个文档的未连接DIV]

<!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
也许现在你可以使用getNodeRoot()方法:https://dev59.com/1Z3ha4cB1Zd3GeqPXKD1 - Supersharp
7个回答

37
如果您调用ShadowRoot的toString()方法,它将返回"[object ShadowRoot]"。基于此事实,下面是我的解决方法:
function isInShadow(node) {
    var parent = (node && node.parentNode);
    while(parent) {
        if(parent.toString() === "[object ShadowRoot]") {
            return true;
        }
        parent = parent.parentNode;
    }
    return false;
}

编辑

Jeremy Banks提出了另一种循环方式的方法。这种方法与我的方法略有不同:它还检查传递的节点本身,而我没有这样做。

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>


在我看来,他应该在评论中建议更改循环。像那样的编辑只应该由发布答案的人来完成。 - Cerbrus
那是一次编辑,实际上改变了帖子的功能。 - Cerbrus
我通常比大多数Stack Overflow用户更支持积极编辑:像Wikipedians一样大胆地编辑,同时接受作者大胆地撤销你所做的一切的权利:P。然而,在这种情况下,我并没有打算改变这篇文章的功能。我忽略了这个差异——这是我的错误! - Jeremy
唉,我本来希望能够直接使用[Symbol.toStringTag]而不是.toString(),但看起来在ShadowRoots上并没有实现,尽管规范似乎暗示它应该被实现。 - Jeremy

15
您可以像这样检查元素是否有一个影子父级:

您可以检查元素是否具有影子父级,就像这样:

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

这里使用了instanceof而不是.toString()


3
然而,在iframe中可能会失败。 - Leo
失败的原因是什么?你能提供一个小例子吗? - Cerbrus
@Leo,那个链接已经失效了。 - try-catch-finally
2
@try-catch-finally 不用担心,这篇帖子基本上给出了相同的答案:文档中的window和iframe中的window不引用同一个对象。整个环境和本地构造函数也是如此。 - Leo

15

可以使用如下的getRootNode方法查找Shadow DOM内部的元素。

function isInShadow(node) {
   return node.getRootNode() instanceof ShadowRoot;
}

2
对于现代浏览器而言,我认为这是最好的答案。 - JoshuaCWebDeveloper

8

⚠️ 警告:废弃风险

::shadow 伪元素已被弃用并正在从动态选择器配置文件中移除。下面的方法仅需要保留在静态选择器配置文件中,但未来它也可能被弃用和移除。 讨论仍在继续

我们可以使用Element.matches() 方法来确定一个元素是否附加到了 Shadow DOM。

只有当元素在 Shadow DOM 中时,我们才能使用选择器 :host 来识别具有 Shadow DOM 的元素,::shadow 来查找这些 Shadow DOM,* 来匹配任何后代元素。

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>


6
有趣的是,这份文件展示了:host::shadow的使用,尽管我看到的描述意味着任何具有::shadow的元素必须必然是一个:host,这将使伪类非常冗余。我不确定是否实际上需要指定:host,因为我从规范中没有理解到某些原因,或者是由于实现限制。它也没有提到:root如何影响阴影DOM(如果有的话),Selectors 4和Shadow DOM规范也没有提及... - BoltClock
这个答案还有效吗? - Fez Vrasta

5

让我们了解轻量DOM:

轻量DOM是托管影子根的元素的用户提供的DOM。有关更多信息,请阅读polymer-project。

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

这意味着:轻型DOM始终相对于承载影子根的下一个祖先元素。

一个元素可以是自定义元素轻型DOM的一部分,同时也可以是另一个自定义元素的影子根的一部分。

示例:

<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>

根据您的问题:
如果您想知道一个元素是否在影子根中,您只需要从文档中获取该元素。
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.:唯一不带阴影根的光DOM是文档的一部分,因为如上所示,光DOM是相对的。 它不会往回工作:如果它是文档的一部分,则根本不在Light DOM中。您需要检查是否有祖先托管像Leo建议的Shadow Root。
您也可以使用此方法处理其他元素。仅将“文档”替换为例如“my-custom-element”,并测试div#LDofMCE是否相对于“my-custom-element”位于Light DOM中。
由于缺乏有关为什么需要此信息的信息,我无法更接近… 编辑: 它不会往回工作应理解为: 此元素是否位于阴影根中?:document.contains()或来自Leo的isInShadow(node)方法提供答案。
“向后”问题:此元素是否位于Light DOM中(如果从文档开始搜索)?:domcument.contains()不提供答案,因为要在Light Dom中 - 其中一个元素的祖先必须是阴影主机。

言归正传

  • 轻量级DOM是相对的。
  • 一个元素可以同时参与影子根和轻量级DOM,不存在“是属于影子DOM还是轻量级DOM?”的情况。

1
使用createElement创建一个元素,但不将其附加到文档中,该元素也返回false。这是您所说的“不向后工作”吗?老实说,我没有理解那句话。不过,在某种程度上,这是一个不错的尝试 :) - Leo

1

这可以检查节点或元素是否在影子DOM中,并适用于iframe

/**
 * @param {!Node} node
 * @return {!boolean}
 */
function isInShadowRoot(node) {
  return node.getRootNode().constructor.name === "ShadowRoot";
}

或者使用 instanceof

const inShadow = node.getRootNode() instanceof node.ownerDocument.defaultView.ShadowRoot

0
您可以通过以下方法检查是否在shadowDOM中包含任何元素:
/** element can be any HTMLELement or Node **/

const root = element && element.getRootNode();
const isOnShadowDOM = root instanceof ShadowRoot;
  • 第一行告诉我们任何元素的根父级是什么。例如,lightDOM元素的根通常为#document,而shadowed元素的根为#shadow-root

  • 第二行告诉我们这个根是否为shadowDOM。在这里,我们只需检查根元素是否为ShadowRoot的实例,如果是,则我们的const将为true,否则为false


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接