Một sự kiện cho CSS position:sticky

TL;DR

Đây là bí mật: Bạn có thể không cần các sự kiện scroll trong ứng dụng tiếp theo của mình. Sử dụng IntersectionObserver, Tôi sẽ hướng dẫn bạn cách kích hoạt một sự kiện tuỳ chỉnh khi các phần tử position:sticky được cố định hoặc khi các phần tử đó ngừng hoạt động. Tất cả đều không có sử dụng trình nghe cuộn. Thậm chí còn có bản minh hoạ tuyệt vời để chứng minh điều đó:

Xem bản minh hoạ | Nguồn

Giới thiệu sự kiện sticky-change

Một trong những hạn chế thực tế của việc sử dụng vị trí cố định CSS là không cung cấp tín hiệu nền tảng để biết thời điểm tài sản hoạt động. Nói cách khác, không có sự kiện nào để biết khi nào một phần tử trở thành cố định hoặc khi nào nó sẽ không còn dính.

Hãy xem ví dụ sau để sửa lỗi <div class="sticky"> 10px từ đầu vùng chứa gốc:

.sticky {
  position: sticky;
  top: 10px;
}

Sẽ không tốt nếu trình duyệt thông báo khi các phần tử đạt đến điểm đó phải không? Dường như tôi không phải là người duy nhất nghĩ là vậy. Một tín hiệu cho position:sticky có thể xác định một số trường hợp sử dụng:

  1. Áp dụng hiệu ứng bóng đổ cho biểu ngữ khi biểu ngữ cố định.
  2. Khi người dùng đọc qua nội dung của bạn, hãy ghi lại số lần truy cập phân tích để biết tiến trình.
  3. Khi người dùng cuộn trang, hãy cập nhật tiện ích Lựa chọn tốt nhất nổi lên thành tiện ích hiện tại .

Với những trường hợp sử dụng này, chúng tôi đã tạo ra một mục tiêu cuối cùng: tạo một sự kiện kích hoạt khi phần tử position:sticky được khắc phục. Hãy đặt tên tệp là Sự kiện sticky-change:

document.addEventListener('sticky-change', e => {
  const header = e.detail.target;  // header became sticky or stopped sticking.
  const sticking = e.detail.stuck; // true when header is sticky.
  header.classList.toggle('shadow', sticking); // add drop shadow when sticking.

  document.querySelector('.who-is-sticking').textContent = header.textContent;
});

Bản minh hoạ sử dụng sự kiện này để tiêu đề một bóng đổ khi chúng được khắc phục. Việc này cũng cập nhật tiêu đề mới ở đầu trang.

Trong bản minh hoạ, hiệu ứng được áp dụng mà không cần sự kiện cuộn.

Hiệu ứng cuộn mà không có sự kiện cuộn?

Cấu trúc của trang.
Cấu trúc của trang.

Hãy tích luỹ thêm một vài thuật ngữ để tôi có thể gọi cho những tên này trong phần còn lại của bài đăng:

  1. Vùng chứa cuộn – vùng nội dung (khung nhìn hiển thị) chứa danh sách "bài đăng trên blog".
  2. Tiêu đề - tiêu đề màu xanh dương trong mỗi phần có position:sticky.
  3. Phần cố định – từng phần nội dung. Văn bản cuộn dưới tiêu đề cố định.
  4. "Chế độ cố định" – khi position:sticky đang áp dụng cho phần tử.

Để biết tiêu đề nào chuyển sang "chế độ cố định", chúng ta cần một cách nào đó để xác định độ lệch cuộn của vùng chứa cuộn. Điều đó sẽ cho chúng tôi một cách để tính tiêu đề đang hiển thị. Tuy nhiên, điều đó khá tốt khó thực hiện nếu không có sự kiện scroll :) Vấn đề khác là position:sticky sẽ xoá phần tử khỏi bố cục khi nó được sửa.

Nếu không có các sự kiện cuộn, chúng ta sẽ mất khả năng thực hiện các thao tác liên quan đến bố cục tính toán trên tiêu đề.

Thêm DOM dumby để xác định vị trí cuộn

Thay vì các sự kiện scroll, chúng ta sẽ sử dụng IntersectionObserver để xác định thời điểm tiêu đề chuyển sang và thoát khỏi chế độ cố định. Thêm hai nút (còn gọi là cảnh sát) trong mỗi phần cố định, một phần ở trên cùng và một phần ở dưới cùng, sẽ đóng vai trò là điểm tham chiếu để xác định vị trí cuộn. Như điểm đánh dấu vào và rời khỏi vùng chứa, mức độ hiển thị của chúng sẽ thay đổi và Intersection Observer kích hoạt một lệnh gọi lại.

Khi không có các phần tử giám sát hiển thị
Những yếu tố giám sát ẩn giấu.

