เหตุการณ์สำหรับตำแหน่ง CSS:Sticky

TL;DR

ความลับ: คุณอาจไม่ต้องใช้ scroll กิจกรรมในแอปถัดไป การใช้ IntersectionObserver, ฉันจะแสดงวิธีทำให้เหตุการณ์ที่กําหนดเองเริ่มทำงานเมื่อองค์ประกอบ position:sticky ได้รับการแก้ไขแล้วหรือเมื่อองค์ประกอบไม่ติดค้าง ทั้งหมดนี้ทำได้โดยไม่ต้องใช้ Listeners ของการเลื่อน และยังมีวิดีโอสาธิตที่ยอดเยี่ยมเพื่อพิสูจน์ให้คุณเห็น

ดูการสาธิต | แหล่งที่มา

ขอแนะนำกิจกรรม sticky-change

ข้อจำกัดอย่างหนึ่งที่ใช้ได้จริงของการใช้ตำแหน่งติดหนึบ CSS ก็คือ ไม่ได้ให้สัญญาณแพลตฟอร์มเพื่อให้ทราบเมื่อพร็อพเพอร์ตี้ทํางานอยู่ กล่าวคือ ไม่มีเหตุการณ์ที่ทราบว่าองค์ประกอบหนึ่งจะติดหนึบเมื่อใดหรือเมื่อใด จะไม่มีการเหนียวแน่นอีกต่อไป

มาดูตัวอย่างต่อไปนี้ ซึ่งจะแก้ไข <div class="sticky"> 10px จากด้านบนของคอนเทนเนอร์หลัก

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

คงจะดีไม่น้อยหากเบราว์เซอร์บอกได้เมื่อองค์ประกอบถึงจุดนั้น ดูเหมือนว่าฉันไม่ใช่คนเดียวที่คิดเช่นนั้น สัญญาณสำหรับ position:sticky อาจช่วยปลดล็อกกรณีการใช้งานได้หลายกรณี ดังนี้

  1. ใช้เงาตกกระทบกับแบนเนอร์ขณะติดอยู่
  2. ขณะที่ผู้ใช้อ่านเนื้อหาของคุณ ให้บันทึก Hit ของ Analytics เพื่อดูความคืบหน้า
  3. เมื่อผู้ใช้เลื่อนหน้าเว็บ ให้อัปเดตวิดเจ็ตสารบัญแบบลอยไปยังส่วนปัจจุบัน

จากการพิจารณากรณีการใช้งานเหล่านี้ เราจึงได้จัดทำเป้าหมายสุดท้ายขึ้น ซึ่งก็คือการสร้างเหตุการณ์ที่ เริ่มทำงานเมื่อองค์ประกอบ position:sticky ได้รับการแก้ไข สมมติว่าเหตุการณ์นี้ชื่อ 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;
});

demo ใช้เหตุการณ์นี้เพื่อแสดงเงาตกกระทบของส่วนหัวเมื่อมีการแก้ไข และยังอัปเดต ชื่อใหม่ที่ด้านบนของหน้า

ในการสาธิต ระบบจะใช้เอฟเฟกต์โดยไม่มี Scrollevents

เอฟเฟกต์การเลื่อนโดยไม่มีเหตุการณ์การเลื่อน

โครงสร้างของหน้า
โครงสร้างของหน้า

เรามาทบทวนคำศัพท์กันก่อนเพื่อจะได้อ้างอิงชื่อเหล่านี้ได้ตลอดทั้งโพสต์

  1. คอนเทนเนอร์ที่เลื่อน - พื้นที่เนื้อหา (วิวพอร์ตที่มองเห็นได้) ที่มีรายการ "บล็อกโพสต์"
  2. ส่วนหัว - ชื่อสีน้ำเงินในแต่ละส่วนที่มีส่วน position:sticky
  3. ส่วนติดหนึบ - แต่ละส่วนเนื้อหา ข้อความที่เลื่อนอยู่ใต้ส่วนหัวแบบติดแน่น
  4. "โหมดติดหนึบ" - เมื่อใช้ position:sticky กับองค์ประกอบ

หากต้องการทราบว่าส่วนหัวใดเข้าสู่ "โหมดติดหนึบ" เราจำเป็นต้องมีวิธีระบุออฟเซตการเลื่อนของคอนเทนเนอร์การเลื่อน วิธีนี้จะช่วยให้เราคำนวณส่วนหัวที่แสดงอยู่ในปัจจุบันได้ แต่ก็ไม่ใช่เรื่องง่าย อาจดำเนินการได้ยากหากไม่มีเหตุการณ์ scroll :) ปัญหาอื่นคือ position:sticky จะนำองค์ประกอบออกจากเลย์เอาต์เมื่อได้รับการแก้ไขแล้ว

