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

สรุปคร่าวๆ

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

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

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

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

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

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

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

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

เราสร้างเป้าหมายสุดท้ายซึ่งก็คือการสร้างเหตุการณ์ที่เริ่มทำงานเมื่อองค์ประกอบ 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;
});

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

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

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

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

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

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

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

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

การเพิ่ม Dumby DOM เพื่อระบุตำแหน่งการเลื่อน

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

ไม่แสดงองค์ประกอบของการเฝ้าระวัง
องค์ประกอบเซนติเนลที่ซ่อนอยู่

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

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

เราขอแนะนำให้คุณเห็น Screencast ตัวเลข 1-4 ตามลำดับ

ผู้สังเกตการณ์ทางแยกจะเริ่มเรียกใช้ Callback เมื่อผู้เฝ้าระวังเข้า/ออกจากคอนเทนเนอร์แบบเลื่อน

CSS

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

ผู้ฟังชั้นล่างถึงเกณฑ์แล้ว
ตำแหน่งขององค์ประกอบจุดชมด้านบนและด้านล่าง
: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 Observers จะสังเกตการเปลี่ยนแปลงในส่วนจุดตัดขององค์ประกอบเป้าหมายและวิวพอร์ตเอกสารหรือคอนเทนเนอร์ระดับบน ในกรณีของเรา เราสังเกตทางแยกที่มีคอนเทนเนอร์หลัก

เวทมนตร์คือ IntersectionObserver ผู้สังเกตการณ์แต่ละรายการจะได้รับ IntersectionObserver สำหรับผู้สังเกตการณ์การแสดงทางแยกภายในคอนเทนเนอร์แบบเลื่อน เมื่อผู้สังเกตการณ์เลื่อนเข้ามาในวิวพอร์ตที่มองเห็นได้ จะทราบดีว่าส่วนหัวติดอยู่กับที่หรือหยุดติดหนึบ และในทำนองเดียวกัน เมื่อผู้เฝ้าระวังออกจากวิวพอร์ต

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

/**
 * 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 จะสร้างความรู้สึกในอันดับต้นๆ และเพิ่มลงไปในแต่ละส่วน ผู้สังเกตการณ์จะคำนวณจุดตัดของจุดตัดกับด้านบนของคอนเทนเนอร์ และตัดสินใจว่าจะเข้าหรือออกจากวิวพอร์ต ข้อมูลดังกล่าวจะเป็นตัวกำหนดว่าส่วนหัวของส่วนจะยังคงอยู่ไหม

/**
 * 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 เริ่มทำงานทันทีที่ผู้สังเกตการณ์ปรากฏ

ขั้นตอนจะคล้ายกับเซนติเนลด้านล่าง (.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] เพื่อให้ Callback เริ่มทำงานเมื่อทั้งโหนดอยู่ในมุมมอง

สุดท้าย มียูทิลิตี 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 ที่มีการพัฒนาในช่วงหลายปีที่ผ่านมาได้ไหม คำตอบคือใช่และไม่ใช่ ความหมายของ API IntersectionObserver ทำให้ใช้งานทุกอย่างได้ยาก แต่อย่างที่ผมแสดงที่นี่ คุณสามารถใช้ สำหรับเทคนิคที่น่าสนใจบางอย่าง

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

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

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

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