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

TL;DR

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

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

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

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

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

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

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

  1. ใช้เงาตกกระทบกับแบนเนอร์ขณะที่ติดอยู่
  2. ขณะที่ผู้ใช้อ่านเนื้อหาของคุณ ให้บันทึก Hit ของ Analytics เพื่อให้ทราบ ความคืบหน้า
  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;
});

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

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

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

วันที่ โครงสร้างของหน้า
โครงสร้างของหน้า

หลบคำศัพท์เฉพาะออกไป เพื่อให้ฉันเรียกชื่อเหล่านี้ได้ ตลอดทั้งโพสต์ที่เหลือ:

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

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

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

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

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

วันที่ ไม่แสดงองค์ประกอบของการเฝ้าระวัง
องค์ประกอบจุดซ่อนเร้น

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

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

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

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

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 จะสร้างฟังก์ชัน โหนด Sentinel และแนบไปกับแต่ละส่วน ผู้สังเกตการณ์จะคำนวณ จุดตัดของจุดตัดกับด้านล่างของคอนเทนเนอร์ และกำหนดว่าเป็น เข้าหรือออก ข้อมูลดังกล่าวจะเป็นตัวกำหนดว่าส่วนหัวของส่วน ติดหรือเปล่า

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

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

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

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

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