حدث لموضع CSS:Sticky

TL;DR

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

مشاهدة العرض التوضيحي | المصدر

لمحة عن حدث sticky-change

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

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

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

ألن يكون من الرائع أن يُعلمك المتصفّح عندما تصل العناصر إلى هذه العلامة؟ يبدو أنني لست الوحيد الذي يفكر في ذلك. يمكن أن تؤدي إشارة "position:sticky" إلى فتح عدد من حالات الاستخدام:

  1. طبِّق تظليلًا خلفيًا على البانر أثناء تثبيته.
  2. عندما يقرأ المستخدم المحتوى الخاص بك، سجِّل نتائج الإحصاءات لمعرفة مستوى تقدّمه.
  3. أثناء تنقّل المستخدم في الصفحة، عدِّل التطبيق المصغّر للجدول الزمني العائم إلى القسم الحالي.

استنادًا إلى حالات الاستخدام هذه، وضعنا هدفًا نهائيًا: إنشاء حدث يتم 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;
});

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

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

هل مطلوب تمرير التأثيرات بدون الانتقال إلى أحداث الانتقال؟

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

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

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

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

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

إضافة عنصر DOM وهمي لتحديد موضع الانتقال

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

بدون عرض عناصر المراقبة
عناصر الحارس المخفية

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

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

من المفيد مشاهدة تسجيل رقمي للشاشة من الخطوة 1 إلى الخطوة 4 بالترتيب التالي:

يطلق مراقبو التقاطع استدعاءات عندما يطلق الحراس الدخول إلى حاوية التمرير أو مغادرتها

خدمة مقارنة الأسعار (CSS)

يتم وضع نقاط التحكّم في أعلى وأسفل كل قسم. يظهر .sticky_sentinel--top في أعلى العنوان بينما يظهر .sticky_sentinel--bottom في أسفل القسم:

مستوى أدنى يتخطى الحد الأدنى المطلوب
موضع عنصرَي Sentinel العلوي والسفلي
: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 Observers التغييرات بشكل غير متزامن في تقاطع عنصر مستهدَف وإطار عرض المستند أو حاوية رئيسية. في حالتنا، نلاحظ وجود تقاطعات مع حاوية رئيسية.

الصلصة السحرية هي 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 Observer" إلى مراقبي الطفرات مفيدة لرصد التغييرات في أنماط العنصر المحسوبة. position: sticky