Chúng ta cần hai người canh gác để xử lý bốn trường hợp cuộn lên và xuống:

  1. Cuộn xuốngtiêu đề trở nên cố định khi giám sát trên cùng của nó vượt qua phần trên cùng của vùng chứa.
  2. Cuộn xuốngtiêu đề rời khỏi chế độ cố định khi đến cuối phần và cột canh dưới cùng đi qua phần trên của vùng chứa.
  3. Cuộn lêntiêu đề rời khỏi chế độ cố định khi giám sát trên cùng cuộn quay lại chế độ xem từ trên cao.
  4. Cuộn lêntiêu đề trở nên cố định khi giám sát dưới cùng di chuyển ngược trở lại vào chế độ xem từ trên cao.

Bạn nên xem bản ghi màn hình từ 1 đến 4 theo thứ tự xuất hiện:

Trình quan sát Intersection kích hoạt lệnh gọi lại khi các người canh gác vào/rời khỏi vùng chứa cuộn.

Dịch vụ so sánh giá (CSS)

Các vệ sĩ được đặt ở trên cùng và dưới cùng của mỗi phần. .sticky_sentinel--top nằm ở trên cùng tiêu đề trong khi .sticky_sentinel--bottom nằm ở cuối mục:

Người giám sát dưới cùng đạt đến ngưỡng.
Vị trí của các phần tử giám sát trên cùng và dưới cùng.
:root {
  --default-padding: 16px;
  --header-height: 80px;
}
.sticky {
  position: sticky;
  top: 10px; /* adjust sentinel height/positioning based on this position. */
  height: var(--header-height);
  padding: 0 var(--default-padding);
}
.sticky_sentinel {
  position: absolute;
  left: 0;
  right: 0; /* needs dimensions */
  visibility: hidden;
}
.sticky_sentinel--top {
  /* Adjust the height and top values based on your on your sticky top position.
  e.g. make the height bigger and adjust the top so observeHeaders()'s
  IntersectionObserver fires as soon as the bottom of the sentinel crosses the
  top of the intersection container. */
  height: 40px;
  top: -24px;
}
.sticky_sentinel--bottom {
  /* Height should match the top of the header when it's at the bottom of the
  intersection container. */
  height: calc(var(--header-height) + var(--default-padding));
  bottom: 0;
}

Thiết lập Quan sát giao lộ

Trình quan sát giao lộ quan sát không đồng bộ các thay đổi trong giao điểm của phần tử mục tiêu và khung nhìn tài liệu hoặc vùng chứa mẹ. Trong trường hợp của chúng ta, chúng ta đang quan sát các điểm giao cắt với vùng chứa mẹ.

Loại sốt thần kỳ là IntersectionObserver. Mỗi giám đốc sẽ nhận được một IntersectionObserver để quan sát chế độ hiển thị của giao lộ trong vùng chứa cuộn. Khi một người canh gác cuộn vào khung nhìn đang được nhìn thấy, chúng tôi biết rằng tiêu đề trở nên cố định hoặc không còn cố định. Tương tự như vậy, khi một giám sát thoát khung nhìn.

Trước tiên, tôi thiết lập trình quan sát cho các quan sát ở đầu trang và chân trang:

/**
 * Notifies when elements w/ the `sticky` class begin to stick or stop sticking.
 * Note: the elements should be children of `container`.
 * @param {!Element} container
 */
function observeStickyHeaderChanges(container) {
  observeHeaders(container);
  observeFooters(container);
}

observeStickyHeaderChanges(document.querySelector('#scroll-container'));

Sau đó, tôi thêm một trình quan sát để kích hoạt khi các phần tử .sticky_sentinel--top vượt qua qua đầu vùng chứa cuộn (theo một trong hai hướng). Hàm observeHeaders tạo và thêm những người gửi hàng đầu vào từng phần. Đối tượng tiếp nhận dữ liệu tính giao điểm của quan điểm với đầu vùng chứa và quyết định xem nó đi vào hay ra khỏi khung nhìn. Đó xác định xem tiêu đề mục có được gắn ở chỗ không.

/**
 * Sets up an intersection observer to notify when elements with the class
 * `.sticky_sentinel--top` become visible/invisible at the top of the container.
 * @param {!Element} container
 */
function observeHeaders(container) {
  const observer = new IntersectionObserver((records, observer) => {
    for (const record of records) {
      const targetInfo = record.boundingClientRect;
      const stickyTarget = record.target.parentElement.querySelector('.sticky');
      const rootBoundsInfo = record.rootBounds;

      // Started sticking.
      if (targetInfo.bottom < rootBoundsInfo.top) {
        fireEvent(true, stickyTarget);
      }

      // Stopped sticking.
      if (targetInfo.bottom >= rootBoundsInfo.top &&
          targetInfo.bottom < rootBoundsInfo.bottom) {
       fireEvent(false, stickyTarget);
      }
    }
  }, {threshold: [0], root: container});

  // Add the top sentinels to each section and attach an observer.
  const sentinels = addSentinels(container, 'sticky_sentinel--top');
  sentinels.forEach(el => observer.observe(el));
}

