10

小提琴最新


我用这种方法开始了这个问题scroll event,但由于建议使用IntersectionObserver似乎更好的方法,我试图让它以这种方式工作。


目标是什么:

我想根据当前/通过查找(我在想?)观察到的内容来更改样式( color+ background-color),或者将覆盖默认样式(白底黑字)。headerdivsectionclassdataheader


标题样式:

font-color

根据内容(div/ section),默认值header应该能够将其更改font-color为仅两种可能的颜色:

  • 黑色的
  • 白色的

background-color

根据内容,background-color可能有无限的颜色或透明,所以最好单独解决,这些可能是最常用的背景颜色:

  • 白色(默认)
  • 黑色的
  • 无颜色(透明)

CSS:

header {
  position: fixed;
  width: 100%;
  top: 0;
  line-height: 32px;
  padding: 0 15px;
  z-index: 5;
  color: black; /* default */
  background-color: white; /* default */
}

具有默认标题的 Div/section 示例内容没有变化:

<div class="grid-30-span g-100vh">
    <img 
    src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1.414 1'%3E%3C/svg%3E"
    data-src="/images/example_default_header.jpg" 
    class="lazyload"
    alt="">
</div>

div/section 示例更改内容的标题:

<div class="grid-30-span g-100vh" data-color="white" data-background="darkblue">
    <img 
    src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1.414 1'%3E%3C/svg%3E"
    data-src="/images/example_darkblue.jpg" 
    class="lazyload"
    alt="">
</div>

<div class="grid-30-span g-100vh" data-color="white" data-background="black">
    <img 
    src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1.414 1'%3E%3C/svg%3E"
    data-src="/images/example_black.jpg" 
    class="lazyload"
    alt="">
</div>

路口观察者方法:

var mq = window.matchMedia( "(min-width: 568px)" );
if (mq.matches) {
  // Add for mobile reset

document.addEventListener("DOMContentLoaded", function(event) { 
  // Add document load callback for leaving script in head
  const header = document.querySelector('header');
  const sections = document.querySelectorAll('div');
  const config = {
    rootMargin: '0px',
    threshold: [0.00, 0.95]
  };

  const observer = new IntersectionObserver(function (entries, self) {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        if (entry.intersectionRatio > 0.95) {
          header.style.color = entry.target.dataset.color !== undefined ? entry.target.dataset.color : "black";
          header.style.background = entry.target.dataset.background !== undefined ? entry.target.dataset.background : "white";   
        } else {
        if (entry.target.getBoundingClientRect().top < 0 ) {
          header.style.color = entry.target.dataset.color !== undefined ? entry.target.dataset.color : "black";
          header.style.background = entry.target.dataset.background !== undefined ? entry.target.dataset.background : "white";
          }
        } 
      }
    });
  }, config);

  sections.forEach(section => {
    observer.observe(section);
  });

});

}
4

5 回答 5

9

您应该看看Intersection Observer (IO),而不是听滚动事件。这是为了解决像你这样的问题。而且它比监听滚动事件然后自己计算位置要高效得多。

首先,这是一个代码笔,它显示了您的问题的解决方案。我不是这个 codepen 的作者,我可能会做一些不同的事情,但它肯定会向您展示如何解决问题的基本方法。

我会改变的事情:您可以在示例中看到,如果您将 99% 的速度滑到一个新部分,标题会发生变化,即使新部分也不会完全可见。

