الملخّص
إليك سر: قد لا تحتاج إلى أحداث 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;
});
يستخدِم العرض التوضيحي هذا الحدث لتغطية الظل الخلفي عندما يتم إصلاحه. كما يحدّث العنوان الجديد في أعلى الصفحة.
هل تريد تمرير التأثيرات بدون عناصر الانتقال للأسفل؟
لنبعد بعض المصطلحات حتى أتمكن من الإشارة إلى هذه الأسماء في بقية المنشور:
- حاوية التمرير - منطقة المحتوى (إطار العرض المرئي) التي تتضمن قائمة "مشاركات المدونة".
- العناوين - عنوان أزرق في كل قسم يحتوي على
position:sticky
. - الأقسام الثابتة: لكل قسم من أقسام المحتوى هو النص الذي يتم تمريره أسفل العناوين اللاصقة.
- "وضع التثبيت" - عندما يتم تطبيق
position:sticky
على العنصر
لمعرفة العنوان الذي يدخل في "وضع التثبيت"، نحتاج إلى طريقة لتحديد
إزاحة التمرير في حاوية التمرير. وهذا من شأنه أن يمنحنا طريقة لحساب
العنوان المعروض حاليًا. في المقابل، يكون تنفيذ هذا الإجراء صعبًا بدون استخدام أحداث scroll
:) والمشكلة الأخرى هي أنّ position:sticky
يزيل العنصر من التنسيق عندما يتم إصلاحه.
لذلك بدون أحداث التمرير، فقدنا القدرة على إجراء العمليات الحسابية المرتبطة بالتنسيق على العناوين.
إضافة dumby DOM لتحديد موضع التمرير
بدلاً من أحداث scroll
، سنستخدم IntersectionObserver
لتحديد وقت دخول headers إلى وضع التثبيت والخروج منه. ستؤدي إضافة عقدتين
(المعروفة أيضًا باسم "عناوين الإرسال") في كل قسم لاصق، إحداهما في أعلى الصفحة والأخرى في أسفلها، كنقاط طريق لمعرفة موضع التمرير. عند دخول هذه العلامات إلى الحاوية وخروجها منها، يتغير مستوى الرؤية
ويطلق مراقب التقاطع رد اتصال.
نحتاج إلى مراسلَين لتغطية أربع حالات من التمرير للأعلى وللأسفل:
- التمرير لأسفل - يصبح العنوان ثابتًا عندما يتقاطع الجزء العلوي من الحاوية مع الجزء العلوي من الحاوية.
- التمرير لأسفل - يترك العنوان وضع التثبيت عند وصوله إلى أسفل القسم ويمر حرارته السفلي أعلى الحاوية.
- التمرير لأعلى - يترك العنوان وضع التثبيت عند تمرير الجزء العلوي من الشاشة إلى العرض من الأعلى.
- التمرير لأعلى - يصبح العنوان ثابتًا عندما يتقاطع العنوان السفلي مع العرض من الأعلى.
من المفيد الاطّلاع على تسجيل رقمي للشاشة من 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
.