حدث لموضع 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 ثابتًا. لنسميها الحدث 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 تحديد وقت دخول الرؤوس إلى وضع التثبيت والخروج منها إضافة عقدتين (يُعرفون أيضًا باسم "الحراس") في كل قسم ملصق إعلاني، واحد في الأعلى وواحد في الأعلى في الجزء السفلي، سيكون بمثابة نقاط طريق لمعرفة موضع التمرير. نظرًا لأن ذلك دخول العلامات إلى الحاوية والخروج منها، ويتغير مستوى رؤيتها تنشِّط أداة مراقبة التقاطع معاودة الاتصال.

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

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

  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 عند تحميل الصفحة.

في المستقبل، "تتبُّع تغيُّر النمط" الامتداد إلى مراقبي التغيُّر لملاحظة التغييرات في والأنماط المحسوبة للعنصر. position: sticky