אירוע עבור מיקום CSS:דביק

אמ;לק

סודי: יכול להיות שאין לך צורך באירועי scroll באפליקציה הבאה. באמצעות IntersectionObserver, למדתי איך להפעיל אירוע בהתאמה אישית כשרכיבי position:sticky מתוקנים או כשהם מפסיקים להדביק. כל זה בלי השימוש ב-פונקציות listener לגלילה. יש אפילו הדגמה מדהימה שמוכיחה את זה:

לצפייה בהדגמה | מקור

היכרות עם האירוע sticky-change

אחת המגבלות המעשיות של שימוש במיקום הדביק ב-CSS היא לא מספק אות פלטפורמה לדעת מתי הנכס פעיל. במילים אחרות, אין אירוע שאפשר לדעת מתי רכיב נתקע או מתי הוא מפסיק להידבקות.

ניקח את הדוגמה הבאה, שתתקן <div class="sticky"> 10px החלק העליון של מאגר ההורה שלו:

.sticky {
  position: sticky;
  top: 10px;
}

לא יהיה נחמד אם הדפדפן יציג הודעה כשהאלמנטים מגיעים לסמן הזה? נראה שאני לא היחיד ככה ככה. אות של position:sticky עשוי לפתוח כמה תרחישים לדוגמה:

  1. מחילים הטלת צללית על באנר שנדבקת.
  2. כשהמשתמש קורא את התוכן שלך, אפשר להקליט היטים של ניתוח נתונים כדי לדעת מהם על ההתקדמות במשחק.
  3. כשמשתמש גולל בדף, אפשר לעדכן ווידג'ט צף של 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;
});

ההדגמה משתמשת את האירוע הזה לכותרות של הטלת צללית כשהם יתוקנו. הוא גם מעדכן את כותרת חדשה בחלק העליון של הדף.

בהדגמה, המערכת מחילה את האפקטים ללא אירועי Scrollevents.

לגלול את האפקטים בלי אירועי גלילה?

מבנה הדף.
מבנה הדף.

נפסיק את השימוש במונחים מסוימים כדי שאוכל להתייחס לשמות האלה. בהמשך הפוסט:

  1. מאגר גלילה - אזור התוכן (אזור תצוגה גלוי) שמכיל את "פוסטים בבלוג".
  2. כותרות - כותרת כחולה בכל קטע שיש להן position:sticky.
  3. קטעים במיקום קבוע – כל קטע תוכן. הטקסט שגולל מתחת כותרות במיקום קבוע.
  4. "מצב קבוע" – כאשר position:sticky מוחל על הרכיב.

כדי לדעת איזו כותרת מכניסה 'מצב דביק', אנחנו צריכים דרך היסט הגלילה של מאגר הגלילה. זה יעזור לנו כדי לחשב את הכותרת שמוצגת עכשיו. אבל זה די יפה, קשה לעשות בלי אירועי scroll :) הבעיה הנוספת היא הרכיב position:sticky מסיר את הרכיב מהפריסה כשהבעיה תיפתר.

לכן בלי אירועי גלילה, איבדנו את היכולת לבצע פעולות שקשורות לפריסה וחישובים בכותרות.

הוספת DOM דו-ממדי כדי לקבוע את מיקום הגלילה

במקום אירועי scroll, נשתמש ב-IntersectionObserver כדי לקבוע מתי כותרות נכנסות למצב 'הקשה ביד אחת' ויוצאות ממנו. הוספת שני צמתים (שנקראים גם 'סנטינלים') בכל קטע במיקום קבוע, אחד למעלה ואחד בחלק התחתון, ישמשו כנקודות ציון לזיהוי מיקום הגלילה. כמו אלה סמנים שנכנסים למאגר ויוצאים ממנו, החשיפה שלהם משתנה 'צומת השידורים' מפעיל קריאה חוזרת (callback).

ללא אלמנטים של סנטינל
אלמנטים של סנטינל.

אנחנו זקוקים לשני סנטימנטים כדי לכסות ארבעה מקרים של גלילה למעלה ולמטה:

  1. גלילה למטההכותרת נתקעת כשהסנטינל העליון שלה חוצה את התמונה החלק העליון של המאגר.
  2. גלילה למטה – האפשרות כותרת משאירה מצב 'דביק' כשהיא מגיעה לתחתית של החלק והסנטינל התחתון שלו חוצים את החלק העליון של המכל.
  3. גלילה למעלה – האפשרות כותרת משאירה מצב 'דביק' כשהסנטינל העליון נגלל. בחזרה לתצוגה מלמעלה.
  4. גלילה למעלההכותרת נתקעת בזמן שהסנטינל התחתון שלה זז חזרה בתצוגה מלמעלה.

כדאי לראות הקלטת מסך מ-1 עד 4 לפי הסדר שבו הן מתרחשות:

צופים צומת מפעילים קריאות חוזרות (callbacks) כאשר החיישנים נכנסים או יוצאים ממאגר הגלילה.

שירות ה-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 היא אפשרות הגיונית בשלב הראשון, אבל זה לא מתאים ברוב המקרים. לדוגמה, בהדגמה, נקבל התקשרות חזרה כאשר sticky ה-class מתווסף לרכיב, אבל לא כאשר הסגנון המחושב של הרכיב משתנה. חשוב לזכור שהמחלקה sticky כבר הוצהרה במהלך טעינת הדף.

בעתיד, 'Style Mutation Observer' ל-Mutation Observers יכולה לעזור לך להבחין בשינויים של הסגנונות המחושבים של הרכיב. position: sticky.