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