现在有了这个,一些解释它是如何工作的(注意,我不会盲目地从 codepen 复制粘贴,我也会将 const 更改为 let,但使用更适合您的项目的任何内容。

首先,您必须指定 IO 的选项:

let options = {
  rootMargin: '-50px 0px -55%'
}

let observer = new IntersectionObserver(callback, options);

在示例中,一旦元素距离进入视图 50 像素,IO 就会执行回调。我不能从头顶推荐一些更好的值,但如果我有时间,我会尝试调整这些参数,看看是否能得到更好的结果。

在他们定义内联回调函数的 codepen 中,我只是这样写,以便更清楚地知道发生了什么。

IO 的下一步是定义一些要观察的元素。在您的情况下,您应该为您的 div 添加一些类,例如<div class="section">

let entries = document.querySelectorAll('div.section');
entries.forEach(entry => {observer.observe(entry);})

最后你必须定义回调函数:

entries.forEach(entry => {
    if (entry.isIntersecting) {
     //specify what should happen if an element is coming into view, like defined in the options. 
    }
  });

编辑:正如我所说,这只是一个关于如何让你开始的例子,它不是你复制粘贴的完整解决方案。在基于可见部分的 ID 的示例中,当前元素被突出显示。您必须更改此部分,以便根据您在元素上设置的某些属性设置颜色和背景颜色,而不是将活动类设置为例如第三个元素。我建议为此使用数据属性

编辑 2:当然你可以继续使用滚动事件, W3C的官方 Polyfill使用滚动事件来模拟旧浏览器的 IO。只是监听滚动事件和计算位置并不高效,尤其是在有多个元素的情况下。所以如果你关心用户体验,我真的推荐使用 IO。只是想添加此答案以显示此类问题的现代解决方案是什么。

编辑 3:我花时间创建了一个基于 IO 的示例,这应该可以帮助您入门。

基本上我定义了两个阈值:一个是 20%,一个是 90%。如果元素在视口中占 90%,则可以假设它将覆盖标题。所以我将标题的类设置为视图中 90% 的元素。

第二个阈值是 20%,这里我们必须检查元素是从顶部还是从底部进入视图。如果它从顶部可见 20%,那么它将与标题重叠。

如您所见,调整这些值并调整逻辑。

const sections = document.querySelectorAll('div');
const config = {
  rootMargin: '0px',
  threshold: [.2, .9]
};

const observer = new IntersectionObserver(function (entries, self) {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      var headerEl = document.querySelector('header');
      if (entry.intersectionRatio > 0.9) {
        //intersection ratio bigger than 90%
        //-> set header according to target
        headerEl.className=entry.target.dataset.header;      
      } else {
        //-> check if element is coming from top or from bottom into view
        if (entry.target.getBoundingClientRect().top < 0 ) {
          headerEl.className=entry.target.dataset.header;
        }
      } 
    }
  });
}, config);

sections.forEach(section => {
  observer.observe(section);
});
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}

.g-100vh {
height: 100vh
}

header {
  min-height: 50px;
  position: fixed;
  background-color: green;
  width: 100%;
}
  
header.white-menu {
  color: white;
  background-color: black;
}

header.black-menu {
  color: black;
  background-color: white;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>


<header>
 <p>Header Content </p>
</header>
<div class="grid-30-span g-100vh white-menu" style="background-color:darkblue;" data-header="white-menu">
    <img 
    src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1.414 1'%3E%3C/svg%3E"
    data-src="/images/example_darkblue.jpg" 
    class="lazyload"
    alt="<?php echo $title; ?>">
</div>

<div class="grid-30-span g-100vh black-menu" style="background-color:lightgrey;" data-header="black-menu">
    <img 
    src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1.414 1'%3E%3C/svg%3E"
    data-src="/images/example_lightgrey.jpg" 
    class="lazyload"
    alt="<?php echo $title; ?>">
</div>

于 2019-09-09T06:53:54.067 回答
4

我可能不完全理解这个问题,但至于你的例子 - 你可以通过使用mix-blend-mode css 属性来解决它,而无需使用 javascript。

例子:

header {background: white; position: relative; height: 20vh;}
header h1 {
  position: fixed;
  color: white;
  mix-blend-mode: difference;
}
div {height: 100vh; }
<header>
  <h1>StudioX, Project Title, Category...</h1>
</header>
<div style="background-color:darkblue;"></div>
<div style="background-color:lightgrey;"></div>

于 2019-09-20T12:19:29.957 回答
2

我遇到了同样的情况,我实现的解决方案非常精确,因为它不依赖于百分比,而是依赖于真实元素的边界框:

class Header {
  constructor() {
    this.header = document.querySelector("header");
    this.span = this.header.querySelector('span');
    this.invertedSections = document.querySelectorAll(".invertedSection");

    window.addEventListener('resize', () => this.resetObserver());

    this.resetObserver();
  }

  resetObserver() {
    if (this.observer) this.observer.disconnect();

    const {
      top,
      height
    } = this.span.getBoundingClientRect();

    this.observer = new IntersectionObserver(entries => this.observerCallback(entries), {
        root: document,
      rootMargin: `-${top}px 0px -${window.innerHeight - top - height}px 0px`,
    });

    this.invertedSections.forEach((el) => this.observer.observe(el));
  };

  observerCallback(entries) {
    let inverted = false;
    entries.forEach((entry) => {
      if (entry.isIntersecting) inverted = true;
    });
    if (inverted) this.header.classList.add('inverted');
    else this.header.classList.remove('inverted');
  };
}

new Header();
header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  padding: 20px 0;
  text-transform: uppercase;
  text-align: center;
  font-weight: 700;
}
header.inverted {
  color: #fff;
}

