網站搜尋

javascript - 偵測 position:sticky 觸發的事件


我正在使用新的 position: sticky(資訊)來創建類似 iOS 的內容列表。

它運作良好,比之前的 JavaScript 替代方案(例如)優越得多,不過據我所知,當它被觸發時不會觸發任何事件,這意味著當滾動條到達頁面頂部時,我無法像使用之前的解決方案那樣做任何事情。

我想在具有 position: sticky 的元素碰到頁面頂部時,新增一個類別(例如 stuck)。有沒有辦法用 JavaScript 監聽這個事件?使用 jQuery 也可以。

答案#1

使用 IntersectionObserver 的示範(使用一個技巧):

// get the sticky element
const stickyElm = document.querySelector('header')

const observer = new IntersectionObserver( 
  ([e]) => e.target.classList.toggle('isSticky', e.intersectionRatio < 1),
  {threshold: [1]}
);

observer.observe(stickyElm)
body{ height: 200vh; font:20px Arial; }

section{
  background: lightblue;
  padding: 2em 1em;
}

header{
  position: sticky;
  top: -1px;                       /* ➜ the trick */

  padding: 1em;
  padding-top: calc(1em + 1px);    /* ➜ compensate for the trick */

  background: salmon;
  transition: .1s;
}

/* styles for when the header is in sticky mode */
header.isSticky{
  font-size: .8em;
  opacity: .5;
}
<section>Space</section>
<header>Sticky Header</header>

top 值需要是 -1px,否則該元素將永遠不會與瀏覽器窗口的頂部相交(因此永遠不會觸發 intersection observer)。

為了對抗這個 1px 的隱藏內容,應該在黏性元素的邊框或內邊距中額外添加 1px 的空間。

💡 或者,如果你希望保持 CSS 不變(top:0),那麼你可以在 intersection observer 層級應用「修正」,方法是添加設置 rootMargin: '-1px 0px 0px 0px'(如 @mattrick 在他的回答中所示)

使用老式 scroll 事件監聽器的示範:

  1. 自動偵測第一個可滾動的父元素
  2. 限制滾動事件的頻率
  3. 功能組合以分離關注點
  4. 事件回調快取:scrollCallback(以便在需要時能夠解除綁定)

// get the sticky element
const stickyElm = document.querySelector('header');

// get the first parent element which is scrollable
const stickyElmScrollableParent = getScrollParent(stickyElm);

// save the original offsetTop. when this changes, it means stickiness has begun.
stickyElm._originalOffsetTop = stickyElm.offsetTop;


// compare previous scrollTop to current one
const detectStickiness = (elm, cb) => () => cb & cb(elm.offsetTop != elm._originalOffsetTop)

// Act if sticky or not
const onSticky = isSticky => {
   console.clear()
   console.log(isSticky)
   
   stickyElm.classList.toggle('isSticky', isSticky)
}

// bind a scroll event listener on the scrollable parent (whatever it is)
// in this exmaple I am throttling the "scroll" event for performance reasons.
// I also use functional composition to diffrentiate between the detection function and
// the function which acts uppon the detected information (stickiness)

const scrollCallback = throttle(detectStickiness(stickyElm, onSticky), 100)
stickyElmScrollableParent.addEventListener('scroll', scrollCallback)



// OPTIONAL CODE BELOW ///////////////////

// find-first-scrollable-parent
// Credit: https://stackoverflow.com/a/42543908/104380
function getScrollParent(element, includeHidden) {
    var style = getComputedStyle(element),
        excludeStaticParent = style.position === "absolute",
        overflowRegex = includeHidden ? /(auto|scroll|hidden)/ : /(auto|scroll)/;

    if (style.position !== "fixed") 
      for (var parent = element; (parent = parent.parentElement); ){
          style = getComputedStyle(parent);
          if (excludeStaticParent && style.position === "static") 
              continue;
          if (overflowRegex.test(style.overflow + style.overflowY + style.overflowX)) 
            return parent;
      }

    return window
}

// Throttle
// Credit: https://jsfiddle.net/jonathansampson/m7G64
function throttle (callback, limit) {
    var wait = false;                  // Initially, we're not waiting
    return function () {               // We return a throttled function
        if (!wait) {                   // If we're not waiting
            callback.call();           // Execute users function
            wait = true;               // Prevent future invocations
            setTimeout(function () {   // After a period of time
                wait = false;          // And allow future invocations
            }, limit);
        }
    }
}
header{
  position: sticky;
  top: 0;

  /* not important styles */
  background: salmon;
  padding: 1em;
  transition: .1s;
}

header.isSticky{
  /* styles for when the header is in sticky mode */
  font-size: .8em;
  opacity: .5;
}

/* not important styles*/

body{ height: 200vh; font:20px Arial; }

section{
  background: lightblue;
  padding: 2em 1em;
}
<section>Space</section>
<header>Sticky Header</header>

這是一個使用第一種技術的 React 元件示範

答案#2

我找到了一個與 @vsync 答案有些相似的解決方案,但它不需要你必須添加到樣式表中的“hack”。你可以簡單地更改 IntersectionObserver 的邊界,以避免需要將元素本身移出視口:

const observer = new IntersectionObserver(callback, {
  rootMargin: '-1px 0px 0px 0px',
  threshold: [1],
});

observer.observe(element);

答案#3

你可以像這樣非常簡單地做到這一點:

  1. 附加滾動事件監聽器
  2. 透過 getComputedStyle() 獲取元素所需的粘性 top 位置
  3. 使用 getBoundingClientRect() 取得元素當前的視口偏移
  4. 如果兩個值匹配,使用 classList.toggle() 添加該類別

這是在程式碼中的樣子:

const el = document.querySelector(".my-sticky-element");
window.addEventListener("scroll", () => {
    const stickyTop = parseInt(window.getComputedStyle(el).top);
    const currentTop = el.getBoundingClientRect().top;
    el.classList.toggle("is-sticky", currentTop === stickyTop);
});