@unlazy/core
Version: 
Universal lazy loading library for placeholder images leveraging native browser APIs
199 lines (195 loc) • 6.92 kB
JavaScript
import { D as DEFAULT_IMAGE_PLACEHOLDER, a as DEFAULT_PLACEHOLDER_SIZE, c as createPngDataUri } from './shared/core.D9MKAmxf.mjs';
import { createPngDataUri as createPngDataUri$1 } from './thumbhash.mjs';
import './shared/core.URvayf9Q.mjs';
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 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 = 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 = 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 createPngDataUri(hash, { ratio, size });
    }
    return createPngDataUri$1(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";
}
export { autoSizes, createIndexedImagePlaceholder, createPlaceholderFromHash, debounce, isCrawler, isLazyLoadingSupported, isSSR, lazyLoad, loadImage, toElementArray };