javascript

examples

examples.js
/**
 * 19.3 Intersection Observer - Examples
 *
 * Efficiently track element visibility in the viewport
 */

// ============================================
// BASIC INTERSECTION OBSERVER
// ============================================

/**
 * Simple visibility detection
 */
function basicIntersectionObserver() {
  const observer = new IntersectionObserver((entries) => {
    entries.forEach((entry) => {
      console.log('=== Intersection Entry ===');
      console.log('Element:', entry.target.id || entry.target.tagName);
      console.log('Is intersecting:', entry.isIntersecting);
      console.log('Intersection ratio:', entry.intersectionRatio);
      console.log('Time:', entry.time);
    });
  });

  // Observe elements
  document.querySelectorAll('.observe-me').forEach((el) => {
    observer.observe(el);
  });

  return observer;
}

// ============================================
// LAZY LOADING IMAGES
// ============================================

/**
 * Lazy load images when they enter viewport
 */
function lazyLoadImages() {
  const imageObserver = new IntersectionObserver(
    (entries, observer) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          const img = entry.target;

          // Load the actual image
          img.src = img.dataset.src;

          // Optional: handle srcset
          if (img.dataset.srcset) {
            img.srcset = img.dataset.srcset;
          }

          // Remove placeholder class
          img.classList.remove('lazy');
          img.classList.add('loaded');

          // Stop observing this image
          observer.unobserve(img);

          console.log('Loaded image:', img.src);
        }
      });
    },
    {
      rootMargin: '50px 0px', // Start loading 50px before visible
      threshold: 0.01, // Trigger as soon as 1% visible
    }
  );

  // Observe all lazy images
  document.querySelectorAll('img[data-src]').forEach((img) => {
    imageObserver.observe(img);
  });

  return imageObserver;
}

/**
 * Advanced lazy loading with loading states
 */
class ImageLazyLoader {
  constructor(options = {}) {
    this.options = {
      rootMargin: options.rootMargin || '100px 0px',
      threshold: options.threshold || 0,
      onLoad: options.onLoad || (() => {}),
      onError: options.onError || (() => {}),
      placeholder:
        options.placeholder ||
        'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7',
    };

    this.observer = new IntersectionObserver(
      this.handleIntersection.bind(this),
      {
        rootMargin: this.options.rootMargin,
        threshold: this.options.threshold,
      }
    );
  }

  handleIntersection(entries) {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        this.loadImage(entry.target);
        this.observer.unobserve(entry.target);
      }
    });
  }

  loadImage(img) {
    const src = img.dataset.src;

    img.classList.add('loading');

    // Create temp image to preload
    const tempImg = new Image();

    tempImg.onload = () => {
      img.src = src;
      img.classList.remove('loading');
      img.classList.add('loaded');
      this.options.onLoad(img);
    };

    tempImg.onerror = () => {
      img.classList.remove('loading');
      img.classList.add('error');
      this.options.onError(img);
    };

    tempImg.src = src;
  }

  observe(element) {
    if (element.dataset.src) {
      element.src = this.options.placeholder;
      this.observer.observe(element);
    }
  }

  observeAll(selector = 'img[data-src]') {
    document.querySelectorAll(selector).forEach((img) => {
      this.observe(img);
    });
  }

  disconnect() {
    this.observer.disconnect();
  }
}

// ============================================
// INFINITE SCROLL
// ============================================

/**
 * Load more content when reaching bottom
 */
class InfiniteScroll {
  constructor(options) {
    this.container = options.container;
    this.loadMore = options.loadMore;
    this.threshold = options.threshold || 200;
    this.loading = false;
    this.hasMore = true;

    // Create sentinel element
    this.sentinel = document.createElement('div');
    this.sentinel.className = 'infinite-scroll-sentinel';
    this.sentinel.style.height = '1px';
    this.container.appendChild(this.sentinel);

    this.observer = new IntersectionObserver(
      this.handleIntersection.bind(this),
      {
        root: null,
        rootMargin: `${this.threshold}px`,
        threshold: 0,
      }
    );

    this.observer.observe(this.sentinel);
  }

  async handleIntersection(entries) {
    const entry = entries[0];

    if (entry.isIntersecting && !this.loading && this.hasMore) {
      this.loading = true;
      console.log('Loading more content...');

      try {
        const result = await this.loadMore();

        if (result && result.hasMore === false) {
          this.hasMore = false;
          this.observer.disconnect();
          this.sentinel.remove();
          console.log('No more content to load');
        }
      } catch (error) {
        console.error('Error loading more:', error);
      } finally {
        this.loading = false;
      }
    }
  }

