حدث لموضع CSS:Sticky

إريك بيدلمان

الملخّص

إليك سر: قد لا تحتاج إلى أحداث scroll في تطبيقك التالي. باستخدام IntersectionObserver، أوضح لك كيف يمكنك تنشيط حدث مخصّص عندما تصبح عناصر position:sticky ثابتة أو عند توقفها عن التثبيت. كل ذلك بدون استخدام أدوات معالجة التمرير هناك أيضًا عرض توضيحي رائع لإثبات ذلك:

الاطّلاع على العرض التوضيحي | المصدر

التعريف بحدث sticky-change

يتمثل أحد القيود العملية لاستخدام موضع ثابت في CSS في أنه لا يوفر إشارة للنظام الأساسي لمعرفة ما إذا كان الموقع نشطًا. بمعنى آخر، ما من حدث يمكن معرفة متى يصبح العنصر لاصقًا أو متى يصبح لاصقًا.

خذ المثال التالي، الذي يربط <div class="sticky"> 10 بكسل من أعلى الحاوية الرئيسية:

.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;
});

يستخدِم العرض التوضيحي هذا الحدث لتغطية الظل الخلفي عندما يتم إصلاحه. كما يحدّث العنوان الجديد في أعلى الصفحة.

في الإصدار التجريبي، يتم تطبيق التأثيرات بدون أحداث التمرير.

هل تريد تمرير التأثيرات بدون عناصر الانتقال للأسفل؟

بنية الصفحة.
بنية الصفحة:

لنبعد بعض المصطلحات حتى أتمكن من الإشارة إلى هذه الأسماء في بقية المنشور:

  1. حاوية التمرير - منطقة المحتوى (إطار العرض المرئي) التي تتضمن قائمة "مشاركات المدونة".
  2. العناوين - عنوان أزرق في كل قسم يحتوي على position:sticky.
  3. الأقسام الثابتة: لكل قسم من أقسام المحتوى هو النص الذي يتم تمريره أسفل العناوين اللاصقة.
  4. "وضع التثبيت" - عندما يتم تطبيق position:sticky على العنصر

لمعرفة العنوان الذي يدخل في "وضع التثبيت"، نحتاج إلى طريقة لتحديد إزاحة التمرير في حاوية التمرير. وهذا من شأنه أن يمنحنا طريقة لحساب العنوان المعروض حاليًا. في المقابل، يكون تنفيذ هذا الإجراء صعبًا بدون استخدام أحداث scroll :) والمشكلة الأخرى هي أنّ position:sticky يزيل العنصر من التنسيق عندما يتم إصلاحه.

لذلك بدون أحداث التمرير، فقدنا القدرة على إجراء العمليات الحسابية المرتبطة بالتنسيق على العناوين.

إضافة dumby DOM لتحديد موضع التمرير

بدلاً من أحداث scroll، سنستخدم IntersectionObserver لتحديد وقت دخول headers إلى وضع التثبيت والخروج منه. ستؤدي إضافة عقدتين (المعروفة أيضًا باسم "عناوين الإرسال") في كل قسم لاصق، إحداهما في أعلى الصفحة والأخرى في أسفلها، كنقاط طريق لمعرفة موضع التمرير. عند دخول هذه العلامات إلى الحاوية وخروجها منها، يتغير مستوى الرؤية ويطلق مراقب التقاطع رد اتصال.

بدون عناصر حارة تظهر
العناصر الحراسة المخفية

نحتاج إلى مراسلَين لتغطية أربع حالات من التمرير للأعلى وللأسفل:

  1. التمرير لأسفل - يصبح العنوان ثابتًا عندما يتقاطع الجزء العلوي من الحاوية مع الجزء العلوي من الحاوية.
  2. التمرير لأسفل - يترك العنوان وضع التثبيت عند وصوله إلى أسفل القسم ويمر حرارته السفلي أعلى الحاوية.
  3. التمرير لأعلى - يترك العنوان وضع التثبيت عند تمرير الجزء العلوي من الشاشة إلى العرض من الأعلى.
  4. التمرير لأعلى - يصبح العنوان ثابتًا عندما يتقاطع العنوان السفلي مع العرض من الأعلى.

من المفيد الاطّلاع على تسجيل رقمي للشاشة من 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 التي تم تطويرها على مر السنين. تبين أن الإجابة هي "نعم" و"لا"، فسمات واجهة برمجة التطبيقات IntersectionObserver الدلالية تجعل من الصعب استخدامها في كل أنواع المحتوى. ولكن كما أوضحت هنا، يمكنك استخدامها لبعض التقنيات الشيقة.

هل هناك طريقة أخرى لاكتشاف تغييرات النمط؟

ليس فعلاً. ما نحتاجه كان طريقة لملاحظة تغييرات النمط على عنصر DOM. لسوء الحظ، لا يوجد شيء في واجهات برمجة التطبيقات للنظام الأساسي على الويب تسمح لك بمشاهدة تغييرات النمط.

سيكون MutationObserver هو الخيار الأول المنطقي، ولكن هذا لا يصلح في معظم الحالات. على سبيل المثال، في الإصدار التجريبي، سنتلقّى استدعاء عند إضافة الفئة sticky إلى عنصر ما، وليس عند تغيير نمط العنصر المحتسب. تذكَّر أنّه سبق أن تم تضمين الفئة sticky عند تحميل الصفحة.

في المستقبل، قد تكون إضافة "Style Mutation Monitorer" لأداة مراقبة التبديلات مفيدة لرصد التغييرات التي تطرأ على الأنماط المحسوبة لأحد العناصر. position: sticky.