ดังนั้น หากไม่มีเหตุการณ์การเลื่อน เราจะไม่สามารถทําการคํานวณที่เกี่ยวข้องกับเลย์เอาต์ในส่วนหัว

การเพิ่ม DOM จำลองเพื่อกำหนดตำแหน่งการเลื่อน

เราจะใช้ IntersectionObserver แทนเหตุการณ์ scroll เพื่อ ระบุเมื่อส่วนหัวเข้าและออกจากโหมดกดค้าง กำลังเพิ่ม 2 โหนด (หรือที่เรียกว่าผู้รักษาการณ์) ในส่วนติดหนึบแต่ละส่วน โดยอยู่ด้านบนและอีก 1 ส่วน จะทำหน้าที่เป็นจุดอ้างอิงสำหรับระบุตำแหน่งการเลื่อน เป็น เครื่องหมายเข้าและออกจากคอนเทนเนอร์ การเปิดเผยการเปลี่ยนแปลง และ Intersection Observer เรียกใช้ Callback

ไม่มีองค์ประกอบ Sentinel ที่แสดง
องค์ประกอบจุดซ่อนเร้น

เราต้องใช้ Sentinel 2 ตัวเพื่อครอบคลุมการเลื่อนขึ้นและลง 4 กรณี ดังนี้

  1. การเลื่อนลง - ส่วนหัวจะติดอยู่เมื่อ Sentinel ด้านบนตัดผ่านด้านบนของคอนเทนเนอร์
  2. การเลื่อนลง - ส่วนหัวจะออกจากโหมดติดแน่นเมื่อถึงด้านล่างของส่วน และเซ็นติเนลด้านล่างตัดผ่านด้านบนของคอนเทนเนอร์
  3. การเลื่อนขึ้น - ส่วนหัวจะออกจากโหมดติดหนึบเมื่อ Sentinel ด้านบนเลื่อนกลับมาอยู่ในมุมมองจากด้านบน
  4. เลื่อนขึ้น - ส่วนหัวเป็นแบบติดหนึบเมื่อผู้สังเกตการณ์ด้านล่างถอยหลัง ในมุมมองจากด้านบน

การดู Screencast ของ 1-4 ตามลำดับที่เกิดขึ้นจะมีประโยชน์

Intersection Observer จะเรียกใช้การเรียกกลับเมื่อ Sentinels เข้าสู่/ออกจากคอนเทนเนอร์การเลื่อน

CSS

ผู้เฝ้าระวังจะมีตำแหน่งอยู่ที่ด้านบนและด้านล่างของแต่ละส่วน .sticky_sentinel--top อยู่ด้านบนของส่วนหัว ส่วน .sticky_sentinel--bottom อยู่ด้านล่างของส่วน

ผู้รักษาการณ์ล่างถึงเกณฑ์แล้ว
ตําแหน่งขององค์ประกอบ Sentinel ด้านบนและด้านล่าง
: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;
}

การตั้งค่า Intersection Observer

ผู้สังเกตการณ์สี่แยก (Intersection Observers) สังเกตเห็นการเปลี่ยนแปลงของจุดตัดของ องค์ประกอบเป้าหมายและวิวพอร์ตเอกสาร หรือคอนเทนเนอร์หลัก ในกรณีของเรา เรากำลังสังเกตทางแยกที่มีคอนเทนเนอร์หลัก

สูตรเด็ดคือ IntersectionObserver เซนซิเนลแต่ละตัวจะได้รับ IntersectionObserver เพื่อสังเกตการมองเห็นจุดตัดภายใน Scroll Container เมื่อ Sentinel เลื่อนไปยังวิวพอร์ตที่มองเห็นได้ เราจะทราบว่าส่วนหัวนั้นติดอยู่หรือหยุดติด เช่นเดียวกัน เมื่อผู้เฝ้าระวังออก วิวพอร์ต

ก่อนอื่น ฉันตั้งค่าผู้สังเกตการณ์สำหรับ Sentinel ส่วนหัวและส่วนท้าย ดังนี้

