بناء رأس ديناميكي مع مراقب التقاطع
نشرت: 2022-03-10واجهة برمجة تطبيقات Intersection Observer هي واجهة برمجة تطبيقات JavaScript تتيح لنا مراقبة عنصر واكتشاف الوقت الذي يمر فيه نقطة محددة في حاوية تمرير - غالبًا (ولكن ليس دائمًا) منفذ العرض - مما يؤدي إلى تشغيل وظيفة رد الاتصال.
يمكن اعتبار برنامج Intersection Observer أكثر فاعلية من الاستماع لأحداث التمرير في السلسلة الرئيسية ، لأنه غير متزامن ، ولن يتم تنشيط رد الاتصال إلا عندما يلبي العنصر الذي نلاحظه الحد المحدد ، بدلاً من ذلك في كل مرة يتم فيها تحديث موضع التمرير. في هذه المقالة ، سنستعرض مثالاً عن كيفية استخدام Intersection Observer لبناء مكون رأس ثابت يتغير عندما يتقاطع مع أقسام مختلفة من صفحة الويب.
الاستخدام الأساسي
لاستخدام Intersection Observer ، نحتاج أولاً إلى إنشاء مراقب جديد ، والذي يأخذ معلمتين: كائن بخيارات المراقب ، ووظيفة رد الاتصال التي نريد تنفيذها عندما يتقاطع العنصر الذي نلاحظه (المعروف باسم هدف المراقب) مع الجذر (حاوية التمرير ، والتي يجب أن تكون أصل العنصر الهدف).
const options = { root: document.querySelector('[data-scroll-root]'), rootMargin: '0px', threshold: 1.0 } const callback = (entries, observer) => { entries.forEach((entry) => console.log(entry)) } const observer = new IntersectionObserver(callback, options)
عندما أنشأنا مراقبنا ، نحتاج بعد ذلك إلى توجيهه لمشاهدة عنصر مستهدف:
const targetEl = document.querySelector('[data-target]') observer.observe(targetEl)
يمكن حذف أي من قيم الخيارات ، حيث ستعود إلى قيمها الافتراضية:
const options = { rootMargin: '0px', threshold: 1.0 }
إذا لم يتم تحديد جذر ، فسيتم تصنيفه على أنه منفذ عرض المتصفح. يوضح مثال الكود أعلاه القيم الافتراضية لكل من rootMargin
threshold
. قد يكون من الصعب تصور ذلك ، لذا يجدر التوضيح:
rootMargin
تشبه قيمة rootMargin
إضافة هوامش CSS إلى عنصر الجذر - تمامًا مثل الهوامش ، يمكن أن تأخذ قيمًا متعددة ، بما في ذلك القيم السالبة. سيتم اعتبار العنصر الهدف متقاطعًا بالنسبة للهوامش.
وهذا يعني أنه يمكن تصنيف العنصر تقنيًا على أنه "متقاطع" حتى عندما يكون خارج نطاق العرض (إذا كان جذر التمرير هو منفذ العرض).
rootMargin
افتراضيًا على 0px
، ولكن يمكن أن يأخذ سلسلة تتكون من قيم متعددة ، تمامًا مثل استخدام خاصية margin
في CSS.
threshold
يمكن أن تتكون threshold
من قيمة واحدة أو مجموعة من القيم بين 0 و 1. وهي تمثل نسبة العنصر التي يجب أن تكون ضمن حدود الجذر حتى يتم اعتبارها متقاطعة . باستخدام القيمة الافتراضية 1 ، سيتم إطلاق رد الاتصال عندما يكون 100٪ من العنصر الهدف مرئيًا داخل الجذر.
ليس من السهل دائمًا تصور متى يتم تصنيف عنصر على أنه مرئي باستخدام هذه الخيارات. لقد صممت أداة صغيرة للمساعدة في السيطرة على Intersection Observer.
إنشاء الرأس
الآن بعد أن فهمنا المبادئ الأساسية ، فلنبدأ في بناء رأسنا الديناميكي. سنبدأ بصفحة ويب مقسمة إلى أقسام. تُظهر هذه الصورة التخطيط الكامل للصفحة التي سنبنيها:
لقد قمت بتضمين عرض توضيحي في نهاية هذه المقالة ، لذا لا تتردد في الانتقال إليه مباشرة إذا كنت حريصًا على إلغاء اختيار الكود. (يوجد أيضًا مستودع جيثب.)
يبلغ الحد الأدنى لارتفاع كل قسم 100vh
في الساعة (على الرغم من أنه قد يكون أطول ، حسب المحتوى). تم إصلاح العنوان الخاص بنا في الجزء العلوي من الصفحة ويظل في مكانه أثناء قيام المستخدم بالتمرير (باستخدام position: fixed
). تحتوي الأقسام على خلفيات ملونة مختلفة ، وعندما تلتقي بالرأس ، تتغير ألوان الرأس لتكمل ألوان القسم. هناك أيضًا علامة لإظهار القسم الحالي الذي يوجد فيه المستخدم ، والذي ينزلق عند وصول القسم التالي. لتسهيل الوصول مباشرة إلى الكود ذي الصلة ، قمت بإعداد عرض توضيحي بسيط مع نقطة البداية (قبل أن نبدأ في استخدام واجهة برمجة تطبيقات Intersection Observer) ، في حالة رغبتك في المتابعة.
وضع علامة على
سنبدأ بـ HTML لرأسنا. سيكون هذا رأسًا بسيطًا إلى حد ما مع رابط الصفحة الرئيسية والتنقل ، ولا شيء رائع بشكل خاص ، لكننا سنستخدم سمتين من سمات البيانات: data-header
للرأس نفسه (حتى نتمكن من استهداف العنصر باستخدام JS) ، وثلاثة روابط ربط مع data-link
السمة ، والتي ستمرر المستخدم إلى القسم ذي الصلة عند النقر عليه:
<header data-header> <nav class="header__nav"> <div class="header__left-content"> <a href="#0">Home</a> </div> <ul class="header__list"> <li> <a href="#about-us" data-link>About us</a> </li> <li> <a href="#flavours" data-link>The flavours</a> </li> <li> <a href="#get-in-touch" data-link>Get in touch</a> </li> </ul> </nav> </header>
بعد ذلك ، HTML لبقية صفحتنا ، والتي تم تقسيمها إلى أقسام. للإيجاز ، لقد قمت فقط بتضمين الأجزاء ذات الصلة بالمقال ، ولكن تم تضمين الترميز الكامل في العرض التوضيحي. يتضمن كل قسم سمة بيانات تحدد اسم لون الخلفية ، id
يتوافق مع أحد روابط الارتساء في الرأس:
<main> <section data-section="raspberry"> <!--Section content--> </section> <section data-section="mint"> <!--Section content--> </section> <section data-section="vanilla"> <!--Section content--> </section> <section data-section="chocolate"> <!--Section content--> </section> </main>
سنضع رأسنا مع CSS بحيث يظل ثابتًا في الجزء العلوي من الصفحة أثناء قيام المستخدم بالتمرير:
header { position: fixed; width: 100%; }
سنمنح أقسامنا أيضًا حدًا أدنى للارتفاع ، ونركز المحتوى. (هذا الرمز ليس ضروريًا لكي يعمل مراقب التقاطع ، إنه فقط للتصميم).
section { padding: 5rem 0; min-height: 100vh; display: flex; justify-content: center; align-items: center; }
تحذير iframe
أثناء إنشاء هذا العرض التوضيحي لـ Codepen ، واجهت مشكلة محيرة حيث كان رمز Intersection Observer الذي كان يجب أن يعمل بشكل مثالي يفشل في إطلاق رد الاتصال عند النقطة الصحيحة من التقاطع ولكن بدلاً من ذلك يتم إطلاقه عندما يتقاطع العنصر الهدف مع حافة منفذ العرض. بعد قليل من الحيرة ، أدركت أن هذا يرجع إلى أنه في Codepen يتم تحميل المحتوى داخل إطار iframe ، والذي يتم التعامل معه بشكل مختلف. (راجع قسم مستندات MDN حول Clipping ومستطيل التقاطع للحصول على التفاصيل الكاملة.)
كحل بديل ، في العرض التوضيحي ، يمكننا تغليف الترميز الخاص بنا في عنصر آخر ، والذي سيكون بمثابة حاوية التمرير - الجذر في خيارات IO - بدلاً من إطار عرض المتصفح ، كما قد نتوقع:
<div class="scroller" data-scroller> <header data-header> <!--Header content--> </header> <main> <!--Sections--> </main> </div>
إذا كنت تريد معرفة كيفية استخدام منفذ العرض كجذر بدلاً من ذلك لنفس العرض التوضيحي ، فسيتم تضمين ذلك في مستودع Github.
CSS
سنحدد في CSS بعض الخصائص المخصصة للألوان التي نستخدمها. سنقوم أيضًا بتحديد خاصيتين مخصصتين إضافيتين لنص الرأس وألوان الخلفية ، وسنقوم بتعيين بعض القيم الأولية. (سنقوم بتحديث هاتين الخاصيتين المخصصتين للأقسام المختلفة لاحقًا.)
:root { --mint: #5ae8d5; --chocolate: #573e31; --raspberry: #f2308e; --vanilla: #faf2c8; --headerText: var(--vanilla); --headerBg: var(--raspberry); }
سنستخدم هذه الخصائص المخصصة في رأسنا:
header { background-color: var(--headerBg); color: var(--headerText); }
سنقوم أيضًا بتعيين الألوان لأقسامنا المختلفة. أنا أستخدم سمات البيانات كمحددات ، ولكن يمكنك بسهولة استخدام فئة إذا كنت تفضل ذلك.
[data-section="raspberry"] { background-color: var(--raspberry); color: var(--vanilla); } [data-section="mint"] { background-color: var(--mint); color: var(--chocolate); } [data-section="vanilla"] { background-color: var(--vanilla); color: var(--chocolate); } [data-section="chocolate"] { background-color: var(--chocolate); color: var(--vanilla); }
يمكننا أيضًا تعيين بعض الأنماط لعنواننا عند عرض كل قسم:
/* Header */ [data-theme="raspberry"] { --headerText: var(--raspberry); --headerBg: var(--vanilla); } [data-theme="mint"] { --headerText: var(--mint); --headerBg: var(--chocolate); } [data-theme="chocolate"] { --headerText: var(--chocolate); --headerBg: var(--vanilla); }
هناك حالة أقوى لاستخدام سمات البيانات هنا لأننا سنقوم بتبديل سمة سمة data-theme
الخاصة بالرأس عند كل تقاطع.
إنشاء المراقب
الآن بعد أن أصبح لدينا HTML و CSS الأساسيان لإعداد صفحتنا ، يمكننا إنشاء مراقب لمشاهدة كل قسم من أقسامنا. نرغب في تشغيل رد اتصال عندما يتلامس قسم مع الجزء السفلي من الرأس أثناء قيامنا بالتمرير لأسفل الصفحة. هذا يعني أننا بحاجة إلى تعيين هامش جذر سلبي يتوافق مع ارتفاع الرأس.
const header = document.querySelector('[data-header]') const sections = [...document.querySelectorAll('[data-section]')] const scrollRoot = document.querySelector('[data-scroller]') const options = { root: scrollRoot, rootMargin: `${header.offsetHeight * -1}px`, threshold: 0 }
نحن نضع حدًا للقيمة 0 ، حيث نريد أن يتم إطلاقه إذا كان أي جزء من القسم يتقاطع مع هامش الجذر.
بادئ ذي بدء ، سننشئ رد اتصال لتغيير قيمة سمة data-theme
للرأس. (يعد هذا أكثر وضوحًا من إضافة الفئات وإزالتها ، خاصةً عندما يتم تطبيق فئات أخرى على عنصر الرأس الخاص بنا.)
/* The callback that will fire on intersection */ const onIntersect = (entries) => { entries.forEach((entry) => { const theme = entry.target.dataset.section header.setAttribute('data-theme', theme) }) }
ثم سننشئ المراقب ليراقب الأقسام المتقاطعة:
/* Create the observer */ const observer = new IntersectionObserver(onIntersect, options) /* Set our observer to observe each section */ sections.forEach((section) => { observer.observe(section) })
الآن يجب أن نرى ألوان الرأس يتم تحديثها عندما يلتقي كل قسم بالرأس.
ومع ذلك ، قد تلاحظ أن الألوان لا يتم تحديثها بشكل صحيح أثناء قيامنا بالتمرير لأسفل. في الواقع ، يتم تحديث العنوان بألوان القسم السابق في كل مرة! من ناحية أخرى ، فإن التمرير لأعلى يعمل بشكل مثالي. نحتاج إلى تحديد اتجاه التمرير وتغيير السلوك وفقًا لذلك.
البحث عن اتجاه التمرير
سنقوم بتعيين متغير في JS الخاص بنا لاتجاه التمرير ، بقيمة أولية 'up'
، ومتغير آخر لآخر موضع تمرير معروف ( prevYPosition
). بعد ذلك ، ضمن رد الاتصال ، إذا كان موضع التمرير أكبر من القيمة السابقة ، فيمكننا تعيين قيمة direction
على أنها 'down'
أو 'up'
إذا كان العكس صحيحًا.
let direction = 'up' let prevYPosition = 0 const setScrollDirection = () => { if (scrollRoot.scrollTop > prevYPosition) { direction = 'down' } else { direction = 'up' } prevYPosition = scrollRoot.scrollTop } const onIntersect = (entries, observer) => { entries.forEach((entry) => { setScrollDirection() /* ... */ }) }
سننشئ أيضًا وظيفة جديدة لتحديث ألوان الرأس ، مروراً بالقسم الهدف كوسيطة:
const updateColors = (target) => { const theme = target.dataset.section header.setAttribute('data-theme', theme) } const onIntersect = (entries) => { entries.forEach((entry) => { setScrollDirection() updateColors(entry.target) }) }
حتى الآن لا ينبغي أن نرى أي تغيير في سلوك العنوان الخاص بنا. ولكن الآن بعد أن عرفنا اتجاه التمرير ، يمكننا تمرير هدف مختلف updateColors()
الخاصة بنا. إذا كان اتجاه التمرير لأعلى ، فسنستخدم هدف الإدخال. إذا كان معطلاً ، فسنستخدم القسم التالي (إذا كان هناك واحد).
const getTargetSection = (target) => { if (direction === 'up') return target if (target.nextElementSibling) { return target.nextElementSibling } else { return target } } const onIntersect = (entries) => { entries.forEach((entry) => { setScrollDirection() const target = getTargetSection(entry.target) updateColors(target) }) }
ومع ذلك ، هناك مشكلة أخرى: لن يتم تحديث العنوان فقط عندما يصل القسم إلى الرأس ، ولكن عندما يظهر العنصر التالي في الجزء السفلي من منفذ العرض. هذا لأن مراقبنا يطلق عملية رد النداء مرتين: مرة أثناء دخول العنصر ، ومرة أخرى أثناء مغادرته.
لتحديد ما إذا كان يجب تحديث الرأس ، يمكننا استخدام مفتاح isIntersecting
من كائن entry
. لنقم بإنشاء دالة أخرى لإرجاع قيمة منطقية لتحديد ما إذا كان يجب تحديث ألوان الرأس أم لا:
const shouldUpdate = (entry) => { if (direction === 'down' && !entry.isIntersecting) { return true } if (direction === 'up' && entry.isIntersecting) { return true } return false }
سنقوم بتحديث وظيفة onIntersect()
بنا وفقًا لذلك:
const onIntersect = (entries) => { entries.forEach((entry) => { setScrollDirection() /* Do nothing if no need to update */ if (!shouldUpdate(entry)) return const target = getTargetSection(entry.target) updateColors(target) }) }
الآن يجب تحديث ألواننا بشكل صحيح. يمكننا ضبط انتقال CSS ، بحيث يكون التأثير أجمل قليلاً:
header { transition: background-color 200ms, color 200ms; }
مضيفا العلامة الديناميكية
بعد ذلك ، سنضيف علامة إلى الرأس تُحدِّث موضعها أثناء قيامنا بالتمرير إلى الأقسام المختلفة. يمكننا استخدام عنصر زائف لهذا ، لذلك لا نحتاج إلى إضافة أي شيء إلى HTML الخاص بنا. سنمنحه بعض أنماط CSS البسيطة لوضعه في أعلى يسار الرأس ، ونعطيه لونًا للخلفية. نحن نستخدم currentColor
لهذا ، حيث سيأخذ قيمة لون نص الرأس:
header::after { content: ''; position: absolute; top: 0; left: 0; height: 0.4rem; background-color: currentColor; }
يمكننا استخدام خاصية مخصصة للعرض ، بقيمة افتراضية 0. سنستخدم أيضًا خاصية مخصصة لترجمة قيمة x. سنقوم بتعيين قيم هذه في وظيفة رد الاتصال الخاصة بنا أثناء قيام المستخدم بالتمرير.
header::after { content: ''; position: absolute; top: 0; left: 0; height: 0.4rem; width: var(--markerWidth, 0); background-color: currentColor; transform: translate3d(var(--markerLeft, 0), 0, 0); }
يمكننا الآن كتابة دالة من شأنها تحديث عرض وموضع العلامة عند نقطة التقاطع:
const updateMarker = (target) => { const id = target.id /* Do nothing if no target ID */ if (!id) return /* Find the corresponding nav link, or use the first one */ let link = headerLinks.find((el) => { return el.getAttribute('href') === `#${id}` }) link = link || headerLinks[0] /* Get the values and set the custom properties */ const distanceFromLeft = link.getBoundingClientRect().left header.style.setProperty('--markerWidth', `${link.clientWidth}px`) header.style.setProperty('--markerLeft', `${distanceFromLeft}px`) }
يمكننا استدعاء الوظيفة في نفس الوقت الذي نقوم فيه بتحديث الألوان:
const onIntersect = (entries) => { entries.forEach((entry) => { setScrollDirection() if (!shouldUpdate(entry)) return const target = getTargetSection(entry.target) updateColors(target) updateMarker(target) }) }
سنحتاج أيضًا إلى تعيين موضع مبدئي للعلامة ، بحيث لا تظهر من العدم. عندما يتم تحميل المستند ، سنقوم باستدعاء وظيفة updateMarker()
، باستخدام القسم الأول كهدف:
document.addEventListener('readystatechange', e => { if (e.target.readyState === 'complete') { updateMarker(sections[0]) } })
أخيرًا ، دعنا نضيف انتقال CSS بحيث تنزلق العلامة عبر الرأس من رابط إلى التالي. أثناء قيامنا بنقل خاصية width
، يمكننا استخدام will-change
لتمكين المتصفح من إجراء التحسينات.
header::after { transition: transform 250ms, width 200ms, background-color 200ms; will-change: width; }
التمرير السلس
للحصول على اللمسة الأخيرة ، سيكون من الجيد ، عندما ينقر المستخدم على رابط ، أن يتم تمريره بسلاسة إلى أسفل الصفحة ، بدلاً من القفز إلى القسم. في هذه الأيام يمكننا القيام بذلك بشكل صحيح في CSS الخاص بنا ، دون الحاجة إلى JS! للحصول على تجربة يمكن الوصول إليها بشكل أكبر ، من الجيد احترام تفضيلات حركة المستخدم من خلال تنفيذ التمرير السلس فقط إذا لم يحددوا تفضيلًا لتقليل الحركة في إعدادات النظام الخاصة بهم:
@media (prefers-reduced-motion: no-preference) { .scroller { scroll-behavior: smooth; } }
عرض نهائي
ينتج عن وضع جميع الخطوات المذكورة أعلاه معًا العرض التوضيحي الكامل.
دعم المتصفح
يتم دعم Intersection Observer على نطاق واسع في المتصفحات الحديثة. عند الضرورة ، يمكن تعبئتها للمتصفحات القديمة - لكني أفضل اتباع نهج التحسين التدريجي حيثما أمكن ذلك. في حالة العنوان الخاص بنا ، لن يكون ضارًا بشكل كبير بتجربة المستخدم لتوفير إصدار بسيط غير متغير للمتصفحات غير الداعمة.
لاكتشاف ما إذا كان برنامج Intersection Observer مدعومًا أم لا ، يمكننا استخدام ما يلي:
if ('IntersectionObserver' in window && 'IntersectionObserverEntry' in window && 'intersectionRatio' in window.IntersectionObserverEntry.prototype) { /* Code to execute if IO is supported */ } else { /* Code to execute if not supported */ }
موارد
اقرأ المزيد عن مراقب التقاطع:
- وثائق مستفيضة ، مع بعض الأمثلة العملية من MDN
- أداة Visualiser مراقب التقاطع
- رؤية عنصر التوقيت مع واجهة برمجة تطبيقات Intersection Observer - برنامج تعليمي آخر من MDN ، يبحث في كيفية استخدام IO لتتبع رؤية الإعلان
- يغطي هذا المقال بقلم Denys Mishunov بعض الاستخدامات الأخرى لعمليات الإدخال والإخراج ، بما في ذلك أصول التحميل البطيء. على الرغم من أن هذا أقل أهمية الآن (بفضل سمة
loading
) ، لا يزال هناك الكثير لتعلمه هنا.