@unlazy/core
Version:
Universal lazy loading library for placeholder images leveraging native browser APIs
210 lines (205 loc) • 7.12 kB
JavaScript
;
const blurhash = require('./shared/core.nriWQooK.cjs');
const thumbhash = require('./thumbhash.cjs');
require('./shared/core.BlPA9V5n.cjs');
const isSSR = typeof window === "undefined";
const isLazyLoadingSupported = !isSSR && "loading" in HTMLImageElement.prototype;
const isCrawler = !isSSR && (!("onscroll" in window) || /(?:gle|ing|ro)bot|crawl|spider/i.test(navigator.userAgent));
function toElementArray(target, parentElement = document) {
if (typeof target === "string")
return [...parentElement.querySelectorAll(target)];
if (target instanceof Element)
return [target];
return [...target];
}
function createIndexedImagePlaceholder(index) {
const now = Date.now();
return blurhash.DEFAULT_IMAGE_PLACEHOLDER.replace(/\s/, ` data-id='${now}-${index}' `);
}
function debounce(fn, delay) {
let timeoutId;
return function(...args) {
if (timeoutId != null)
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
fn(...args);
timeoutId = void 0;
}, delay);
};
}
function lazyLoad(selectorsOrElements = 'img[loading="lazy"]', {
hash = true,
hashType = "blurhash",
placeholderSize = blurhash.DEFAULT_PLACEHOLDER_SIZE,
updateSizesOnResize = false,
onImageLoad
} = {}) {
const cleanupHandlers = /* @__PURE__ */ new Set();
for (const [index, image] of toElementArray(selectorsOrElements).entries()) {
const resizeObserverCleanup = updateSizesAttribute(image, { updateOnResize: updateSizesOnResize });
if (updateSizesOnResize && resizeObserverCleanup)
cleanupHandlers.add(resizeObserverCleanup);
if (
// @ts-expect-error: Build-time variable
(typeof __UNLAZY_HASH_DECODING__ === "undefined" || __UNLAZY_HASH_DECODING__) && hash
) {
const placeholder = createPlaceholderFromHash({
image,
hash: typeof hash === "string" ? hash : void 0,
hashType,
size: placeholderSize
});
if (placeholder)
image.src = placeholder;
}
if (!image.dataset.src && !image.dataset.srcset) {
if (typeof __UNLAZY_LOGGING__ === "undefined" || __UNLAZY_LOGGING__)
console.error("[unlazy] Missing `data-src` or `data-srcset` attribute", image);
continue;
}
if (isCrawler || !isLazyLoadingSupported) {
updatePictureSources(image);
updateImageSrcset(image);
updateImageSrc(image);
continue;
}
if (!image.src)
image.src = createIndexedImagePlaceholder(index);
if (image.complete && image.naturalWidth > 0) {
loadImage(image, onImageLoad);
continue;
}
const loadHandler = () => loadImage(image, onImageLoad);
image.addEventListener("load", loadHandler, { once: true });
cleanupHandlers.add(
() => image.removeEventListener("load", loadHandler)
);
}
return () => {
for (const fn of cleanupHandlers) fn();
cleanupHandlers.clear();
};
}
function autoSizes(selectorsOrElements = 'img[data-sizes="auto"], source[data-sizes="auto"]') {
for (const image of toElementArray(selectorsOrElements))
updateSizesAttribute(image);
}
function loadImage(image, onImageLoad) {
if (isDescendantOfPicture(image)) {
updatePictureSources(image);
updateImageSrcset(image);
updateImageSrc(image);
onImageLoad?.(image);
return;
}
const temporaryImage = new Image();
const { srcset: dataSrcset, src: dataSrc, sizes: dataSizes } = image.dataset;
if (dataSizes === "auto") {
const width = getOffsetWidth(image);
if (width)
temporaryImage.sizes = `${width}px`;
} else if (image.sizes) {
temporaryImage.sizes = image.sizes;
}
if (dataSrcset)
temporaryImage.srcset = dataSrcset;
if (dataSrc)
temporaryImage.src = dataSrc;
temporaryImage.addEventListener("load", () => {
updateImageSrcset(image);
updateImageSrc(image);
onImageLoad?.(image);
}, { once: true });
}
function createPlaceholderFromHash({
image,
hash,
hashType = "blurhash",
size = blurhash.DEFAULT_PLACEHOLDER_SIZE,
ratio
} = {}) {
if (image && !hash) {
const { blurhash, thumbhash } = image.dataset;
hash = thumbhash || blurhash;
hashType = thumbhash ? "thumbhash" : "blurhash";
}
if (!hash)
return;
try {
if (hashType === "blurhash") {
if (image && !ratio) {
const elementWidth = image.width || image.offsetWidth || size;
const elementHeight = image.height || image.offsetHeight || size;
ratio = elementWidth / elementHeight;
}
return blurhash.createPngDataUri(hash, { ratio, size });
}
return thumbhash.createPngDataUri(hash);
} catch (error) {
if (typeof __UNLAZY_LOGGING__ === "undefined" || __UNLAZY_LOGGING__)
console.error(`[unlazy] Failed to generate ${hashType} placeholder:`, error);
}
}
const resizeObserverCache = /* @__PURE__ */ new WeakMap();
function updateSizesAttribute(element, options) {
if (element.dataset.sizes !== "auto")
return;
const width = getOffsetWidth(element);
if (width)
element.sizes = `${width}px`;
if (isDescendantOfPicture(element) && options?.processSourceElements) {
for (const sourceElement of [...element.parentElement.getElementsByTagName("source")]) {
updateSizesAttribute(sourceElement, { processSourceElements: true });
}
}
if (options?.updateOnResize) {
if (!resizeObserverCache.has(element)) {
const debouncedSizeUpdate = debounce(() => updateSizesAttribute(element), 500);
const observer = new ResizeObserver(debouncedSizeUpdate);
resizeObserverCache.set(element, observer);
observer.observe(element);
}
return () => {
const observer = resizeObserverCache.get(element);
if (observer) {
observer.disconnect();
resizeObserverCache.delete(element);
}
};
}
}
function updateImageSrc(image) {
if (image.dataset.src) {
image.src = image.dataset.src;
image.removeAttribute("data-src");
}
}
function updateImageSrcset(image) {
if (image.dataset.srcset) {
image.srcset = image.dataset.srcset;
image.removeAttribute("data-srcset");
}
}
function updatePictureSources(image) {
const pictureElement = image.parentElement;
if (pictureElement?.tagName.toLowerCase() === "picture") {
[...pictureElement.querySelectorAll("source[data-srcset]")].forEach(updateImageSrcset);
[...pictureElement.querySelectorAll("source[data-src]")].forEach(updateImageSrc);
}
}
function getOffsetWidth(element) {
return element instanceof HTMLSourceElement ? element.parentElement?.getElementsByTagName("img")[0]?.offsetWidth : element.offsetWidth;
}
function isDescendantOfPicture(element) {
return element.parentElement?.tagName.toLowerCase() === "picture";
}
exports.autoSizes = autoSizes;
exports.createIndexedImagePlaceholder = createIndexedImagePlaceholder;
exports.createPlaceholderFromHash = createPlaceholderFromHash;
exports.debounce = debounce;
exports.isCrawler = isCrawler;
exports.isLazyLoadingSupported = isLazyLoadingSupported;
exports.isSSR = isSSR;
exports.lazyLoad = lazyLoad;
exports.loadImage = loadImage;
exports.toElementArray = toElementArray;