TL;DR
إليك سر: قد لا تحتاج إلى حدث scroll
في تطبيقك التالي. استخدام
IntersectionObserver
,
أعرض كيفية تنشيط حدث مخصّص عندما تصبح عناصر position:sticky
ثابتة أو عند توقفها عن اللصق. كل ذلك بدون
استخدام أدوات معالجة التمرير. وهناك أيضًا عرض توضيحي رائع لإثبات ذلك:
التعريف بحدث sticky-change
يتمثل أحد القيود العملية لاستخدام الموضع الثابت في CSS في أنه لا يوفِّر إشارة للنظام الأساسي لمعرفة الوقت الذي يكون فيه الموقع نشطًا. بمعنى آخر، ليس هناك حدث يجب معرفته عندما يصبح العنصر ثابتًا أو عندما يتوقف عن التصاق.
لأخذ المثال التالي، الذي يثبت <div class="sticky">
10 بكسل من
أعلى الحاوية الرئيسية:
.sticky {
position: sticky;
top: 10px;
}
ألن يكون من اللطيف أن يحصل المتصفح على إشعار عند ضغط العناصر على هذه العلامة؟
يبدو أنني لست الوحيد
الذي يفكر في ذلك. يمكن أن تؤدي إشارة "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;
});
يستخدم العرض التوضيحي هذا الحدث على ظهور ظل خلفي عندما يتم إصلاحها. ويعمل أيضًا على تحديث عنوان جديد في أعلى الصفحة.
هل مطلوب تمرير التأثيرات بدون الانتقال إلى أحداث الانتقال؟
لنتخلص من بعض المصطلحات حتى أتمكن من الإشارة إلى هذه الأسماء في بقية المنشور:
- حاوية التمرير - منطقة المحتوى (إطار العرض المرئي) التي تحتوي على قائمة "مشاركات المدونة".
- الرؤوس - عنوان باللون الأزرق في كل قسم يحتوي على
position:sticky
. - الأقسام الثابتة - كل قسم من أقسام المحتوى النص الذي يتم تمريره تحت استخدام عناوين ثابتة.
- "وضع التثبيت" - عند تطبيق
position:sticky
على العنصر
لمعرفة العنوان الذي يدخل في "وضع التثبيت"، نحتاج إلى طريقة لتحديد
إزاحة التمرير في حاوية التمرير. هذا من شأنه أن يمنحنا طريقة
لحساب العنوان المعروض حاليًا. ومع ذلك، يصبح ذلك
الصعب القيام به بدون أحداث scroll
:) المشكلة الأخرى هي
تزيل السمة position:sticky
العنصر من التنسيق عندما يصبح ثابتًا.
وبدون أحداث التمرير، فقدنا إمكانية تنفيذ عمليات العمليات الحسابية على العناوين.
إضافة dumby DOM لتحديد موضع التمرير
بدلاً من أحداث scroll
، سنستخدم IntersectionObserver
تحديد وقت دخول الرؤوس إلى وضع التثبيت والخروج منها إضافة عقدتين
(يُعرفون أيضًا باسم "الحراس") في كل قسم ملصق إعلاني، واحد في الأعلى وواحد في الأعلى
في الجزء السفلي، سيكون بمثابة نقاط طريق لمعرفة موضع التمرير. نظرًا لأن ذلك
دخول العلامات إلى الحاوية والخروج منها، ويتغير مستوى رؤيتها
تنشِّط أداة مراقبة التقاطع معاودة الاتصال.
نحتاج إلى اثنين من الحراس لتغطية أربع حالات من الانتقال للأعلى وللأسفل:
- الانتقال للأسفل: يصبح العنوان ثابتًا عندما يمر أعلى مستوى. في الجزء العلوي من الحاوية.
- التمرير لأسفل: يترك العنوان وضع التثبيت عند وصوله إلى الجزء السفلي من القسم وحارسه السفلي عبر الجزء العلوي من الحاوية.
- الانتقال للأعلى: يترك العنوان وضع التثبيت عند تمرير الجزء العلوي من الصفحة. مرة أخرى إلى الرؤية من الأعلى.
- الانتقال للأعلى: يصبح العنوان ثابتًا عندما يمتد الجزء السفلي من الجهاز للخلف. في الرؤية من الأعلى.
من المفيد رؤية تسجيل رقمي للشاشة من 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