सीएसएस की पोज़िशन के लिए इवेंट:स्टिकी

एरिक बिडेलमैन

बहुत ज़्यादा शब्द हैं, पढ़ा नहीं गया

अहम जानकारी: ऐसा हो सकता है कि आपको अपने अगले ऐप्लिकेशन में scroll इवेंट की ज़रूरत न पड़े. IntersectionObserver का इस्तेमाल करके, मैं दिखाता हूं कि position:sticky एलिमेंट के ठीक हो जाने या उनके बने रहने पर, कस्टम इवेंट को कैसे चालू किया जा सकता है. यह सब स्क्रोल लिसनर के इस्तेमाल के बिना किया गया है. यहां तक कि इसे साबित करने के लिए एक शानदार डेमो भी उपलब्ध है:

डेमो देखें | सोर्स

पेश है sticky-change इवेंट

सीएसएस स्टिकी पोज़िशन का इस्तेमाल करने की एक मुख्य सीमा यह है कि यह प्रॉपर्टी के चालू होने पर, प्लैटफ़ॉर्म का सिग्नल नहीं देता. दूसरे शब्दों में कहें, तो यह पता करने में कोई इवेंट नहीं है कि कोई एलिमेंट कब स्टिकी हो जाता है या कब स्टिकी होना बंद हो जाता है.

नीचे दिया गया उदाहरण लें, जिसमें पैरंट कंटेनर के ऊपरी हिस्से से <div class="sticky"> की लंबाई 10 पिक्सल की होनी चाहिए:

.sticky {
  position: sticky;
  top: 10px;
}

क्या यह अच्छा नहीं होगा कि ब्राउज़र बता दे कि एलिमेंट कब उस मार्क पर हिट करते हैं? साफ़ तौर पर सिर्फ़ मैं ही नहीं हूं जिसे ऐसा लगता है. position:sticky का सिग्नल कई इस्तेमाल के उदाहरणों से अनलॉक हो सकता है:

  1. बैनर के चिपकने पर उस पर ड्रॉप शैडो लगाएं.
  2. जब लोग आपका कॉन्टेंट पढ़ते हैं, तब उनकी प्रोग्रेस जानने के लिए, आंकड़ों के हिट रिकॉर्ड करें.
  3. जब उपयोगकर्ता पेज स्क्रोल करता है, तब फ़्लोटिंग 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;
});

डेमो के ठीक हो जाने पर, इस इवेंट का इस्तेमाल ड्रॉप शैडो को हेडर बनाने के लिए किया जाता है. इससे पेज के सबसे ऊपर मौजूद नया टाइटल भी अपडेट हो जाता है.

डेमो में, इफ़ेक्ट, स्क्रोल इवेंट के बिना लागू किए जाते हैं.

क्या आपको बिना स्क्रोल इवेंट के इफ़ेक्ट स्क्रोल करने हैं?

पेज का स्ट्रक्चर.
पेज का स्ट्रक्चर.

चलिए, कुछ शब्दावली बना लेते हैं, ताकि मैं बाकी पोस्ट में इन नामों का उल्लेख कर सकूं:

  1. स्क्रोल करने वाला कंटेनर - कॉन्टेंट एरिया (दिखने वाला व्यूपोर्ट) जिसमें "ब्लॉग पोस्ट" की सूची होती है.
  2. हेडर - हर सेक्शन में मौजूद नीले रंग का टाइटल, जिसमें position:sticky मौजूद हों.
  3. स्टिकी सेक्शन - हर कॉन्टेंट सेक्शन. स्टिकी हेडर के नीचे स्क्रोल करने वाला टेक्स्ट.
  4. "स्टिकी मोड" - जब position:sticky, एलिमेंट पर लागू होता है.

यह जानने के लिए कि कौनसा हेडर "स्टिकी मोड" में चला जाता है, हमें स्क्रोल करने वाले कंटेनर के स्क्रोल ऑफ़सेट तय करने का कोई तरीका चाहिए. इससे हमें अभी दिख रहे हेडर की गिनती करने का तरीका मिल जाएगा. हालांकि, scroll इवेंट के बिना ऐसा करना बहुत मुश्किल होता है :) दूसरी समस्या यह है कि position:sticky, एलिमेंट के ठीक हो जाने पर उसे लेआउट से हटा देता है.

इसलिए, स्क्रोल इवेंट के बिना, हम हेडर पर लेआउट से जुड़ी कैलकुलेशन नहीं कर सकते.

स्क्रोल की जगह तय करने के लिए डंबी DOM जोड़ें

