react-img-toolkit
Version:
A lightweight React library for optimizing image loading through preloading, lazy loading, and caching capabilities
663 lines (639 loc) • 31.6 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
var React = require('react');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var React__default = /*#__PURE__*/_interopDefaultLegacy(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 = React.useRef(0);
var hasPreloaded = React.useRef(false); // Ensure preload happens only once
// Extract and deduplicate URLs from `data`
var uniqueUrls = React.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 = React.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 = React.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
React.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 = React.useState(null), preloadContainer = _e[0], setPreloadContainer = _e[1];
// Add Safari-specific preloading
React.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
React.useEffect(function () {
return function () {
if (preloadContainer) {
preloadContainer.remove();
}
};
}, [preloadContainer]);
return React__default["default"].createElement(React__default["default"].Fragment, null, children);
};
var useImageStatus = function (_a) {
var src = _a.src;
var _b = React.useState("idle"), status = _b[0], setStatus = _b[1];
React.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 = React.useState(false), isIntersecting = _c[0], setIsIntersecting = _c[1];
var _d = React.useState(false), isLoaded = _d[0], setIsLoaded = _d[1];
var ref = React.useRef(null);
React.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]);
React.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 = React.useState(true), loading = _b[0], setLoading = _b[1];
var _c = React.useState(false), isCached = _c[0], setIsCached = _c[1];
React.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 = React.useState(null), image = _b[0], setImage = _b[1];
var _c = React.useState(true), isLoading = _c[0], setIsLoading = _c[1];
var _d = React.useState(null), error = _d[0], setError = _d[1];
React.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 };
};
exports.ImagePreloader = ImagePreloader;
exports.cacheImages = cacheImages;
exports.extractImageUrlsFromData = extractImageUrlsFromData;
exports.extractImageUrlsFromHtml = extractImageUrlsFromHtml;
exports.isHtmlString = isHtmlString;
exports.isImageCached = isImageCached;
exports.preloadImages = preloadImages;
exports.useImageCache = useImageCache;
exports.useImageLoad = useImageLoad;
exports.useImagePreloader = useImagePreloader;
exports.useImageStatus = useImageStatus;
exports.useLazyImage = useLazyImage;
//# sourceMappingURL=index.js.map