สรุปคร่าวๆ
ความลับก็คือ คุณอาจไม่จำเป็นต้องมีเหตุการณ์ scroll
ในแอปถัดไป เมื่อใช้
IntersectionObserver
ผมจะแสดงวิธีเริ่มการทำงานของเหตุการณ์ที่กำหนดเองเมื่อองค์ประกอบ position:sticky
ได้รับการแก้ไขแล้วหรือเมื่อองค์ประกอบหยุดนิ่ง ทั้งหมดนี้ไม่ต้องใช้
ตัว Listener แบบเลื่อน นอกจากนี้ยังมีการสาธิตที่ยอดเยี่ยมให้พิสูจน์ด้วย:
ขอแนะนำกิจกรรม sticky-change
หนึ่งในข้อจำกัดที่ใช้ได้จริงของการใช้ตำแหน่ง Sticky ของ CSS คือ ไม่มีการให้สัญญาณแพลตฟอร์มเพื่อให้ทราบเมื่อพร็อพเพอร์ตี้ทำงานอยู่ กล่าวคือ จะไม่มีเหตุการณ์ใดๆ ที่จะทราบเมื่อองค์ประกอบเป็นแบบติดหนึบหรือเมื่อองค์ประกอบหยุดนิ่ง
ดูตัวอย่างต่อไปนี้ ซึ่งจะแก้ไข <div class="sticky">
10 พิกเซลจากด้านบนของคอนเทนเนอร์ระดับบนสุด
.sticky {
position: sticky;
top: 10px;
}
คงจะดีใช่ไหมหากเบราว์เซอร์บอกเวลาที่องค์ประกอบต่างๆ ทำเครื่องหมาย
ดูเหมือนฉันไม่ใช่คนเดียวที่คิดอย่างนั้น สัญญาณสำหรับ position:sticky
สามารถปลดล็อก Use Case ได้หลายกรณี:
- ใช้เงาตกกระทบบนแบนเนอร์ขณะที่ติดอยู่
- ขณะที่ผู้ใช้อ่านเนื้อหาของคุณ ให้บันทึก Hit จาก Analytics เพื่อดูความคืบหน้า
- เมื่อผู้ใช้เลื่อนหน้าเว็บ ให้อัปเดตวิดเจ็ต 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;
});
การสาธิตจะใช้เหตุการณ์นี้เพื่อสร้างส่วนหัวเงาตกกระทบเมื่อได้รับการแก้ไข และยังอัปเดตชื่อใหม่ที่ด้านบนของหน้าด้วย
เอฟเฟกต์การเลื่อนโดยไม่มีเหตุการณ์การเลื่อนใช่ไหม
ตอนนี้เรามาตัดคำศัพท์กันก่อน จะได้เรียกชื่อเหล่านี้ ได้ตลอดทั้งโพสต์
- การเลื่อนคอนเทนเนอร์ - พื้นที่เนื้อหา (วิวพอร์ตที่มองเห็นได้) ที่มีรายการ "บล็อกโพสต์"
- ส่วนหัว - ชื่อสีน้ำเงินในแต่ละส่วนที่มี
position:sticky
- ส่วนที่มีความสามารถในการดึงดูด - ส่วนของเนื้อหาแต่ละส่วน ข้อความที่เลื่อนลงมาใต้ ส่วนหัวแบบติดหนึบ
- "โหมดกดค้าง" - เมื่อ
position:sticky
มีผลกับองค์ประกอบ
เพื่อให้รู้ว่าส่วนหัวใดเข้าสู่ "โหมดกดค้าง" เราจะต้องกำหนดออฟเซ็ตการเลื่อนของคอนเทนเนอร์การเลื่อน ซึ่งจะช่วยให้เราคำนวณส่วนหัวที่กำลังแสดงอยู่ได้ แต่นั่นถือว่าค่อนข้างซับซ้อนหากไม่มีเหตุการณ์ scroll
:) ส่วนปัญหาอื่นๆ คือ position:sticky
จะนำองค์ประกอบออกจากเลย์เอาต์เมื่อแก้ไขแล้ว
เราจึงสูญเสียความสามารถในการคำนวณเกี่ยวกับเลย์เอาต์ในส่วนหัวหากไม่มีเหตุการณ์การเลื่อน
การเพิ่ม DOM Dumby เพื่อกำหนดตำแหน่งการเลื่อน
เราจะใช้ IntersectionObserver
เพื่อกำหนดเมื่อheadersเข้าและออกจากโหมดติดหนึบแทนเหตุการณ์ scroll
การเพิ่มโหนด 2 รายการ
(หรือที่เรียกว่า "เซนทิเนล)" ในแต่ละส่วนที่ติดหนึบ โดยจุดหนึ่งอยู่ที่ด้านบนสุดและอีกโหนดหนึ่งที่ด้านล่างจะทำหน้าที่เป็นจุดอ้างอิงในการกำหนดตำแหน่งการเลื่อน เมื่อเครื่องหมายเหล่านี้เข้าและออกจากคอนเทนเนอร์ การเปิดเผยของเครื่องหมายจะเปลี่ยนแปลง และ Intersection Observer จะเริ่มการเรียกกลับ
เราต้องการเจ้าหน้าที่ 2 คนเพื่อให้ครอบคลุมกรณีการเลื่อนขึ้นลง 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;
}
การตั้งค่าผู้สังเกตการณ์ทางแยก
ผู้สังเกตการณ์ทางแยกจะสังเกตการเปลี่ยนแปลงที่เกิดขึ้นในเวลาตัดกันขององค์ประกอบเป้าหมายและวิวพอร์ตของเอกสารหรือคอนเทนเนอร์ระดับบนสุด ในกรณีของเรา เราจะสังเกตทางแยกที่มีคอนเทนเนอร์หลัก
สูตรเด็ดคือ 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
จะสร้างความรู้สึกยอดนิยมและเพิ่มลงในแต่ละส่วน ผู้สังเกตการณ์คำนวณจุดตัดของ 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]
เพื่อให้โค้ดเรียกกลับเริ่มทำงานทันทีที่เซ็นเซอร์ปรากฏ
ขั้นตอนนี้คล้ายกับ 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
.