אמ;לק
הנה סוד: יכול להיות שלא תצטרכו אירועי scroll
באפליקציה הבאה. באמצעות IntersectionObserver
,
אני מראה איך אפשר להפעיל אירוע מותאם אישית כשרכיבי position:sticky
מתוקנים או כשהם מפסיקים להיתקע. כל זאת ללא שימוש בפונקציות listener לגלילה. יש אפילו הדגמה מדהימה שמוכיחה זאת:
חדש: האירוע של sticky-change
אחת מהמגבלות המעשיות של שימוש במיקום דביק של CSS היא בכך שהוא לא מספק אות לפלטפורמה כדי לדעת מתי הנכס פעיל. במילים אחרות, אין אירוע שניתן לדעת מתי אלמנט הופך להיות "דביק" או מתי הוא מפסיק להיות "דביק".
ניקח את הדוגמה הבאה, שמתקנת <div class="sticky">
10px מהחלק העליון של מאגר ההורה שלו:
.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;
});
הדמו משתמש באירוע הזה ככותרת של הטלת צללית, ברגע שיתוקנו. היא גם מעדכנת את הכותרת החדשה בראש הדף.
אפקטים של גלילה בלי אירועי גלילה?
בוא נעלה כמה מהמונחים כדי שאוכל להתייחס לשמות האלה בשאר הפוסט:
- Scrolling container - אזור התוכן (אזור התצוגה הגלוי) שמכיל את הרשימה של 'blog posts' (פוסטים בבלוג).
- כותרות - כותרת כחולה בכל קטע שבו יש
position:sticky
. - קטעים דביקים - כל קטע תוכן. הטקסט שגולל מתחת לכותרות הדביקות.
- "Sticky mode" - כאשר
position:sticky
מוחל על הרכיב.
כדי לדעת איזו כותרת נכנסת ל'מצב דביק', נדרשת דרך כלשהי לקביעת קיזוז הגלילה של מאגר הגלילה. כך נוכל לחשב את הכותרת שמופיעה כרגע. עם זאת, די מסובך לעשות בלי scroll
אירועים :) הבעיה הנוספת היא
position:sticky
שמסיר את הרכיב מהפריסה כשהבעיה תיפתר.
לכן, בלי אירועי גלילה, איבדנו את האפשרות לבצע חישובים שקשורים לפריסה בכותרות.
הוספת DOM של dumby כדי לקבוע את מיקום הגלילה
במקום אירועים מסוג scroll
, נשתמש ב-IntersectionObserver
כדי לקבוע מתי headers נכנסות ויוצאות ממצב 'הקשה ביד אחת'. הוספה של שני צמתים (שנקראים גם סנטינל) בכל קטע במיקום קבוע, אחת בחלק העליון ואחד בחלק התחתון, תשמש כציוני דרך לזיהוי מיקום הגלילה. כשהסמנים האלה נכנסים
למאגר ויוצאים ממנו, החשיפה שלהם משתנה, וה-Intersection Reportinger מפעיל קריאה חוזרת (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;
}
הגדרת צופים בצמתים
צופים בצמתים שמים לב באופן אסינכרוני שינויים בהצטלבות של רכיב יעד ואזור התצוגה של המסמך או במאגר הורה. במקרה שלנו, אנחנו רואים צמתים עם קונטיינר הורה.
הרוטב הקסום הוא 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
יוצרת את צומתי סנטינל ומצרפת אותם לכל קטע. הצופה מחשב את הצטלבות של הסנטיינל עם החלק התחתון של הקונטיינר, ומחליט אם להיכנס אליו או לצאת ממנו. המידע הזה קובע אם כותרת הקטע נשארת או לא.
/**
* 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 Writeer כדי לבדוק שינויים בסגנונות המחושבים של הרכיב.
position: sticky
.