section {
  height: 500px;
}
section.invertedSection {
  background-color: #000;
}
<body>
  <header>
    <span>header</span>
  </header>
  <main>
    <section></section>
    <section class="invertedSection"></section>
    <section></section>
    <section class="invertedSection"></section>
  </main>
</body>

它的作用实际上很简单:我们不能使用 IntersectionObserver 知道header和其他元素何时交叉(因为root必须是被观察元素的父元素),但我们可以计算header要添加rootMargin到的位置和大小观察者。

有时,标题比它的内容高(因为填充和其他东西)所以我计算标题中的边界框(我希望它只有在这个元素与黑色部分重叠span时才变成白色)。

因为窗口的高度可以改变,我必须在窗口调整大小时重置 IntersectionObserver。

由于片段的 iframe 限制,该root属性设置为document此处(否则您可以不定义此字段)。

使用rootMargin,我指定我希望观察者在哪个区域寻找交叉点。

然后我观察每个黑色部分。在回调函数中,我定义是否至少有一个部分重叠,如果是这样,我inverted在标题中添加一个类名。

如果我们可以使用属性calc(100vh - 50px)中的值rootMargin,我们可能不需要使用resize监听器。

我们甚至可以通过添加侧 rootMargin 来改进这个系统,例如,如果我的黑色部分只有窗口宽度的一半,并且可能与标题中的跨度相交,也可能不相交,具体取决于其水平位置。

于 2020-10-27T12:12:27.090 回答
1

@昆汀 D

我在互联网上搜索了类似的东西,我发现这段代码是满足我需求的最佳解决方案。

因此我决定在它的基础上创建一个通用的“Observer”类,它可以在许多需要 IntesectionObserver 的情况下使用,包括更改标题样式。我没有对它进行太多测试,仅在一些基本情况下,它对我有用。我没有在具有水平滚动的页面上测试它。

以这种方式使用它很容易,只需将其保存为 .js 文件并将其包含/导入到您的代码中,就像插件一样。:) 我希望有人会发现它有用。

如果有人找到更好的想法(尤其是对于“水平”网站),很高兴在这里看到它们。

编辑:我没有做出正确的“未观察”,所以我修复了它。

