如何在滚动时替换粘性元素?

How to replace a sticky element while scrolling?

上下文

我正在制作一个博客网站,我想要一个粘性元素,随着用户滚动,该元素会在每个新的一年和每个月更新。这样 header 就会显示所列博客文章的当前月份和年份。

编码的时候,我尝试用HTML实现一个效果,不行再用CSS,不行再用JS。我认为这是一个很好的做法,因为它使用内置功能并减少了所需的计算资源,但如果您不同意这个意见,请告诉我。

问题

理想情况下,元素的样式会在 'stuck' 时更改。为此,我查看了使用 IntersectionObserverDavid Walsh's solution,但在添加多个元素时会出现故障。

我面临的主要问题是,当有多个条目时,脚本将元素检测为 'pinned',当它位于 window.

的底部边框时

代码

这是一个片段。我也做了一个jsfiddle with the same code.

//Essentially putting David Walsh's code in a loop

document.querySelectorAll(".myElement").forEach((i) => {
const observer = new IntersectionObserver(([i]) => i.target.classList.toggle("is-pinned", i.intersectionRatio < 1),
{threshold: [1]});
observer.observe(i);
})
#parent { 
  height: 2000px; 
}

.myElement {
  position: sticky;
  top: -1px;
}

/* styles for when the header is in sticky mode. The transition times accentuate the undesired effect */
.myElement.is-pinned {
  color: red;
  transition: color 0.3s, background-color 0.3s;
  background-color: orange;
}
<div id="parent">
  <!-- Adding more than one 'hello' element. The br's are here to add vertical space and be longer than the viewport height -->
  <br><br><br><br>
  <div class="myElement">Hello!</div>
  
  <br><br><br><br>
  <div class="myElement">Hello 2!</div>
  
  <br><br><br><br>
  <div class="myElement">Hello 3!</div>
  
  <br><br><br><br>
  <div class="myElement">Hello 4!</div>
  
  <br><br><br><br>
  <div class="myElement">Hello 5!</div>
  
  <br><br><br><br>
  <div class="myElement">Hello 6!</div>
  
  <br><br><br><br>
  <div class="myElement">Hello 7!</div>
  
  <br><br><br><br>
  <div class="myElement">Hello 8!</div>
</div>

您的 JS 最后一行有误。将其更改为:

document.querySelectorAll(".myElement").forEach((i) => {
  const observer = new IntersectionObserver(
  ([i]) => i.target.classList.toggle("is-pinned", i.intersectionRatio < 1), {
    threshold: [1]
  });
  observer.observe(document.querySelector(".myElement")); // Use the element instead!
})

首先,你只需要一个IntersectionObserver。只要您需要相同的回调和选项(在本例中就是这样做的),您就可以 observe() 具有相同观察者的多个元素。只有您的 observer.observe(i); 需要在循环内。

但是如果您向上或向下跳转页面,您的单个观察者可能会被同时调用多个条目。所以你需要遍历所有观察到的条目。

更重要的是,intersectionRatio 不关心元素在屏幕上的位置。元素在框的顶部和底部均超过 100% 可见性阈值。

您只关心框顶部的元素。 IntersectionObserverEntry 对象还有一个 boundingClientRect 属性 告诉你元素现在在哪里。您可以使用它来仅切换顶部的元素。

所以你最终得到这个:

const observer = new IntersectionObserver((entries) => {
    for (let i of entries) {
        i.target.classList.toggle(
            "is-pinned", i.boundingClientRect.y < 0);
    }
}, {threshold: [1]});

document.querySelectorAll(".myElement").forEach(i => observer.observe(i));

但是,这仍然会给您带来问题。在您的示例中,您滚动的框足够长,如果您从顶部直接跳到底部,则元素从“框下方 0% 可见”到“框顶部可见 99%”。这没有超过 100% 的阈值,因此 IntersectionObserver 回调永远不会为这些元素触发!这意味着他们没有得到 is-pinned class.

您可以简单地向同一个观察者添加另一个 0% 的阈值来捕捉这些变化:

const observer = new IntersectionObserver((entries) => {
    for (let i of entries) {
        i.target.classList.toggle(
            "is-pinned", i.boundingClientRect.y < 0);
    }
}, {threshold: [0, 1]});

document.querySelectorAll(".myElement").forEach(i => observer.observe(i));

现在,从可见到粘性(或反之亦然)的元素和从不可见到粘性(或反之亦然)的元素都会 class 切换。