如何将嵌套的影子 DOM 放在彼此的顶部?

How to position nested shadow DOMs on top of each other?

提问人:OfirD 提问时间:10/12/2022 更新时间:10/13/2022 访问量:287

问:

是否可以将嵌套的影子 DOM 放在彼此的顶部?

使用普通 DOM 时,嵌套的 div 默认只是堆叠:

.d1 {
  width: 150px;
  height: 150px;
  background-color: lightseagreen;
}
.d2 {
  width: 100px;
  height: 100px;  
  background-color: darkslateblue;

}
.d3 {
  width: 50px;
  height: 50px;
  background-color: lightgray;
}
<div class="container">
  <div class="d1">
    <div class="d2">
      <div class="d3">
      </div>
    </div>
  </div>
</div>

但是有了影子 DOM,这怎么能做到这一点呢?

id = 0;
depth = 3;
customElements.define('box-component', class extends HTMLElement {
  constructor() {
    super();
    
    if (id > depth) 
        return;
    this.id = id++;
    
    var template = document.getElementById("box-template");
    const shadowRoot = this.attachShadow({mode: 'open'});
    shadowRoot.innerHTML = template.innerHTML;
    
    // The only goal of all of the following boilerplate is to set 'exportparts' attribute,
    // such that each parent exports its *descendants* (not just *children*) parts.
    // This is done by accessing the shadow dom of the child from its parent.
    // (note: the outermost component doesn't need to export its parts)
    if (id == 0)
        return;
    
    const parts = [...this.shadowRoot.children]     
      .filter(elem => elem.getAttribute('part'))
    
    let thisExportParts = null;
    let childExportParts = null;
    
    thisExportParts = parts.map(elem => [
      [elem.getAttribute('part'), `${elem.getAttribute('part')}${this.id}`].join(':')
    ].join(','));
    
    if (this.shadowRoot && this.shadowRoot.children) {
        const childShadowRoot = 
        [...this.shadowRoot.children].filter(elem => elem.shadowRoot)[0];
      if (childShadowRoot) {
        const fullChildExportParts = childShadowRoot.getAttribute('exportparts');
        if (fullChildExportParts) {
          childExportParts = fullChildExportParts.split(',').map(part => {
            return part.includes(':') ? part.split(':')[1] : part;
          }).join(',');
        }
      }
    }
    this.setAttribute('exportparts', [thisExportParts, childExportParts].filter(_ => _).join(','));
  }
});
box-component::part(box1) {
  width: 150px;
  height: 150px;
  background-color: lightseagreen;
}
box-component::part(box2) {
  width: 100px;
  height: 100px;
  background-color: darkslateblue;
}
box-component::part(box3) {
  width: 50px;
  height: 50px;
  background-color: lightgray;
}
<template id="box-template">
  <style> 
  </style>
  <div part="box"></div> 
  <box-component></box-component>
</template>

<box-component class="container"></box-component>

CSS 嵌套的 Web 组件 shadow-dom

评论

0赞 OfirD 10/12/2022
有关使用 exportparts 的进一步阅读

答:

1赞 OfirD 10/12/2022 #1

有可能。在影子 DOM 中,应添加以下内容:

:host { 
  position: absolute; 
  top: 0;
}

此规则将文档中自定义组件元素(影子主机)的所有实例的样式设置为绝对定位,同时还使定位相对于每个定位的父元素(这就是需要的原因)。:hosttop: 0

实际上,这类似于将问题中的普通 DOM 代码更改为使用定位(注意,如果没有定位,仅设置是行不通的(进一步阅读)):top: 0

.container {
  position: relative;
}
.container div {
  position: absolute;
  top: 0;
}
.d1 {
  width: 150px;
  height: 150px;
  background-color: lightseagreen;
}
.d2 {
  width: 100px;
  height: 100px;  
  background-color: darkslateblue;

}
.d3 {
  width: 50px;
  height: 50px;
  background-color: lightgray;
}
<div class="container">
  <div class="d1">
    <div class="d2">
      <div class="d3">
      </div>
    </div>
  </div>
</div>

所以,这是完整的代码:

id = 0;
depth = 3;
customElements.define('box-component', class extends HTMLElement {
  constructor() {
    super();
    
    if (id > depth) 
        return;
    this.id = id++;
    
    var template = document.getElementById("box-template");
    const shadowRoot = this.attachShadow({mode: 'open'});
    shadowRoot.innerHTML = template.innerHTML;
    
    // The only goal of all of the following boilerplate is to set 'exportparts' attribute,
    // such that each parent exports its *descendants* (not just *children*) parts.
    // This is done by accessing the shadow dom of the child from its parent.
    // (note: the outermost component doesn't need to export its parts)
    if (id == 0)
        return;
    
    const parts = [...this.shadowRoot.children]     
      .filter(elem => elem.getAttribute('part'))
    
    let thisExportParts = null;
    let childExportParts = null;
    
    thisExportParts = parts.map(elem => [
      [elem.getAttribute('part'), `${elem.getAttribute('part')}${this.id}`].join(':')
    ].join(','));
    
    if (this.shadowRoot && this.shadowRoot.children) {
        const childShadowRoot = 
        [...this.shadowRoot.children].filter(elem => elem.shadowRoot)[0];
      if (childShadowRoot) {
        const fullChildExportParts = childShadowRoot.getAttribute('exportparts');
        if (fullChildExportParts) {
          childExportParts = fullChildExportParts.split(',').map(part => {
            return part.includes(':') ? part.split(':')[1] : part;
          }).join(',');
        }
      }
    }
    this.setAttribute('exportparts', [thisExportParts, childExportParts].filter(_ => _).join(','));
  }
});
box-component::part(box1) {
  width: 150px;
  height: 150px;
  background-color: lightseagreen;
}
box-component::part(box2) {
  width: 100px;
  height: 100px;
  background-color: darkslateblue;
}
box-component::part(box3) {
  width: 50px;
  height: 50px;
  background-color: lightgray;
}
<template id="box-template">
  <style> 
    :host { 
      position: absolute; 
      top: 0px;
    }
  </style>
  <div part="box"></div> 
  <box-component></box-component>
</template>

<box-component class="container"></box-component>