  reset() {
    this.hasMore = true;
    this.loading = false;

    if (!document.contains(this.sentinel)) {
      this.container.appendChild(this.sentinel);
      this.observer.observe(this.sentinel);
    }
  }

  destroy() {
    this.observer.disconnect();
    this.sentinel.remove();
  }
}

// ============================================
// SCROLL ANIMATIONS
// ============================================

/**
 * Trigger animations when elements come into view
 */
function scrollAnimations() {
  const animationObserver = new IntersectionObserver(
    (entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          // Get animation name from data attribute
          const animation = entry.target.dataset.animation || 'fadeIn';

          entry.target.classList.add('animated', animation);

          // Optionally unobserve after animation
          if (entry.target.dataset.animateOnce !== 'false') {
            animationObserver.unobserve(entry.target);
          }
        } else {
          // Reset animation if should animate each time
          if (entry.target.dataset.animateOnce === 'false') {
            entry.target.classList.remove(
              'animated',
              entry.target.dataset.animation
            );
          }
        }
      });
    },
    {
      threshold: 0.2, // Trigger when 20% visible
    }
  );

  document.querySelectorAll('[data-animation]').forEach((el) => {
    animationObserver.observe(el);
  });

  return animationObserver;
}

/**
 * Progress-based animations
 */
function progressAnimations() {
  // Generate thresholds from 0 to 1 in 0.01 increments
  const thresholds = Array.from({ length: 101 }, (_, i) => i / 100);

  const progressObserver = new IntersectionObserver(
    (entries) => {
      entries.forEach((entry) => {
        // Use intersection ratio as progress
        const progress = entry.intersectionRatio;

        entry.target.style.setProperty('--scroll-progress', progress);

        // Example: translate based on scroll progress
        const translateY = (1 - progress) * 50;
        entry.target.style.transform = `translateY(${translateY}px)`;
        entry.target.style.opacity = progress;
      });
    },
    {
      threshold: thresholds,
    }
  );

  document.querySelectorAll('.parallax').forEach((el) => {
    progressObserver.observe(el);
  });

  return progressObserver;
}

// ============================================
// ANALYTICS & TRACKING
// ============================================

/**
 * Track when content becomes visible
 */
function visibilityTracker() {
  const tracked = new Set();

  const trackingObserver = new IntersectionObserver(
    (entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting && !tracked.has(entry.target)) {
          tracked.add(entry.target);

          // Log visibility event
          const trackingData = {
            element: entry.target.id || entry.target.className,
            type: entry.target.dataset.trackType || 'content',
            timestamp: Date.now(),
            visiblePercent: Math.round(entry.intersectionRatio * 100),
          };

          console.log('Content viewed:', trackingData);

          // Send to analytics
          // analytics.track('content_viewed', trackingData);

          trackingObserver.unobserve(entry.target);
        }
      });
    },
    {
      threshold: 0.5, // Must be 50% visible
    }
  );

  document.querySelectorAll('[data-track]').forEach((el) => {
    trackingObserver.observe(el);
  });

  return trackingObserver;
}

/**
 * Measure time in view
 */
class ViewTimeTracker {
  constructor() {
    this.viewTimes = new Map();
    this.activeElements = new Map();

    this.observer = new IntersectionObserver(
      this.handleIntersection.bind(this),
      { threshold: 0.5 }
    );
  }

  handleIntersection(entries) {
    const now = Date.now();

    entries.forEach((entry) => {
      const id = entry.target.id || entry.target.dataset.trackId;
      if (!id) return;

      if (entry.isIntersecting) {
        // Start timing
        this.activeElements.set(id, now);
      } else if (this.activeElements.has(id)) {
        // Stop timing and accumulate
        const startTime = this.activeElements.get(id);
        const duration = now - startTime;

        const total = (this.viewTimes.get(id) || 0) + duration;
        this.viewTimes.set(id, total);

        this.activeElements.delete(id);

        console.log(`${id} viewed for ${duration}ms (total: ${total}ms)`);
      }
    });
  }

  track(element) {
    this.observer.observe(element);
  }

  getViewTime(id) {
    let time = this.viewTimes.get(id) || 0;

    // Add current session if still viewing
    if (this.activeElements.has(id)) {
      time += Date.now() - this.activeElements.get(id);
    }

    return time;
  }

