UNPKG

react-img-toolkit

Version:

A lightweight React library for optimizing image loading through preloading, lazy loading, and caching capabilities

471 lines (451 loc) 20.7 kB
import React, { useRef, useMemo, useCallback, useEffect, useState } from 'react'; /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ function __awaiter(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } function __generator(thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (g && (g = 0, op[0] && (_ = 0)), _) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } } typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; var isHtmlString = function (str) { return /<\/?[a-z][\s\S]*>/i.test(str); }; var extractImageUrlsFromHtml = function (html) { var imgUrls = []; var imgRegex = /<img[^>]+src="([^">]+)"/g; var match; while ((match = imgRegex.exec(html)) !== null) { imgUrls.push(match[1]); } return imgUrls; }; var extractImageUrlsFromData = function (data) { var urls = []; if (Array.isArray(data)) { data.forEach(function (item) { urls = urls.concat(extractImageUrlsFromData(item)); }); } else if (typeof data === "object" && data !== null) { for (var key in data) { if (Object.prototype.hasOwnProperty.call(data, key)) { urls = urls.concat(extractImageUrlsFromData(data[key])); } } } else if (typeof data === "string") { if (isHtmlString(data)) { urls = urls.concat(extractImageUrlsFromHtml(data)); } else if (data.startsWith("http")) { urls.push(data); } } return urls; }; var preloadImages = function (imageUrls) { return __awaiter(void 0, void 0, void 0, function () { var promises; return __generator(this, function (_a) { switch (_a.label) { case 0: promises = imageUrls.map(function (url) { return new Promise(function (resolve, reject) { var img = new Image(); img.src = url; img.onload = resolve; img.onerror = function (error) { console.error("Failed to load image at ".concat(url), error); reject(error); }; }); }); _a.label = 1; case 1: _a.trys.push([1, 3, , 4]); return [4 /*yield*/, Promise.all(promises)]; case 2: _a.sent(); return [3 /*break*/, 4]; case 3: _a.sent(); console.error("Some images failed to load."); return [3 /*break*/, 4]; case 4: return [2 /*return*/]; } }); }); }; function isImageCached(url) { return __awaiter(this, void 0, void 0, function () { var cache, cachedResponse; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, caches.open("image-preloader-cache")]; case 1: cache = _a.sent(); return [4 /*yield*/, cache.match(url)]; case 2: cachedResponse = _a.sent(); return [2 /*return*/, !!cachedResponse]; } }); }); } function cacheImages(urls) { return __awaiter(this, void 0, void 0, function () { var cache; var _this = this; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, caches.open("image-preloader-cache")]; case 1: cache = _a.sent(); return [4 /*yield*/, Promise.all(urls.map(function (url) { return __awaiter(_this, void 0, void 0, function () { var error_1; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, isImageCached(url)]; case 1: if (!!(_a.sent())) return [3 /*break*/, 5]; _a.label = 2; case 2: _a.trys.push([2, 4, , 5]); return [4 /*yield*/, cache.add(url)]; case 3: _a.sent(); return [3 /*break*/, 5]; case 4: error_1 = _a.sent(); console.error("Failed to cache image: ".concat(url), error_1); return [3 /*break*/, 5]; case 5: return [2 /*return*/]; } }); }); }))]; case 2: _a.sent(); return [2 /*return*/]; } }); }); } var useImagePreloader = function (_a) { var _b = _a === void 0 ? {} : _a, _c = _b.data, data = _c === void 0 ? [] : _c, onSuccess = _b.onSuccess, onError = _b.onError; var preloadedImagesCount = useRef(0); var hasPreloaded = useRef(false); // Ensure preload happens only once // Extract and deduplicate URLs from `data` var uniqueUrls = useMemo(function () { var urlsFromData = extractImageUrlsFromData(data); return Array.from(new Set(urlsFromData)); // Use Set to ensure uniqueness }, [data]); // Separate function to determine uncached URLs var getUncachedUrls = useCallback(function () { return __awaiter(void 0, void 0, void 0, function () { var uncachedUrls, _i, uniqueUrls_1, url; return __generator(this, function (_a) { switch (_a.label) { case 0: uncachedUrls = []; _i = 0, uniqueUrls_1 = uniqueUrls; _a.label = 1; case 1: if (!(_i < uniqueUrls_1.length)) return [3 /*break*/, 4]; url = uniqueUrls_1[_i]; return [4 /*yield*/, isImageCached(url)]; case 2: if (!(_a.sent())) { uncachedUrls.push(url); } _a.label = 3; case 3: _i++; return [3 /*break*/, 1]; case 4: return [2 /*return*/, uncachedUrls]; } }); }); }, [uniqueUrls]); // Function to preload images var preloadImg = useCallback(function () { return __awaiter(void 0, void 0, void 0, function () { var uncachedUrls, error_1; return __generator(this, function (_a) { switch (_a.label) { case 0: if (hasPreloaded.current) return [2 /*return*/]; // Prevent multiple preloads _a.label = 1; case 1: _a.trys.push([1, 6, , 7]); return [4 /*yield*/, getUncachedUrls()]; case 2: uncachedUrls = _a.sent(); if (!uncachedUrls.length) return [3 /*break*/, 5]; return [4 /*yield*/, cacheImages(uncachedUrls)]; case 3: _a.sent(); return [4 /*yield*/, preloadImages(uncachedUrls)]; case 4: _a.sent(); preloadedImagesCount.current += uncachedUrls.length; if (preloadedImagesCount.current === uniqueUrls.length) { hasPreloaded.current = true; // Mark preload as complete onSuccess === null || onSuccess === void 0 ? void 0 : onSuccess(); } _a.label = 5; case 5: return [3 /*break*/, 7]; case 6: error_1 = _a.sent(); onError === null || onError === void 0 ? void 0 : onError(error_1); return [3 /*break*/, 7]; case 7: return [2 /*return*/]; } }); }); }, [uniqueUrls, getUncachedUrls, onSuccess, onError, hasPreloaded]); // Trigger preload once when component mounts or data changes useEffect(function () { preloadImg(); }, [preloadImg]); return { imageUrls: uniqueUrls }; }; var ImagePreloader = function (_a) { var _b = _a.data, data = _b === void 0 ? {} : _b, onSuccess = _a.onSuccess, onError = _a.onError, children = _a.children; useImagePreloader({ onError: onError, onSuccess: onSuccess, data: data }).imageUrls; return React.createElement(React.Fragment, null, children); }; var useImageStatus = function (_a) { var src = _a.src; var _b = useState("idle"), status = _b[0], setStatus = _b[1]; useEffect(function () { if (!src) { setStatus("idle"); return; } var checkImageStatus = function () { return __awaiter(void 0, void 0, void 0, function () { var isCached, img; return __generator(this, function (_a) { switch (_a.label) { case 0: setStatus("loading"); return [4 /*yield*/, isImageCached(src)]; case 1: isCached = _a.sent(); if (isCached) { setStatus("loaded"); return [2 /*return*/]; } img = new Image(); img.onload = function () { return __awaiter(void 0, void 0, void 0, function () { var cache, error_1; return __generator(this, function (_a) { switch (_a.label) { case 0: setStatus("loaded"); _a.label = 1; case 1: _a.trys.push([1, 4, , 5]); return [4 /*yield*/, caches.open("image-preloader-cache")]; case 2: cache = _a.sent(); return [4 /*yield*/, cache.add(src)]; case 3: _a.sent(); return [3 /*break*/, 5]; case 4: error_1 = _a.sent(); console.warn("Failed to add image to cache:", src, error_1); return [3 /*break*/, 5]; case 5: return [2 /*return*/]; } }); }); }; img.onerror = function () { setStatus("error"); }; img.src = src; return [2 /*return*/, function () { img.onload = null; img.onerror = null; }]; } }); }); }; checkImageStatus(); }, [src]); return status; }; var useLazyImage = function (_a) { var src = _a.src, _b = _a.options, options = _b === void 0 ? {} : _b; var _c = useState(false), isIntersecting = _c[0], setIsIntersecting = _c[1]; var _d = useState(false), isLoaded = _d[0], setIsLoaded = _d[1]; var ref = useRef(null); useEffect(function () { var element = ref.current; if (!element) return; var observer = new IntersectionObserver(function (_a) { var entry = _a[0]; setIsIntersecting(entry.isIntersecting); }, { threshold: options.threshold || 0, rootMargin: options.rootMargin || "0px", }); observer.observe(element); return function () { observer.unobserve(element); observer.disconnect(); }; }, [options.threshold, options.rootMargin]); useEffect(function () { if (!isIntersecting || isLoaded) return; var img = new Image(); img.src = src; img.onload = function () { setIsLoaded(true); }; return function () { img.onload = null; }; }, [isIntersecting, isLoaded, src]); return { isIntersecting: isIntersecting, isLoaded: isLoaded, ref: ref }; }; var useImageCache = function (_a) { var src = _a.src; var _b = useState(true), loading = _b[0], setLoading = _b[1]; var _c = useState(false), isCached = _c[0], setIsCached = _c[1]; useEffect(function () { var checkAndCacheImage = function () { return __awaiter(void 0, void 0, void 0, function () { var browserCached, img; return __generator(this, function (_a) { switch (_a.label) { case 0: setLoading(true); return [4 /*yield*/, isImageCached(src)]; case 1: browserCached = _a.sent(); if (browserCached) { setIsCached(true); setLoading(false); return [2 /*return*/]; } img = new Image(); img.onload = function () { return __awaiter(void 0, void 0, void 0, function () { var cache, error_1; return __generator(this, function (_a) { switch (_a.label) { case 0: setIsCached(true); setLoading(false); _a.label = 1; case 1: _a.trys.push([1, 4, , 5]); return [4 /*yield*/, caches.open("image-preloader-cache")]; case 2: cache = _a.sent(); return [4 /*yield*/, cache.add(src)]; case 3: _a.sent(); return [3 /*break*/, 5]; case 4: error_1 = _a.sent(); console.warn("Failed to add image to browser cache:", src, error_1); return [3 /*break*/, 5]; case 5: return [2 /*return*/]; } }); }); }; img.onerror = function () { setLoading(false); }; img.src = src; return [2 /*return*/, function () { img.onload = null; img.onerror = null; }]; } }); }); }; checkAndCacheImage(); }, [src]); return { cachedSrc: src, loading: loading, isCached: isCached, }; }; var useImageLoad = function (_a) { var url = _a.url, crossOrigin = _a.crossOrigin, referrerPolicy = _a.referrerPolicy; var _b = useState(null), image = _b[0], setImage = _b[1]; var _c = useState(true), isLoading = _c[0], setIsLoading = _c[1]; var _d = useState(null), error = _d[0], setError = _d[1]; useEffect(function () { var img = new Image(); var handleLoad = function () { setImage(img); setIsLoading(false); setError(null); }; var handleError = function () { setImage(null); setIsLoading(false); setError(new Error("Failed to load image from ".concat(url))); }; if (crossOrigin) { img.crossOrigin = crossOrigin; } if (referrerPolicy) { img.referrerPolicy = referrerPolicy; } img.addEventListener('load', handleLoad); img.addEventListener('error', handleError); img.src = url; return function () { img.removeEventListener('load', handleLoad); img.removeEventListener('error', handleError); }; }, [url, crossOrigin, referrerPolicy]); return { image: image, isLoading: isLoading, error: error }; }; export { ImagePreloader, cacheImages, extractImageUrlsFromData, extractImageUrlsFromHtml, isHtmlString, isImageCached, preloadImages, useImageCache, useImageLoad, useImagePreloader, useImageStatus, useLazyImage }; //# sourceMappingURL=index.esm.js.map