javascript
examples
examples.js⚡javascript
/**
* 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,
};
}