  getAllViewTimes() {
    const result = {};
    this.viewTimes.forEach((time, id) => {
      result[id] = this.getViewTime(id);
    });
    return result;
  }

  disconnect() {
    // Finalize all active elements
    this.activeElements.forEach((startTime, id) => {
      const duration = Date.now() - startTime;
      const total = (this.viewTimes.get(id) || 0) + duration;
      this.viewTimes.set(id, total);
    });

    this.activeElements.clear();
    this.observer.disconnect();
  }
}

// ============================================
// STICKY ELEMENTS
// ============================================

/**
 * Detect when element becomes sticky
 */
function stickyObserver() {
  const stickyElements = document.querySelectorAll('.sticky');

  stickyElements.forEach((sticky) => {
    // Create sentinel just before sticky element
    const sentinel = document.createElement('div');
    sentinel.className = 'sticky-sentinel';
    sentinel.style.height = '1px';
    sentinel.style.marginBottom = '-1px';
    sticky.parentNode.insertBefore(sentinel, sticky);

    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          // When sentinel leaves viewport, sticky is "stuck"
          if (!entry.isIntersecting) {
            sticky.classList.add('is-stuck');
          } else {
            sticky.classList.remove('is-stuck');
          }
        });
      },
      {
        rootMargin: `-${sticky.offsetTop}px 0px 0px 0px`,
        threshold: 0,
      }
    );

    observer.observe(sentinel);
  });
}

// ============================================
// CUSTOM ROOT (SCROLLABLE CONTAINER)
// ============================================

/**
 * Observe within a scrollable container
 */
function containerIntersection() {
  const scrollContainer = document.querySelector('.scroll-container');

  const observer = new IntersectionObserver(
    (entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          entry.target.classList.add('visible');
        } else {
          entry.target.classList.remove('visible');
        }
      });
    },
    {
      root: scrollContainer, // Use container instead of viewport
      rootMargin: '0px',
      threshold: 0.5,
    }
  );

  scrollContainer.querySelectorAll('.item').forEach((item) => {
    observer.observe(item);
  });

  return observer;
}

// ============================================
// VIDEO AUTOPLAY
// ============================================

/**
 * Autoplay videos when in view, pause when out
 */
function videoAutoplay() {
  const videoObserver = new IntersectionObserver(
    (entries) => {
      entries.forEach((entry) => {
        const video = entry.target;

        if (entry.isIntersecting && entry.intersectionRatio >= 0.5) {
          video.play().catch((e) => {
            console.log('Autoplay prevented:', e.message);
          });
        } else {
          video.pause();
        }
      });
    },
    {
      threshold: [0, 0.5, 1],
    }
  );

  document.querySelectorAll('video[data-autoplay]').forEach((video) => {
    video.muted = true; // Required for autoplay in most browsers
    videoObserver.observe(video);
  });

  return videoObserver;
}

// ============================================
// NODE.JS SIMULATION
// ============================================

console.log('=== IntersectionObserver Examples ===');
console.log('Note: IntersectionObserver is a browser API.');
console.log('');
console.log('Example use cases:');
console.log('');

const useCases = [
  {
    name: 'Lazy Loading Images',
    description: 'Load images when they enter viewport',
    options: { rootMargin: '50px 0px', threshold: 0.01 },
  },
  {
    name: 'Infinite Scroll',
    description: 'Load more content at page bottom',
    options: { rootMargin: '200px', threshold: 0 },
  },
  {
    name: 'Scroll Animations',
    description: 'Trigger animations on scroll',
    options: { threshold: 0.2 },
  },
  {
    name: 'View Time Tracking',
    description: 'Measure how long content is viewed',
    options: { threshold: 0.5 },
  },
  {
    name: 'Video Autoplay',
    description: 'Play/pause based on visibility',
    options: { threshold: [0, 0.5, 1] },
  },
];

useCases.forEach(({ name, description, options }) => {
  console.log(`${name}:`);
  console.log(`  ${description}`);
  console.log(`  Options: ${JSON.stringify(options)}`);
  console.log('');
});

// Export for browser use
if (typeof module !== 'undefined' && module.exports) {
  module.exports = {
    basicIntersectionObserver,
    lazyLoadImages,
    ImageLazyLoader,
    InfiniteScroll,
    scrollAnimations,
    progressAnimations,
    visibilityTracker,
    ViewTimeTracker,
    stickyObserver,
    containerIntersection,
    videoAutoplay,
  };
}
Examples - JavaScript Tutorial | DeepML