/* The Intersection Observer API provides a way to asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-level document's viewport.

ROOT:
It is not necessary for the root to be the ancestor element of the target. The root is allways the document, and the so-called root element is used only to get its size and position, to create an area in the document, with options.rootMargin.
Leave it false to have the viewport as root.

TARGET:
IntersectionObserver triggers when the target is entering at the specified ratio(s), and when it exits at the same ratio(s).

For more on IntersectionObserverEntry object, see:
https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#targeting_an_element_to_be_observed

IntersectionObserverEntry.time               // Timestamp when the change occurred
IntersectionObserverEntry.rootBounds         // Unclipped area of root
IntersectionObserverEntry.intersectionRatio  // Ratio of intersectionRect area to boundingClientRect area
IntersectionObserverEntry.target             // the Element target
IntersectionObserverEntry.boundingClientRect // target.boundingClientRect()
IntersectionObserverEntry.intersectionRect   // boundingClientRect, clipped by its containing block ancestors, and intersected with rootBounds

THRESHOLD:
Intersection ratio/threshold can be an array, and then it will trigger on each value, when in and when out.
If root element's size, for example, is only 10% of the target element's size, then intersection ratio/threshold can't be set to more than 10% (that is 0.1).

CALLBACKS:
There can be created two functions; when the target is entering and when it's exiting. These functions can do what's required for each event (visible/invisible).
Each function is passed three arguments, the root (html) element, IntersectionObserverEntry object, and intersectionObserver options used for that observer.

Set only root and targets to only have some info in the browser's console.

For more info on IntersectionObserver see: https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API

Polyfill: <script src="https://polyfill.io/v3/polyfill.js?features=IntersectionObserver"></script>
or:
https://github.com/w3c/IntersectionObserver/tree/main/polyfill


Based on answer by Quentin D, answered Oct 27 '20 at 12:12
https://stackoverflow.com/questions/57834100/change-style-header-nav-with-intersection-observer-io

root     - (any selector) - root element, intersection parent (only the first element is selected).
targets  - (any selector) - observed elements that trigger function when visible/invisible.
inCb     - (function name) - custom callback function to trigger when the target is intersecting.
outCb    - (function name) - custom callback function to trigger when the target is not intersecting.
thres    - (number 0-1) - threshold to trigger the observer (e.g. 0.1 will trigger when 10% is visible).
unobserve- (bolean) - if true, the target is unobserved after triggering the callback.

EXAMPLE:
(place in 'load' event listener, to have the correct dimensions)

var invertedHeader = new Observer({
   root: '.header--main', // don't set to have the viewport as root
   targets: '[data-bgd-dark]',
   thres: [0, .16],
   inCb: someCustomFunction,
});
*/

class Observer {
   constructor({
      root = false,
      targets = false,
      inCb = this.isIn,
      outCb = this.isOut,
      thres = 0,
      unobserve = false,
   } = {}) {
      // this element's position creates with rootMargin the area in the document
      // which is used as intersection observer's root area.
      // the real root is allways the document.
      this.area = document.querySelector(root); // intersection area
      this.targets = document.querySelectorAll(targets); // intersection targets
      this.inCallback = inCb; // callback when intersecting
      this.outCallback = outCb; // callback when not intersecting
      this.unobserve = unobserve; // unobserve after intersection
      this.margins; // rootMargin for observer
      this.windowW = document.documentElement.clientWidth;
      this.windowH = document.documentElement.clientHeight;

      // intersection is being checked like:
      // if (entry.isIntersecting || entry.intersectionRatio >= this.ratio),
      // and if ratio is 0, "entry.intersectionRatio >= this.ratio" will be true,
      // even for non-intersecting elements, therefore:
      this.ratio = thres;
      if (Array.isArray(thres)) {
         for (var i = 0; i < thres.length; i++) {
            if (thres[i] == 0) {
               this.ratio[i] = 0.0001;
            }
         }
      } else {
         if (thres == 0) {
            this.ratio = 0.0001;
         }
      }

      // if root selected use its position to create margins, else no margins (viewport as root)
      if (this.area) {
         this.iArea = this.area.getBoundingClientRect(); // intersection area
         this.margins = `-${this.iArea.top}px -${(this.windowW - this.iArea.right)}px -${(this.windowH - this.iArea.bottom)}px -${this.iArea.left}px`;
      } else {
         this.margins = '0px';
      }

      // Keep this last (this.ratio has to be defined before).
      // targets are required to create an observer.
      if (this.targets) {
         window.addEventListener('resize', () => this.resetObserver());
         this.resetObserver();
      }
   }

