如何从一个含有 Shadow DOM 的模板中的 HTML 元素中移除影子根?

15
我正在使用Chrome Canary (33.0.1712.3) 探索导入、模板、影子DOM和自定义元素。在网格布局中,我有一个特定的内容元素(显示区域),将显示来自文件的不同Web组件或克隆的轻DOM片段。
然而,一旦添加了影子DOM,我就无法重新显示普通的HTML DOM,因为我不知道如何删除影子根。一旦创建,影子根将会一直存在并干扰普通DOM的渲染。(我已查看了各种W3C规范,如Web组件介绍、影子DOM、模板、Bidelman在HTML5 Rocks上的文章等)我在下面的简单示例中隔离了问题:
点击“显示普通的div”,点击“显示阴影模板”,然后点击“显示普通的div”。每次点击后,在devtools中检查输出。第三次点击后,按钮下方没有任何输出,并且在devtools中我看到:
<div id="content">
  #document-fragment
  <div id="plaindiv">Plain old div</div>
</div>

我需要添加什么内容到removeShadow()函数中以删除阴影根并将内容元素完全重置为其初始状态?

removing_shadows.html

<!DOCTYPE html>
<html lang="en">

<head>
  <meta http-equiv="content-type" content="text/html; charset=UTF-8"/>

  <template id="shadowedTemplateComponent">
    <style>
      div { background: lightgray; }
      #t { color: red; }
    </style>

    <div id="t">template</div>

    <script>console.log("Activated the shadowed template component.");</script>
  </template>

  <template id="plainDiv">
    <div id="plaindiv">Plain old div</div>
  </template>
</head>

<body>
<div>
  <input type="button" value="show plain old div" onclick="showPlainOldDiv()"/>
  <input type="button" value="show shadowed template" onclick="showShadowTemplate()"/>
  <div id="content"></div>
</div>

<script>
  function removeChildren(elt) {
    console.log('removing children: %s', elt);
    while (elt.firstChild) {
      elt.removeChild(elt.firstChild);
    }
  }
  function removeShadow(elt) {
    if (elt.shadowRoot) {
      console.log('removing shadow: %s', elt);
      removeChildren(elt.shadowRoot); // Leaves the shadow root property.
      // elt.shadowRoot = null; doesn't work
      // delete elt.shadowRoot; doesn't work
      // What goes here to delete the shadow root (#document-fragment in devtools)?
    }
  }

  function showPlainOldDiv() {
    console.log('adding a plain old div');
    var host = document.querySelector('#content');
    removeChildren(host);
    removeShadow(host);
    var template = document.querySelector('#plainDiv');
    host.appendChild(template.content.cloneNode(true));
  }

  function showShadowTemplate() {
    console.log('adding shadowed template component');
    var host = document.querySelector('#content');
    removeChildren(host);
    removeShadow(host);
    var template = document.querySelector('#shadowedTemplateComponent');
    var root = host.shadowRoot || host.webkitCreateShadowRoot();
    root.appendChild(template.content.cloneNode(true));
  }
</script>
</body>
</html>
4个回答

11

Shadow DOM规范从v0版本升级到了v1版本。

其中一项变化是,在v1版本中不再支持在自身上创建影子根节点,而宿主元素只能包含一个影子根节点。

因此,使用新的空白影子根节点替换影子根节点的方法似乎不再有效。

解决方案:

  • 如果host元素本身(例如您的示例中的div)除了容纳影子DOM之外没有其他特殊值,可以直接将host元素整个替换掉
  • 如果仍然想保留host元素,则使用类似于e.shadowRoot.innerHTML = ''的方式清除影子DOM可能已经足够了

5
您添加了一个shadow root之后就无法删除它,但是您可以用新的shadow root代替它。正如此处所述,最新的shadow root会获胜并成为渲染根。您可以用只包含<content>伪元素的新shadow root来替换原来的shadow root,并将所有内容从light DOM插入到shadow DOM中。在那时,据我所知,它将在功能上等同于完全没有shadow DOM。

1

rmcclellan是正确的,您无法真正“删除”ShadowRoot v2。但是,您可以伪造它。

OuterHTML部分解决方案

elementWithShadowDOMv2.outerHTML = elementWithShadowDOMv2.outerHTML;

然而,有一个重要的警告:尽管没有可见的变化,elementWithShadowDOMv2仍然引用已被销毁的带有ShadowDOMv2的元素,就好像调用了elementWithShadowDOMv2.parentNode.removeChild( elementWithShadowDOMv2 )。这也会“删除”元素上的事件监听器。请观察下面的演示。

var addShadowHere = document.getElementById("add-shadow-here");

