สรุปคร่าวๆ
เคล็ดลับ: คุณอาจไม่ต้องใช้เหตุการณ์ scroll
ในแอปถัดไป การใช้
IntersectionObserver
เราจะแสดงวิธีทำให้เหตุการณ์ที่กำหนดเองเริ่มทำงานเมื่อองค์ประกอบ position:sticky
ได้รับการแก้ไขแล้วหรือเมื่อองค์ประกอบไม่คงที่ ไม่ต้องใช้ Listener แบบเลื่อนเลย นอกจากนี้ยังมีการสาธิตที่ยอดเยี่ยมเพื่อพิสูจน์
ขอแนะนำกิจกรรม sticky-change
ข้อจำกัดในทางปฏิบัติอย่างหนึ่งของการใช้ตำแหน่งติดหนึบของ CSS คือไม่ให้สัญญาณแพลตฟอร์มให้รู้เมื่อพร็อพเพอร์ตี้ทำงานอยู่ กล่าวอีกนัยหนึ่งคือ ไม่มีเหตุการณ์ที่ทราบว่าองค์ประกอบหนึ่งติดหนึบเมื่อใด หรือเมื่อองค์ประกอบไม่ติดหนึบแล้ว
ลองดูตัวอย่างต่อไปนี้ ซึ่งแก้ไข <div class="sticky">
10px จากด้านบนของคอนเทนเนอร์ระดับบนสุด
.sticky {
position: sticky;
top: 10px;
}
คงจะดีใช่ไหมหากเบราว์เซอร์บอกให้พวกเขาเห็นองค์ประกอบเหล่านั้นเมื่อองค์ประกอบเข้าจุด
ดูเหมือนฉันจะไม่ใช่คนเดียวที่คิดอย่างนั้น
สัญญาณของ position:sticky
อาจช่วยปลดล็อกกรณีการใช้งานได้หลายอย่าง เช่น
- ใช้เงาตกกระทบกับแบนเนอร์ขณะที่ยึดติด
- ขณะที่ผู้ใช้อ่านเนื้อหาของคุณ ให้บันทึก Hit ของข้อมูลวิเคราะห์เพื่อให้ทราบความคืบหน้า
- ขณะที่ผู้ใช้เลื่อนหน้าเว็บ ให้อัปเดตวิดเจ็ต 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;
});
การสาธิตจะใช้เหตุการณ์นี้เพื่อกำหนดทิศทางของเงาตกกระทบเมื่อได้รับการแก้ไข และยังอัปเดตชื่อใหม่ที่ด้านบนของหน้าอีกด้วย
เอฟเฟกต์การเลื่อนโดยไม่มีเหตุการณ์การเลื่อนใช่ไหม
![โครงสร้างของหน้า](https://developer.chrome.com/static/docs/css-ui/sticky-headers/image/structure-the-page-de19d5c111bd1.png?authuser=6&hl=th)
เราจะลองเรียกคำศัพท์ง่ายๆ มาใช้เพื่ออ้างอิง ชื่อเหล่านี้ตลอดทั้งโพสต์
- Scrolling Container - พื้นที่เนื้อหา (วิวพอร์ตที่มองเห็นได้) ที่มีรายการ "บล็อกโพสต์"
- ส่วนหัว - ชื่อสีน้ำเงินในแต่ละส่วนที่มี
position:sticky
- ส่วนติดหนึบ - แต่ละส่วนเนื้อหา ข้อความที่เลื่อนอยู่ใต้ส่วนหัว แบบติดหนึบ
- "โหมดติดหนึบ" - เมื่อใช้
position:sticky
กับองค์ประกอบ
หากต้องการทราบว่าส่วนหัวใดเข้าสู่ "โหมดกดค้าง" เราต้องหาวิธีกำหนดออฟเซ็ตการเลื่อนของคอนเทนเนอร์แบบเลื่อน ซึ่งจะทำให้เรามีวิธีคำนวณส่วนหัวที่แสดงอยู่ แต่ก็เป็นเรื่องที่ยุ่งยากมากหากไม่มีเหตุการณ์ scroll
:) ปัญหาอีกอย่างคือ position:sticky
จะนำองค์ประกอบออกจากเลย์เอาต์เมื่อได้รับการแก้ไขแล้ว
หากไม่มีเหตุการณ์การเลื่อน เราจึงทำการคำนวณที่เกี่ยวข้องกับเลย์เอาต์ส่วนหัวไม่ได้
การเพิ่ม Dumby DOM เพื่อระบุตำแหน่งการเลื่อน
เราจะใช้ IntersectionObserver
แทนเหตุการณ์ scroll
ในการพิจารณาเมื่อheadersเข้าและออกจากโหมดติดหนึบ การเพิ่มโหนด 2 โหนด (หรือที่เรียกกันว่า "ผู้รักษา" ในส่วนติดหนึบแต่ละส่วน ทั้ง 1 โหนดที่ด้านบนและอีก 1 โหนดที่ด้านล่างจะทำหน้าที่เป็นจุดอ้างอิงสำหรับการหาตำแหน่งการเลื่อน เมื่อเครื่องหมายเหล่านี้เข้าและออกจากคอนเทนเนอร์ การเปิดเผยการเปลี่ยนแปลงและ
Intersection Observer จะเริ่มการทำงานของ Callback
![ไม่แสดงองค์ประกอบของการเฝ้าระวัง](https://developer.chrome.com/static/docs/css-ui/sticky-headers/image/without-sentinel-elements-f9529d3483c91.gif?authuser=6&hl=th)
เราต้องขอให้คุณช่วยดูแล 2 กรณีเพื่อให้ครอบคลุมกรณีการเลื่อนขึ้นและลง 4 กรณี ดังนี้
- การเลื่อนลง - ส่วนหัวจะติดหนึบเมื่อเซนติเมทด้านบนก้าวข้ามด้านบนของคอนเทนเนอร์
- เลื่อนลง - ส่วนหัวจะออกจากโหมดกดค้างเมื่อไปถึงด้านล่างของส่วน และเซนติเนลด้านล่างพาดผ่านด้านบนของคอนเทนเนอร์
- เลื่อนขึ้น - ส่วนหัวจะออกจากโหมดติดหนึบเมื่อมองเห็นจุดบนของประโยคนั้นเลื่อนกลับเข้าสู่มุมมองจากด้านบน
- เลื่อนขึ้น - ส่วนหัวจะติดหนึบเมื่อผู้รักษาประตูด้านล่างถอยหลังเข้าสู่มุมมองจากด้านบน
เราขอแนะนำให้คุณเห็น Screencast ตัวเลข 1-4 ตามลำดับ
CSS
ผู้เฝ้าระวังจะมีตำแหน่งอยู่ที่ด้านบนและด้านล่างของแต่ละส่วน
.sticky_sentinel--top
อยู่ด้านบนของส่วนหัวโดยที่ .sticky_sentinel--bottom
อยู่ที่ด้านล่างของส่วนดังกล่าว:
![ผู้ฟังชั้นล่างถึงเกณฑ์แล้ว](https://developer.chrome.com/static/docs/css-ui/sticky-headers/image/bottom-sentinel-reaching-208a13f07bd7c.png?authuser=6&hl=th)
: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
.