UNPKG

ultimate-jekyll-manager

Version:
376 lines (312 loc) 9.85 kB
// Lazy Loading Module module.exports = (Manager, options) => { // Shortcuts const { webManager } = Manager; // Configuration const config = { selector: '[data-lazy], .lazy, img[data-src], img[data-srcset], picture source[data-srcset], iframe[data-src], video[data-src]', rootMargin: '50px 0px', // Start loading 50px before element comes into view threshold: 0.01, // Trigger when 1% of element is visible loadedClass: 'lazy-loaded', loadingClass: 'lazy-loading', errorClass: 'lazy-error', backgroundSelector: '[data-lazy-bg]', successCallback: null, errorCallback: null }; // Merge with any provided options Object.assign(config, options?.lazyLoading || {}); // Track loaded elements to avoid reprocessing const loadedElements = new WeakSet(); // IntersectionObserver instance let observer = null; // Wait for DOM to be ready webManager.dom().ready().then(() => { initLazyLoading(); }); function initLazyLoading() { // Check if IntersectionObserver is supported if (!('IntersectionObserver' in window)) { console.warn('IntersectionObserver not supported, loading all images immediately'); loadAllImages(); return; } // Create the observer observer = new IntersectionObserver(handleIntersection, { rootMargin: config.rootMargin, threshold: config.threshold }); // Start observing elements observeElements(); // Re-observe on dynamic content changes setupMutationObserver(); } function handleIntersection(entries, observer) { entries.forEach(entry => { if (!entry.isIntersecting) { return; // Element is not in view, skip } // Element is in view, process it const element = entry.target; // Stop observing this element observer.unobserve(element); // Load the element loadElement(element); }); } function loadElement(element) { // Skip if already loaded if (loadedElements.has(element)) { return; } // Mark as loading element.classList.add(config.loadingClass); element.classList.remove(config.errorClass); // Determine element type and load accordingly const tagName = element.tagName.toLowerCase(); if (tagName === 'img') { loadImage(element); } else if (tagName === 'source') { loadSource(element); } else if (tagName === 'iframe') { loadIframe(element); } else if (tagName === 'video') { loadVideo(element); } else if (element.hasAttribute('data-lazy-bg')) { loadBackgroundImage(element); } else { // Generic lazy loading for other elements loadGeneric(element); } } function loadImage(img) { const src = img.getAttribute('data-src'); const srcset = img.getAttribute('data-srcset'); const sizes = img.getAttribute('data-sizes'); // Create a new image to test loading const tempImg = new Image(); // Set up load handler tempImg.onload = () => { // Apply sources to actual image if (src) { img.src = src; img.removeAttribute('data-src'); } if (srcset) { img.srcset = srcset; img.removeAttribute('data-srcset'); } if (sizes) { img.sizes = sizes; img.removeAttribute('data-sizes'); } // Mark as loaded markAsLoaded(img); }; // Set up error handler tempImg.onerror = () => { markAsError(img); }; // Start loading if (srcset) { tempImg.srcset = srcset; } if (src) { tempImg.src = src; } // Handle images that are already cached if (tempImg.complete && tempImg.naturalHeight !== 0) { tempImg.onload(); } } function loadSource(source) { const srcset = source.getAttribute('data-srcset'); const media = source.getAttribute('data-media'); const type = source.getAttribute('data-type'); if (srcset) { source.srcset = srcset; source.removeAttribute('data-srcset'); } if (media) { source.media = media; source.removeAttribute('data-media'); } if (type) { source.type = type; source.removeAttribute('data-type'); } // Find parent picture element and trigger update const picture = source.closest('picture'); if (picture) { const img = picture.querySelector('img'); if (img) { // Force browser to re-evaluate picture sources const currentSrc = img.currentSrc; img.src = img.src; } } markAsLoaded(source); } function loadIframe(iframe) { const src = iframe.getAttribute('data-src'); if (src) { iframe.src = src; iframe.removeAttribute('data-src'); // Listen for load event iframe.addEventListener('load', () => { markAsLoaded(iframe); }); iframe.addEventListener('error', () => { markAsError(iframe); }); } } function loadVideo(video) { const src = video.getAttribute('data-src'); const poster = video.getAttribute('data-poster'); if (poster) { video.poster = poster; video.removeAttribute('data-poster'); } if (src) { video.src = src; video.removeAttribute('data-src'); } // Also handle source elements within video const sources = video.querySelectorAll('source[data-src]'); sources.forEach(source => { const src = source.getAttribute('data-src'); if (src) { source.src = src; source.removeAttribute('data-src'); } }); // Load the video video.load(); // Listen for loadeddata event video.addEventListener('loadeddata', () => { markAsLoaded(video); }); video.addEventListener('error', () => { markAsError(video); }); } function loadBackgroundImage(element) { const bgImage = element.getAttribute('data-lazy-bg'); if (bgImage) { // Create temporary image to test loading const tempImg = new Image(); tempImg.onload = () => { // Apply background image element.style.backgroundImage = `url('${bgImage}')`; element.removeAttribute('data-lazy-bg'); markAsLoaded(element); }; tempImg.onerror = () => { markAsError(element); }; tempImg.src = bgImage; } } function loadGeneric(element) { // Handle any custom lazy loading attributes const lazyAttrs = Array.from(element.attributes).filter(attr => attr.name.startsWith('data-lazy-') ); lazyAttrs.forEach(attr => { const actualAttrName = attr.name.replace('data-lazy-', ''); element.setAttribute(actualAttrName, attr.value); element.removeAttribute(attr.name); }); markAsLoaded(element); } function markAsLoaded(element) { loadedElements.add(element); element.classList.remove(config.loadingClass); element.classList.add(config.loadedClass); // Call success callback if provided if (config.successCallback && typeof config.successCallback === 'function') { config.successCallback(element); } // Dispatch custom event element.dispatchEvent(new CustomEvent('lazyloaded', { bubbles: true, detail: { element } })); } function markAsError(element) { loadedElements.add(element); element.classList.remove(config.loadingClass); element.classList.add(config.errorClass); // Call error callback if provided if (config.errorCallback && typeof config.errorCallback === 'function') { config.errorCallback(element); } // Dispatch custom event element.dispatchEvent(new CustomEvent('lazyerror', { bubbles: true, detail: { element } })); console.error('Failed to lazy load element:', element); } function observeElements() { // Find all elements matching our selector const elements = document.querySelectorAll(config.selector); elements.forEach(element => { // Skip if already loaded if (loadedElements.has(element)) { return; } // Start observing observer.observe(element); }); // Also observe background images const bgElements = document.querySelectorAll(config.backgroundSelector); bgElements.forEach(element => { if (!loadedElements.has(element)) { observer.observe(element); } }); } function setupMutationObserver() { // Watch for new elements added to the DOM const mutationObserver = new MutationObserver((mutations) => { mutations.forEach(mutation => { if (mutation.type === 'childList') { // Check if any new lazy-loadable elements were added mutation.addedNodes.forEach(node => { if (node.nodeType === 1) { // Element node // Check the node itself if (node.matches && node.matches(config.selector)) { if (!loadedElements.has(node)) { observer.observe(node); } } // Check descendants if (node.querySelectorAll) { const lazyElements = node.querySelectorAll(config.selector); lazyElements.forEach(element => { if (!loadedElements.has(element)) { observer.observe(element); } }); } } }); } }); }); // Start observing the document body for changes mutationObserver.observe(document.body, { childList: true, subtree: true }); } function loadAllImages() { // Fallback for browsers without IntersectionObserver const elements = document.querySelectorAll(config.selector); elements.forEach(element => { loadElement(element); }); } };