UNPKG

react-img-toolkit

Version:

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

644 lines (624 loc) 31 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, crossOrigin, referrerPolicy) { return __awaiter(void 0, void 0, void 0, function () { var isSafari, preloadContainer, promises, error_1; return __generator(this, function (_a) { switch (_a.label) { case 0: console.log('Starting preload for images:', imageUrls); isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); preloadContainer = document.createElement('div'); preloadContainer.style.cssText = isSafari ? "\n position: absolute;\n left: -9999px;\n top: 0;\n width: 1px;\n height: 1px;\n overflow: hidden;\n z-index: -1;\n " : "display: none;"; document.body.appendChild(preloadContainer); promises = imageUrls.map(function (url) { return __awaiter(void 0, void 0, void 0, function () { var response, blob, image_1, fetchError_1, img_1, loadPromise, error_2; return __generator(this, function (_a) { switch (_a.label) { case 0: _a.trys.push([0, 8, , 9]); console.log('Preloading image:', url); if (!!isSafari) return [3 /*break*/, 6]; _a.label = 1; case 1: _a.trys.push([1, 5, , 6]); console.log('Trying fetch for:', url); return [4 /*yield*/, fetch(url, { mode: 'cors', credentials: 'include', headers: { 'Accept': 'image/webp,image/*,*/*;q=0.8', 'Origin': window.location.origin, 'Cache-Control': 'no-cache', }, })]; case 2: response = _a.sent(); if (!response.ok) { throw new Error("Fetch failed with status: ".concat(response.status)); } return [4 /*yield*/, response.blob()]; case 3: blob = _a.sent(); image_1 = new Image(); image_1.src = URL.createObjectURL(blob); return [4 /*yield*/, new Promise(function (resolve, reject) { image_1.onload = function () { URL.revokeObjectURL(image_1.src); resolve(); }; image_1.onerror = function () { URL.revokeObjectURL(image_1.src); reject(); }; })]; case 4: _a.sent(); console.log('Fetch successful for:', url); return [2 /*return*/]; case 5: fetchError_1 = _a.sent(); console.warn('Fetch failed, falling back to image element:', url, fetchError_1); return [3 /*break*/, 6]; case 6: img_1 = new Image(); img_1.crossOrigin = crossOrigin; img_1.referrerPolicy = referrerPolicy; loadPromise = new Promise(function (resolve, reject) { img_1.onload = function () { console.log('Image loaded via <img> tag:', url); resolve(); }; img_1.onerror = function (error) { console.error('Image failed to load:', url, error); reject(new Error("Image failed: ".concat(url))); }; }); preloadContainer.appendChild(img_1); img_1.src = url; return [4 /*yield*/, loadPromise]; case 7: _a.sent(); img_1.remove(); return [3 /*break*/, 9]; case 8: error_2 = _a.sent(); console.error("Failed to preload image: ".concat(url), error_2); throw error_2; case 9: return [2 /*return*/]; } }); }); }); _a.label = 1; case 1: _a.trys.push([1, 3, 4, 5]); return [4 /*yield*/, Promise.all(promises)]; case 2: _a.sent(); console.log('✅ All images preloaded successfully'); return [3 /*break*/, 5]; case 3: error_1 = _a.sent(); console.error('⚠️ Some images failed to load:', error_1); return [3 /*break*/, 5]; case 4: preloadContainer.remove(); console.log('🧹 Cleaned up preload container'); return [7 /*endfinally*/]; case 5: 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, _d = _b.crossOrigin, crossOrigin = _d === void 0 ? 'anonymous' : _d, _e = _b.referrerPolicy, referrerPolicy = _e === void 0 ? 'no-referrer' : _e; 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, crossOrigin, referrerPolicy)]; 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, crossOrigin, referrerPolicy]); // 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, _c = _a.crossOrigin, crossOrigin = _c === void 0 ? 'anonymous' : _c, _d = _a.referrerPolicy, referrerPolicy = _d === void 0 ? 'no-referrer' : _d; var imageUrls = useImagePreloader({ onError: onError, onSuccess: onSuccess, data: data, crossOrigin: crossOrigin, referrerPolicy: referrerPolicy }).imageUrls; // Create a hidden container for Safari preloading var _e = useState(null), preloadContainer = _e[0], setPreloadContainer = _e[1]; // Add Safari-specific preloading useEffect(function () { if (!preloadContainer) { var container = document.createElement('div'); container.style.display = 'none'; document.body.appendChild(container); setPreloadContainer(container); } if (onError && navigator.userAgent.includes('Safari')) { var checkImages = function () { return __awaiter(void 0, void 0, void 0, function () { var _loop_1, _i, imageUrls_1, url; return __generator(this, function (_a) { switch (_a.label) { case 0: _loop_1 = function (url) { var response, img_1, error_1; return __generator(this, function (_b) { switch (_b.label) { case 0: _b.trys.push([0, 3, , 4]); return [4 /*yield*/, fetch(url, { mode: 'cors', credentials: 'same-origin', headers: { 'Accept': 'image/webp,image/*,*/*;q=0.8' } })]; case 1: response = _b.sent(); if (!response.ok) { throw new Error("Failed to fetch image: ".concat(url)); } img_1 = new Image(); img_1.crossOrigin = crossOrigin; img_1.referrerPolicy = referrerPolicy; // Add to container if (preloadContainer) { preloadContainer.appendChild(img_1); } // Wait for image to load return [4 /*yield*/, new Promise(function (resolve, reject) { img_1.onload = function () { return resolve(img_1); }; img_1.onerror = function () { return reject(new Error("Image failed to load: ".concat(url))); }; img_1.src = url; })]; case 2: // Wait for image to load _b.sent(); // Remove from container img_1.remove(); return [3 /*break*/, 4]; case 3: error_1 = _b.sent(); onError(error_1); return [3 /*break*/, 4]; case 4: return [2 /*return*/]; } }); }; _i = 0, imageUrls_1 = imageUrls; _a.label = 1; case 1: if (!(_i < imageUrls_1.length)) return [3 /*break*/, 4]; url = imageUrls_1[_i]; return [5 /*yield**/, _loop_1(url)]; case 2: _a.sent(); _a.label = 3; case 3: _i++; return [3 /*break*/, 1]; case 4: return [2 /*return*/]; } }); }); }; checkImages(); } }, [imageUrls, onError, crossOrigin, referrerPolicy, preloadContainer]); // Clean up on unmount useEffect(function () { return function () { if (preloadContainer) { preloadContainer.remove(); } }; }, [preloadContainer]); 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