الملخّص
إليك سر: قد لا تحتاج إلى أحداث scroll
في تطبيقك بعد ذلك. باستخدام IntersectionObserver
،
أوضح كيفية تنشيط حدث مخصَّص عندما تصبح عناصر position:sticky
ثابتة أو عند توقفها عن اللصق. كل ذلك دون استخدام
مستمعي التمرير. وهناك أيضًا عرض توضيحي رائع لإثبات ذلك:
التعريف بحدث sticky-change
يتمثل أحد القيود العملية لاستخدام موضع تثبيت CSS في أنه لا يوفر إشارة نظام أساسي لمعرفة الوقت الذي يكون فيه الموقع نشطًا. بمعنى آخر، ليس هناك حدث لمعرفة متى يصبح العنصر لزجًا أو عندما يتوقف عن اللصق.
إليك المثال التالي الذي يثبت <div class="sticky">
10 بكسل من أعلى حاويتها الرئيسية:
.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;
});
يستخدم العرض التوضيحي هذا الحدث لإضافة عنوان لتظليل القطرات عندما يتم إصلاحها. كما أنه يقوم أيضًا بتحديث العنوان الجديد في أعلى الصفحة.
هل مطلوب تمرير التأثيرات بدون الانتقال إلى أحداث الانتقال؟
![بنية الصفحة.](https://developer.chrome.com/static/docs/css-ui/sticky-headers/image/structure-the-page-de19d5c111bd1.png?authuser=6&hl=ar)
دعونا نتخلص من بعض المصطلحات حتى أتمكن من الإشارة إلى هذه الأسماء في بقية المنشور:
- حاوية التمرير - منطقة المحتوى (إطار العرض المرئي) التي تتضمن قائمة "مشاركات المدونة".
- الرؤوس - عنوان باللون الأزرق في كل قسم يحتوي على
position:sticky
. - الأقسام الثابتة - كل قسم من أقسام المحتوى النص الذي يتم تمريره أسفل العناوين الملصقة.
- "وضع التثبيت" - عند تطبيق
position:sticky
على العنصر
لمعرفة العنوان الذي يدخل في "وضع التثبيت"، نحتاج إلى طريقة لتحديد إزاحة التمرير في حاوية التمرير. وسيمنحنا ذلك طريقة لحساب العنوان المعروض حاليًا. مع ذلك، سيكون من الصعب جدًا القيام به بدون أحداث scroll
:) المشكلة الأخرى هي أنّ position:sticky
تزيل العنصر من التنسيق عندما يتم إصلاحه.
لذلك بدون أحداث التمرير، فقدنا إمكانية إجراء العمليات الحسابية المتعلقة بالتنسيق في العناوين.
إضافة dumby DOM لتحديد موضع التمرير
وبدلاً من أحداث scroll
، سنستخدم IntersectionObserver
لتحديد وقت دخول headers إلى وضع التثبيت والخروج منها. ستؤدي إضافة عقدتين
(المعروفة أيضًا باسم "الحراس") في كل قسم ثابت، إحداهما في الجزء العلوي والأخرى في الجزء السفلي،
كنقاط طريق لمعرفة موضع التمرير. وعند دخول هذه العلامات إلى الحاوية والخروج منها، يتغير مستوى رؤيتها، وينشط "مراقب التقاطع" معاودة الاتصال.
![بدون عرض عناصر الحارس](https://developer.chrome.com/static/docs/css-ui/sticky-headers/image/without-sentinel-elements-f9529d3483c91.gif?authuser=6&hl=ar)
نحتاج إلى اثنين من الحراس لتغطية أربع حالات من الانتقال للأعلى وللأسفل:
- التمرير لأسفل: يصبح العنوان ثابتًا عندما يمر أعلى مستوى من الحاوية.
- الانتقال للأسفل: يؤدي الانتقال إلى الرأس إلى ترك وضع التثبيت عند وصوله إلى أسفل القسم وتقاطع عناصر التحكم السفلية مع الجزء العلوي من الحاوية.
- الانتقال للأعلى: يؤدي العنوان إلى ترك وضع التثبيت عندما يتم تمرير الجزء العلوي من الصفحة من الأعلى إلى الشاشة الرئيسية.
- الانتقال للأعلى: يصبح العنوان ثابتًا عندما يعود الجزء السفلي إلى مكان العرض من الأعلى.
من المفيد رؤية تسجيل رقمي للشاشة من 1 إلى 4 بترتيب حدوثه:
خدمة مقارنة الأسعار (CSS)
يتم وضع الحراس أعلى وأسفل كل قسم.
يوجد .sticky_sentinel--top
في الجزء العلوي من الرأس بينما
.sticky_sentinel--bottom
في الجزء السفلي من القسم:
![مستوى أدنى يتخطّى الحد الأدنى](https://developer.chrome.com/static/docs/css-ui/sticky-headers/image/bottom-sentinel-reaching-208a13f07bd7c.png?authuser=6&hl=ar)
: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 Observer" (مراقب تغيُّر النمط) مفيدة لرصد التغييرات التي تطرأ على الأنماط المحسوبة لأحد العناصر.
position: sticky
.