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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  1. नीचे की ओर स्क्रोल करना - जब हेडर का ऊपरी हिस्सा कंटेनर के ऊपरी हिस्से से पार हो जाता है, तो हेडर स्टिकी हो जाता है.
  2. नीचे स्क्रोल करना - जब हेडर, सेक्शन के सबसे निचले हिस्से तक पहुंचता है, तब हेडर उससे स्टिकी मोड में चला जाता है और उसका नीचे का सेंटिनल, कंटेनर के ऊपरी हिस्से को पार कर जाता है.
  3. ऊपर की ओर स्क्रोल करना - header जब हेडर का सबसे ऊपर वाला हिस्सा, स्टिकी मोड से बाहर आ जाता है, तब वह ऊपर से व्यू की तरफ़ स्क्रोल करता है.
  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 के सिमैंटिक की वजह से हर चीज़ के लिए इसका इस्तेमाल करना मुश्किल हो जाता है. लेकिन जैसा कि यहां दिखाया गया है, आप इसका इस्तेमाल कुछ दिलचस्प तकनीकों के लिए कर सकते हैं.

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

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

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

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