Đối tượng tiếp nhận dữ liệu được định cấu hình bằng threshold: [0] để lệnh gọi lại của nó kích hoạt ngay khi khi người canh gác nhìn thấy.

Quá trình này tương tự như trọng điểm dưới cùng (.sticky_sentinel--bottom). Trình quan sát thứ hai được tạo để kích hoạt khi chân trang đi qua phần dưới cùng của vùng chứa cuộn. Hàm observeFooters tạo các nút trọng điểm và đính kèm chúng vào từng phần. Đối tượng tiếp nhận dữ liệu tính toán điểm giao nhau của điểm giao cắt với đáy của vùng chứa và quyết định xem nó có vào hoặc rời đi. Thông tin đó xác định liệu tiêu đề mục là vẫn được giữ nguyên.

/**
 * Sets up an intersection observer to notify when elements with the class
 * `.sticky_sentinel--bottom` become visible/invisible at the bottom of the
 * container.
 * @param {!Element} container
 */
function observeFooters(container) {
  const observer = new IntersectionObserver((records, observer) => {
    for (const record of records) {
      const targetInfo = record.boundingClientRect;
      const stickyTarget = record.target.parentElement.querySelector('.sticky');
      const rootBoundsInfo = record.rootBounds;
      const ratio = record.intersectionRatio;

      // Started sticking.
      if (targetInfo.bottom > rootBoundsInfo.top && ratio === 1) {
        fireEvent(true, stickyTarget);
      }

      // Stopped sticking.
      if (targetInfo.top < rootBoundsInfo.top &&
          targetInfo.bottom < rootBoundsInfo.bottom) {
        fireEvent(false, stickyTarget);
      }
    }
  }, {threshold: [1], root: container});

  // Add the bottom sentinels to each section and attach an observer.
  const sentinels = addSentinels(container, 'sticky_sentinel--bottom');
  sentinels.forEach(el => observer.observe(el));
}

Đối tượng tiếp nhận dữ liệu được định cấu hình bằng threshold: [1] để lệnh gọi lại của nó kích hoạt khi toàn bộ nút nằm trong khung hiển thị.

Cuối cùng, có hai tiện ích để kích hoạt sự kiện tuỳ chỉnh sticky-change và tạo dựng lực lượng canh gác:

/**
 * @param {!Element} container
 * @param {string} className
 */
function addSentinels(container, className) {
  return Array.from(container.querySelectorAll('.sticky')).map(el => {
    const sentinel = document.createElement('div');
    sentinel.classList.add('sticky_sentinel', className);
    return el.parentElement.appendChild(sentinel);
  });
}

/**
 * Dispatches the `sticky-event` custom event on the target element.
 * @param {boolean} stuck True if `target` is sticky.
 * @param {!Element} target Element to fire the event on.
 */
function fireEvent(stuck, target) {
  const e = new CustomEvent('sticky-change', {detail: {stuck, target}});
  document.dispatchEvent(e);
}

Vậy là xong!

Bản minh hoạ cuối cùng

Chúng tôi đã tạo một sự kiện tuỳ chỉnh khi các phần tử có position:sticky trở thành sửa và thêm hiệu ứng cuộn mà không sử dụng sự kiện scroll.

Xem bản minh hoạ | Nguồn

Kết luận

Tôi thường tự hỏi liệu IntersectionObserver có muốn là một công cụ hữu ích thay thế một số mẫu giao diện người dùng dựa trên sự kiện scroll đã phát triển trong những năm qua. Kết quả là có và không. Ngữ nghĩa của API IntersectionObserver khiến việc sử dụng cho mọi thứ trở nên khó khăn. Nhưng như Tôi đã trình bày ở đây, bạn có thể sử dụng đối tượng này cho một số kỹ thuật thú vị.

Một cách khác để phát hiện các thay đổi về kiểu?

Thực ra là không. Điều chúng tôi cần là một cách để quan sát những thay đổi về kiểu trên phần tử DOM. Rất tiếc, không có giao diện nào trong API nền tảng web cho phép bạn kiểu đồng hồ thay đổi.

MutationObserver là lựa chọn đầu tiên hợp lý nhưng không phù hợp với hầu hết các trường hợp. Ví dụ: trong bản minh hoạ, chúng ta sẽ nhận được một lệnh gọi lại khi sticky lớp được thêm vào một phần tử, nhưng không được thêm khi kiểu đã tính của phần tử thay đổi. Hãy nhớ rằng lớp sticky đã được khai báo khi tải trang.

Trong tương lai, một "Trình quan sát thay đổi kiểu" cho Trình quan sát đột biến có thể hữu ích khi quan sát các thay đổi đối với kiểu đã tính toán của phần tử. position: sticky.