CSS অবস্থানের জন্য একটি ইভেন্ট: স্টিকি

টিএল; ডিআর

এখানে একটি গোপন বিষয়: আপনার পরবর্তী অ্যাপে scroll ইভেন্টের প্রয়োজন নাও হতে পারে। একটি IntersectionObserver ব্যবহার করে, আমি দেখাই কিভাবে আপনি একটি কাস্টম ইভেন্ট ফায়ার করতে পারেন যখন position:sticky উপাদানগুলি স্থির হয়ে যায় বা যখন তারা আটকে যাওয়া বন্ধ করে। স্ক্রোল শ্রোতাদের ব্যবহার ছাড়াই সব। এমনকি এটি প্রমাণ করার জন্য একটি দুর্দান্ত ডেমো রয়েছে:

ডেমো দেখুন | উৎস

sticky-change ইভেন্ট প্রবর্তন

CSS স্টিকি পজিশন ব্যবহার করার ব্যবহারিক সীমাবদ্ধতাগুলির মধ্যে একটি হল যে এটি প্রপার্টি সক্রিয় হলে তা জানার জন্য একটি প্ল্যাটফর্ম সংকেত প্রদান করে না । অন্য কথায়, একটি উপাদান কখন স্টিকি হয়ে যায় বা কখন এটি আঠালো হওয়া বন্ধ করে তা জানার কোনো ঘটনা নেই।

নিম্নলিখিত উদাহরণটি নিন, যা একটি <div class="sticky"> 10px এর মূল কন্টেইনারের উপরে থেকে ঠিক করে:

.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 ব্যবহার করতে যাচ্ছি। প্রতিটি স্টিকি বিভাগে দুটি নোড (ওরফে সেন্টিনেল) যোগ করা, একটি উপরে এবং একটি নীচে, স্ক্রোল অবস্থান নির্ধারণের জন্য পথপয়েন্ট হিসাবে কাজ করবে। এই মার্কারগুলি কন্টেইনারে প্রবেশ এবং ছেড়ে যাওয়ার সাথে সাথে তাদের দৃশ্যমানতা পরিবর্তন হয় এবং ইন্টারসেকশন অবজারভার একটি কলব্যাক ফায়ার করে।

সেন্টিনেল উপাদান দেখানো ছাড়া
লুকানো সেন্টিনেল উপাদান.

উপরে এবং নীচে স্ক্রোল করার চারটি কেস কভার করার জন্য আমাদের দুটি সেন্টিনেলের প্রয়োজন:

  1. নীচে স্ক্রোল করা - শিরোনামটি আঠালো হয়ে যায় যখন এর শীর্ষ সেন্টিনেলটি পাত্রের শীর্ষে অতিক্রম করে।
  2. নীচে স্ক্রোল করা হচ্ছে - হেডারটি স্টিকি মোড ছেড়ে যায় কারণ এটি বিভাগের নীচে পৌঁছায় এবং এর নীচের সেন্টিনেলটি পাত্রের শীর্ষে অতিক্রম করে।
  3. উপরে স্ক্রোল করা হচ্ছে - শিরোনামটি স্টিকি মোড ছেড়ে দেয় যখন এর শীর্ষ সেন্টিনেল উপরের দিক থেকে দৃশ্যে ফিরে আসে।
  4. উপরে স্ক্রোল করা - শিরোনামটি চটচটে হয়ে যায় কারণ এর নীচের সেন্টিনেলটি উপরের দিক থেকে দৃশ্যে ফিরে আসে।

1-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 ইভেন্ট-ভিত্তিক UI প্যাটার্নগুলির প্রতিস্থাপনের জন্য একটি সহায়ক হাতিয়ার হবে। দেখা যাচ্ছে উত্তর হল হ্যাঁ এবং না। IntersectionObserver API-এর শব্দার্থবিদ্যা সবকিছুর জন্য ব্যবহার করা কঠিন করে তোলে। কিন্তু আমি এখানে দেখিয়েছি, আপনি কিছু আকর্ষণীয় কৌশলের জন্য এটি ব্যবহার করতে পারেন।

শৈলী পরিবর্তন সনাক্ত করার আরেকটি উপায়?

আসলে তা না. আমাদের যা প্রয়োজন তা হল একটি DOM উপাদানে শৈলী পরিবর্তনগুলি পর্যবেক্ষণ করার একটি উপায়। দুর্ভাগ্যবশত, ওয়েব প্ল্যাটফর্ম API-এ এমন কিছুই নেই যা আপনাকে শৈলী পরিবর্তনগুলি দেখতে দেয়।

একটি MutationObserver একটি যৌক্তিক প্রথম পছন্দ হবে কিন্তু এটি বেশিরভাগ ক্ষেত্রে কাজ করে না। উদাহরণস্বরূপ, ডেমোতে, আমরা একটি কলব্যাক পাব যখন কোনো উপাদানে sticky ক্লাস যোগ করা হয়, কিন্তু যখন উপাদানটির গণনা করা শৈলী পরিবর্তন হয় তখন নয়। মনে রাখবেন যে পৃষ্ঠা লোডের উপর ইতিমধ্যেই sticky ক্লাস ঘোষণা করা হয়েছে।

ভবিষ্যতে, মিউটেশন পর্যবেক্ষকদের জন্য একটি " স্টাইল মিউটেশন অবজারভার " এক্সটেনশন একটি উপাদানের গণনা করা শৈলীতে পরিবর্তনগুলি পর্যবেক্ষণ করতে কার্যকর হতে পারে। position: sticky