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