ultimate-jekyll-manager
Version:
Ultimate Jekyll dependency manager
376 lines (312 loc) • 9.85 kB
JavaScript
// 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);
});
}
};