   resetObserver() {
      if (this.observer) this.observer.disconnect();

      const options = {
         root: null, // null for the viewport
         rootMargin: this.margins,
         threshold: this.ratio,
      }

      this.observer = new IntersectionObserver(
         entries => this.observerCallback(entries, options),
         options,
      );

      this.targets.forEach((target) => this.observer.observe(target));
   };

   observerCallback(entries, options) {
      entries.forEach(entry => {
         // "entry.intersectionRatio >= this.ratio" for older browsers
         if (entry.isIntersecting || entry.intersectionRatio >= this.ratio) {
            // callback when visible
            this.inCallback(this.area, entry, options);

            // unobserve
            if (this.unobserve) {
               this.observer.unobserve(entry.target);
            }
         } else {
            // callback when hidden
            this.outCallback(this.area, entry, options);
            // No unobserve, because all invisible targets will be unobserved automatically
         }
      });
   };

   isIn(rootElmnt, targetElmt, options) {
      if (!rootElmnt) {
         console.log(`IO Root: VIEWPORT`);
      } else {
         console.log(`IO Root: ${rootElmnt.tagName} class="${rootElmnt.classList}"`);
      }
      console.log(`IO Target: ${targetElmt.target.tagName} class="${targetElmt.target.classList}" IS IN (${targetElmt.intersectionRatio * 100}%)`);
      console.log(`IO Threshold: ${options.threshold}`);
      //console.log(targetElmt.rootBounds);
      console.log(`============================================`);
   }
   isOut(rootElmnt, targetElmt, options) {
      if (!rootElmnt) {
         console.log(`IO Root: VIEWPORT`);
      } else {
         console.log(`IO Root: ${rootElmnt.tagName} class="${rootElmnt.classList}"`);
      }
      console.log(`IO Target: ${targetElmt.target.tagName} class="${targetElmt.target.classList}" IS OUT `);
      console.log(`============================================`);
   }
}
于 2021-08-17T17:52:05.133 回答
0

这仍然需要调整,但您可以尝试以下方法:

const header = document.getElementsByTagName('header')[0];

const observer = new IntersectionObserver((entries) => {
	entries.forEach((entry) => {
    if (entry.isIntersecting) {
    	  header.style.color = entry.target.dataset.color || '';
        header.style.backgroundColor = entry.target.dataset.background;
    }
  });
}, { threshold: 0.51 });

[...document.getElementsByClassName('observed')].forEach((t) => {
    t.dataset.background = t.dataset.background || window.getComputedStyle(t).backgroundColor;
    observer.observe(t);    
});
body {
  font-family: arial;
  margin: 0;
}

header {
  border-bottom: 1px solid red;
  margin: 0 auto;
  width: 100vw;
  display: flex;
  justify-content: center;
  position: fixed;
  background: transparent;  
  transition: all 0.5s ease-out;
}

header div {
  padding: 0.5rem 1rem;
  border: 1px solid red;
  margin: -1px -1px -1px 0;
}

.observed {
  height: 100vh;
  border: 1px solid black;
}

.observed:nth-of-type(2) {
  background-color: grey;
}

.observed:nth-of-type(3) {
  background-color: white;
}
<header>
  <div>One</div>
  <div>Two</div>
  <div>Three</div>
</header>

<div class="observed">
  <img src="http://placekitten.com/g/200/300">
  <img src="http://placekitten.com/g/400/300">
</div>
  
<div class="observed" data-color="white" data-background="black">
  <img src="http://placekitten.com/g/600/300">
</div>

<div class="observed" data-color="black" data-background="white">
  <img src="http://placekitten.com/g/600/250">
</div>

CSS 确保每个观察到的部分占用 100vw,并且当其中任何一个以至少 51% 的百分比进入视野时,观察者会做它的事情。

在回调中,标题背景颜色然后设置为相交元素的背景颜色。

于 2019-09-26T18:07:52.583 回答