अब हम scroll इवेंट के बजाय, IntersectionObserver का इस्तेमाल करेंगे. इससे यह तय किया जा सकेगा कि headers स्टिकी मोड में कब आते और कब बंद होते हैं. हर स्टिकी सेक्शन में दो नोड जोड़ने पर, स्क्रोल की पोज़िशन का पता लगाने के लिए, सबसे ऊपर और एक सबसे नीचे नोड जोड़े जाएंगे. जब ये मार्कर कंटेनर में आते हैं और उससे बाहर निकलते हैं, तो उनकी दिखने की सेटिंग बदल जाती है और इंटरसेक्शन ऑब्ज़र्वर एक कॉलबैक चालू कर देता है.

सेंटिनल एलिमेंट दिखाए बिना
छिपे हुए सेंसर एलिमेंट.

ऊपर और नीचे स्क्रोल करने के चार मामलों को कवर करने के लिए, हमें दो सेंसर की ज़रूरत है:

  1. नीचे की ओर स्क्रोल करना - जब हेडर सबसे ऊपर की ओर जाता है, तो वह चिपचिपा हो जाता है.
  2. नीचे की ओर स्क्रोल करना - हेडर सेक्शन में सबसे नीचे पहुंचने पर, स्टिकी मोड से बाहर निकल जाता है और उसका निचला सेंटीनल कंटेनर के ऊपर से गुज़रता है.
  3. ऊपर की ओर स्क्रोल करने से - हेडर जब ऊपर से देखने के लिए स्क्रोल किया जाता है, तब स्टिकी मोड छोड़ दिया जाता है.
  4. ऊपर की ओर स्क्रोल करना - हेडर जब ऊपर से देखने के लिए पीछे की ओर जाता है, तो वह चिपचिपा हो जाता है.

स्क्रीनकास्ट को एक से चार के क्रम में देखना फ़ायदेमंद होता है:

इंटरसेक्शन ऑब्ज़र्वर तब कॉलबैक फ़ायर करते हैं, जब भेजने वाले स्क्रोल कंटेनर में जाते हैं या उससे बाहर निकलते हैं.

सीएसएस

संवेदनशील जानकारी को हर सेक्शन के सबसे ऊपर और सबसे नीचे रखा जाता है. .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 इवेंट का इस्तेमाल किए बिना स्क्रोल इफ़ेक्ट जोड़े जाते हैं, तब हमने कस्टम इवेंट बनाया है.

डेमो देखें | सोर्स

नतीजा

मैं अक्सर सोचती थी कि पिछले कई सालों में बने scroll इवेंट पर आधारित यूज़र इंटरफ़ेस (यूआई) पैटर्न को बदलने के लिए, IntersectionObserver एक मददगार टूल होगा या नहीं. पता चला कि इसका जवाब हां है और नहीं. IntersectionObserver API के सिमेंटिक्स की वजह से, हर काम में इसका इस्तेमाल करना मुश्किल हो जाता है. लेकिन जैसा कि मैंने यहां दिखाया है, कुछ दिलचस्प तकनीकों के लिए इसका इस्तेमाल किया जा सकता है.

क्या स्टाइल में बदलावों का पता लगाने का कोई और तरीका है?

दरअसल ऐसा नहीं है. हमें किसी DOM एलिमेंट की स्टाइल में हुए बदलावों पर नज़र रखने का तरीका चाहिए था. माफ़ करें, वेब प्लैटफ़ॉर्म के एपीआई में ऐसा कुछ नहीं है जिसकी मदद से, स्टाइल में किए गए बदलाव देखे जा सकें.

MutationObserver सबसे पहली पसंद होगी. हालांकि, यह ज़्यादातर मामलों में काम नहीं करता. उदाहरण के लिए, डेमो में हमें किसी एलिमेंट में sticky क्लास जोड़े जाने पर कॉलबैक मिलेगा. हालांकि, एलिमेंट के कंप्यूट किए गए स्टाइल में बदलाव होने पर, हमें कॉलबैक नहीं मिलेगा. याद रखें कि पेज लोड होने पर, sticky क्लास का एलान पहले ही कर दिया गया था.

आने वाले समय में, "स्टाइल म्यूटेशन ऑब्ज़र्वर" म्यूटेशन ऑब्ज़र्वर का एक्सटेंशन, किसी एलिमेंट के कंप्यूट किए गए स्टाइल में हुए बदलावों पर नज़र रखने में मददगार हो सकता है. position: sticky.