/**
 * 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'));

จากนั้น ฉันจึงเพิ่มผู้สังเกตการณ์เพื่อให้เริ่มทำงานเมื่อองค์ประกอบ .sticky_sentinel--top ผ่านไป ผ่านด้านบนของคอนเทนเนอร์แบบเลื่อน (ไม่ว่าจะอยู่ในทิศทางใด) ฟังก์ชัน observeHeaders จะสร้างผู้สังเกตการณ์อันดับสูงสุดและเพิ่ม แต่ละส่วน ผู้สังเกตการณ์จะคํานวณจุดตัดของ Sentinel กับด้านบนของคอนเทนเนอร์ และตัดสินใจว่า Sentinel กำลังเข้าหรือออกจากวิวพอร์ต นั่น ข้อมูลจะเป็นตัวกำหนดว่าส่วนหัวของส่วนนั้นติดอยู่หรือไม่

/**
 * 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));
}

ผู้สังเกตการณ์ได้รับการกำหนดค่าด้วย threshold: [0] เพื่อให้ Callback เริ่มทำงานทันที ในขณะที่ผู้เฝ้าระวังยังคงมองเห็นได้

กระบวนการนี้คล้ายกับของ Sentinel ด้านล่าง (.sticky_sentinel--bottom) ระบบจะสร้างผู้สังเกตการณ์ที่ 2 เพื่อทริกเกอร์เมื่อส่วนท้ายผ่านด้านล่างของคอนเทนเนอร์การเลื่อน ฟังก์ชัน observeFooters จะสร้างโหนดเซนซิเนลและแนบไว้กับแต่ละส่วน ผู้สังเกตการณ์จะคํานวณจุดตัดระหว่างยามกับด้านล่างของคอนเทนเนอร์และตัดสินใจว่าวัตถุกำลังเข้าหรือออกจากคอนเทนเนอร์ ข้อมูลดังกล่าวจะเป็นตัวกำหนดว่าส่วนหัวของส่วนจะติดอยู่หรือไม่

/**
 * 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));
}

กำหนดค่าผู้สังเกตการณ์ด้วย threshold: [1] เพื่อให้การเรียกกลับทำงานเมื่อโหนดทั้งโหนดอยู่ในมุมมอง

สุดท้าย มียูทิลิตี 2 รายการสำหรับเริ่มเหตุการณ์ที่กำหนดเอง sticky-change และสร้างการเฝ้าระวัง

/**
 * @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);
}

เท่านี้ก็เรียบร้อย

การสาธิตขั้นสุดท้าย

เราสร้างเหตุการณ์ที่กำหนดเองเมื่อองค์ประกอบที่มี position:sticky กลายเป็น แก้ไขและเพิ่มเอฟเฟกต์การเลื่อนโดยไม่ต้องใช้เหตุการณ์ scroll

ดูการสาธิต | แหล่งที่มา

บทสรุป

เรามักจะสงสัยว่า IntersectionObserver จะเป็นเครื่องมือที่มีประโยชน์ในการแทนที่รูปแบบ UI scroll ตามเหตุการณ์บางส่วนที่พัฒนาขึ้นในช่วงหลายปีที่ผ่านมาหรือไม่ ผลปรากฏว่าคำตอบคือทั้งใช่และไม่ ความหมายของ IntersectionObserver API ทำให้ใช้งานกับทุกสิ่งได้ยาก แต่เนื่องจาก ที่เราได้แสดงไว้ที่นี่ คุณสามารถใช้สิ่งนี้กับเทคนิคที่น่าสนใจ

อีกวิธีในการตรวจหาการเปลี่ยนแปลงสไตล์ใช่ไหม

ไม่ครับ สิ่งที่เราต้องการคือวิธีสังเกตการเปลี่ยนแปลงสไตล์ในองค์ประกอบ DOM ขออภัย ไม่มี API ของแพลตฟอร์มเว็บที่ให้คุณติดตามการเปลี่ยนแปลงสไตล์ได้

MutationObserver จะเป็นตัวเลือกแรกที่สมเหตุสมผล แต่ไม่เหมาะสำหรับ เกือบทุกกรณี ตัวอย่างเช่น ในการสาธิต เราจะได้รับการติดต่อกลับเมื่อ sticky ระบบจะเพิ่มคลาสไปยังองค์ประกอบ แต่ไม่เพิ่มเมื่อรูปแบบที่คำนวณแล้วขององค์ประกอบมีการเปลี่ยนแปลง โปรดทราบว่าระบบประกาศคลาส sticky เมื่อโหลดหน้าเว็บแล้ว

ในอนาคต "ตัวสังเกตการเปลี่ยนแปลงรูปแบบ" อาจเป็นประโยชน์ต่อการสังเกตการเปลี่ยนแปลง ของรูปแบบที่คำนวณแล้วของ เอลิเมนต์ position: sticky