addShadowHere.addEventListener("mouseenter", function() {
  addShadowHere.style.border = '2em solid blue';
});
addShadowHere.addEventListener("mouseleave", function() {
  addShadowHere.style.border = '';
});

var shadow = addShadowHere.attachShadow({mode:"open"});
var button = shadow.appendChild(document.createElement("button"));

button.textContent = "Click Here to Destroy The ShadowDOMv2";

button.addEventListener("click", function() {
  addShadowHere.outerHTML = addShadowHere.outerHTML;
  
  update();
});

update();

function update() {
  // This just displays the current parent of the addShadowHere element
  document.getElementById("parent-value").value = "" + (
    addShadowHere.parentNode &&
      addShadowHere.parentNode.cloneNode(false).outerHTML
  );
}
<div id="add-shadow-here">Text Hidden By Shadow DOM</div>
addShadowHere.parentNode => <input readonly="" id="parent-value" />

请注意,在删除ShadowDOM后,蓝色边框停止工作。这是因为事件侦听器不再在新元素上注册:事件侦听器仍然在已从DOM中删除的旧元素上注册。
因此,您必须刷新对该元素的任何引用并重新附加任何事件侦听器。以下是如何重新获取对新元素的引用的示例。
function removeShadowWithCaveat(elementWithShadow) {
  if (!elementWithShadow.parentNode) return elementWithShadow.cloneNode(true);

  var parent = elementWithShadow.parentNode;
  var prior = elementWithShadow.previousSibling;

  elementWithShadow.outerHTML = elementWithShadow.outerHTML;

  return prior.nextSibling || parent.firstChild;
}

如果您需要访问那些由现有阴影根自然隐藏的元素,并且在阴影根被移除后将变得可见,那么这里有一种替代方法,可以完美地保留这些节点。
function removeShadowWithCaveat(elementWithShadow) {
  if (!elementWithShadow.parentNode) return elementWithShadow.cloneNode(true);

  var ref = elementWithShadow.cloneNode(true);
  while (elementWithShadow.lastChild) ref.appendChild( elementWithShadow.lastChild );
  elementWithShadow.parentNode.replaceChild(elementWithShadow, elementWithShadow);

  return ref;
}

工作解决方案

var createShadowProp = (
  "createShadowRoot" in Element.prototype ? "createShadowRoot" : "webkitCreateShadowRoot"
);

function removeChildren(elt) {
  console.log('removing children: %s', elt);
  while (elt.firstChild) {
    elt.removeChild(elt.firstChild);
  }
}
function removeShadowWithCaveat(elementWithShadow) {
  if (!elementWithShadow.parentNode) return elementWithShadow.cloneNode(true);
  
  var ref = elementWithShadow.cloneNode(true);
  while (elementWithShadow.lastChild) ref.appendChild( elementWithShadow.lastChild );
  elementWithShadow.parentNode.replaceChild(elementWithShadow, elementWithShadow);
  
  return ref;
}

function showPlainOldDiv() {
  console.log('adding a plain old div');
  var host = document.querySelector('#content');
  removeChildren(host);
  
  // Remove the shadow
  host = removeShadowWithCaveat(host);
  
  var template = document.querySelector('#plainDiv');
  host.appendChild(template.content.cloneNode(true));
}

function showShadowTemplate() {
  console.log('adding shadowed template component');
  var host = document.querySelector('#content');
  removeChildren(host);

  // Remove the shadow
  host = removeShadowWithCaveat(host);
  
  var template = document.querySelector('#shadowedTemplateComponent');
  var root = host.shadowRoot || host[createShadowProp]({
    "open": true
  });
  root.appendChild(template.content.cloneNode(true));
}
<div>
  <input type="button" value="show plain old div" onclick="showPlainOldDiv()"/>
  <input type="button" value="show shadowed template" onclick="showShadowTemplate()"/>
  <div id="content"></div>
</div>

<template id="shadowedTemplateComponent" style="display:none">
  <style>
    div { background: lightgray; }
    #t { color: red; }
  </style>

  <div id="t">template</div>

  <script>console.log("Activated the shadowed template component.");</script>
</template>

<template id="plainDiv" style="display:none">
  <div id="plaindiv">Plain old div</div>
</template>

此外,还要注意供应商前缀的误用(这是许多开发人员遇到问题的问题)。您是正确的,在提问时只有带前缀的版本createShadowRoot(即webkitCreateShadowRoot)。尽管如此,您必须始终检查未加前缀的createShadowRoot版本是否可用,以防浏览器将来标准化API(现在已经实现了)。让您的代码今天工作可能很好,但是让您的代码在未来几年内工作更加出色。

0

在Chrome中:

  1. 按F12,打开DevTool
  2. 点击DevTool中的齿轮图标
  3. 取消选中“显示用户代理影子DOM”复选框

享受吧!


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