אמ;לק
זהו סוד: יכול להיות שלא יהיה צורך באירועי scroll
באפליקציה הבאה. באמצעות IntersectionObserver
,
כאן מוסבר איך אפשר להפעיל אירוע בהתאמה אישית כשרכיבי position:sticky
מתוקנים או כשהם מפסיקים להדביק. כל זה בלי צורך
במאזינים גלילה. יש אפילו הדגמה מדהימה שמוכיחה את זה:
היכרות עם האירוע sticky-change
אחת המגבלות המעשיות לשימוש במיקום דביק של CSS היא שאין אות פלטפורמה שמאפשרת לדעת מתי הנכס פעיל. במילים אחרות, אין אירוע שאפשר לדעת מתי רכיב הופך לדביק או מתי הוא מפסיק להיות במיקום קבוע.
ניקח לדוגמה את הדוגמה הבאה, שמתקנים 10px של <div class="sticky">
בחלק העליון של קונטיינר ההורה:
.sticky {
position: sticky;
top: 10px;
}
לא יהיה נחמד אם הדפדפן יציג הודעה כשהאלמנטים מגיעים לסמן הזה?
נראה שאני לא היחיד שחושב כך. אות של position:sticky
יכול לפתוח כמה תרחישים לדוגמה:
- מחילים הטלת צללית על באנר שנדבקת.
- כשהמשתמשים קוראים את התוכן שלכם, אתם יכולים להקליט היטים של ניתוח נתונים כדי לדעת מה ההתקדמות שלהם.
- כשהמשתמש גולל בדף, מעדכנים את ווידג'ט ה-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
מוחל על הרכיב.
כדי לדעת איזה header נכנס ל-'sticky mode', אנחנו צריכים דרך לקבוע את היסט הגלילה של מאגר הגלילה. כך נוכל לחשב את הכותרת שמופיעה כרגע. אבל זה די מסובך בלי אירועי scroll
:) הבעיה הנוספת היא
ש-position:sticky
מסיר את הרכיב מהפריסה כשהבעיה תיפתר.
לכן בלי אירועי גלילה, איבדנו את היכולת לבצע חישובים שקשורים לפריסה בכותרות.
הוספת DOM דו-ממדי כדי לקבוע את מיקום הגלילה
במקום אירועי scroll
, נשתמש ב-IntersectionObserver
כדי לקבוע מתי headers ייכנסו למצב 'דביק' ויצאו ממנו. הוספת שני צמתים (שנקראים גם סנטינלים) לכל קטע במיקום קבוע, אחד למעלה והשני בחלק התחתון, ישמשו כנקודות ציון לזיהוי מיקום הגלילה. כשהסמנים האלה נכנסים למאגר ויוצאים ממנו, החשיפה שלהם משתנה ו-Intersection Observer מפעיל קריאה חוזרת (callback).
אנחנו זקוקים לשני סנטימנטים כדי לכסות ארבעה מקרים של גלילה למעלה ולמטה:
- גלילה למטה – הכותרת נדבקת כשהסנטינל העליון של הקונטיינר חוצה את החלק העליון של הקונטיינר.
- גלילה למטה – כותרת משאירה מצב 'דביק' כשהיא מגיעה לתחתית הקטע והסנטינל התחתון שלו חוצה את החלק העליון של הקונטיינר.
- גלילה למעלה – האפשרות header משאירה מצב 'דביק' כשהסנטינל העליון של הדף נגלל בחזרה לתצוגה מלמעלה.
- גלילה למעלה – הכותרת נשארת במיקום קבוע כשהסנטינל התחתון של המסך חוזר לתצוגה מלמעלה.
כדאי לראות הקלטת מסך מ-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
יוצרת את הסנטינים המובילים ומוסיפה אותם לכל קטע. התצפית מחשבת את החתך של הסנטינל עם החלק העליון של הקונטיינר ומחליטים אם הוא נכנס לאזור התצוגה או יוצא ממנו. המידע הזה קובע אם כותרת הקטע תישאר בתוקף או לא.
/**
* 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]
כך שהקריאה החוזרת שלו מופעלת ברגע שהסנטינל מוצג.
התהליך דומה לסנטינל התחתון (.sticky_sentinel--bottom
).
צופה שני נוצר כדי להפעיל כשהכותרות התחתונות עוברות דרך החלק התחתון של מאגר הגלילה. הפונקציה 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]
כך שהקריאה החוזרת שלו מופעלת כשכל הצומת נמצא בטווח התצוגה.
לסיום, יש שתי כלים שלי להפעלת האירוע המותאם אישית 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
יכול להיות כלי מועיל שיחליף חלק מתבניות ממשק המשתמש מבוססות-האירועים של scroll
שפותחו לאורך השנים. מסתבר שהתשובה היא כן ולא. הסמנטיקה של API IntersectionObserver
מקשה על השימוש בכולם. אבל כמו שהראיתי כאן, אפשר להשתמש בו לכמה טכניקות מעניינות.
דרך נוספת לזהות שינויים בסגנון?
לא ממש. מה שהיינו צריכים זו דרך לראות שינויי סגנון ברכיב DOM. לצערי אין שום דבר בממשקי ה-API של פלטפורמת האינטרנט שמאפשרים לצפות בשינויים בסגנון.
מומלץ להשתמש קודם כל על ידי MutationObserver
, אבל זה לא עובד ברוב המקרים. לדוגמה, בהדגמה, נקבל קריאה חוזרת כשמוסיפים את המחלקה sticky
לרכיב, אבל לא כשהסגנון המחושב של הרכיב משתנה.
חשוב לזכור שהמחלקה sticky
כבר הוצהרה במהלך טעינת הדף.
בעתיד, תוסף "Style Mutation Observer" ל-Mutation Observers עשוי להועיל לצפייה בשינויים בסגנונות המחושבים של אלמנט.
position: sticky
.