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

कम शब्दों में कहा जाए तो

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

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

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

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

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

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

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

  1. बैनर के चिपकने पर, उस पर ड्रॉप शैडो लागू करें.
  2. जब कोई उपयोगकर्ता आपका कॉन्टेंट पढ़ता है, तो उसकी प्रोग्रेस जानने के लिए Analytics हिट रिकॉर्ड करें.
  3. जब कोई उपयोगकर्ता पेज को स्क्रोल करता है, तो फ़्लोटिंग टीओसी विजेट को मौजूदा सेक्शन में अपडेट करें.

इस्तेमाल के इन उदाहरणों को ध्यान में रखते हुए, हमने एक मकसद तय किया है: ऐसा इवेंट बनाएं जो 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;
});

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

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

क्या स्क्रोल इवेंट के बिना स्क्रोल इफ़ेक्ट इस्तेमाल किए जा सकते हैं?

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

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

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

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

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

स्क्रोल की पोज़िशन तय करने के लिए, डंबी डीओएम जोड़ना

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

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

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

  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 इवेंट का इस्तेमाल किए बिना स्क्रोल इफ़ेक्ट जोड़े.

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

नतीजा

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

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

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

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

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