/*!
* Module Images
*/
/**
* @namespace Images
*/
const MODULE_NAME = 'Images';
//###[ IMPORTS ]########################################################################################################
import {orDefault, isArray, isPlainObject, assert, isEmpty, isElement, hasValue, Deferred} from './basic.js';
import {waitForRepaint} from './timers.js';
//###[ DATA ]###########################################################################################################
const PRELOADED_IMAGES = {
unnamed : [],
named : {}
};
//###[ EXPORTS ]########################################################################################################
/**
* @namespace Images:preload
*/
/**
* Preloads images by URL, so that subsequent usages are served from browser cache.
* Images can be preloaded anonymously or with a given name. So you can either just use the url again,
* or, to be super-sure, call the method again, with just the image name to get the preloaded image itself.
*
* The function returns a Deferred, which resolves, after the images have loaded, with either an array of preloaded
* images or a single image, if only one has been defined. The Deferred contains all images newly created for
* preloading on the provision property before the Deferred resolves.
*
* @param {(String|String[]|Object.<String, String>)} images - a URL, an array of URLs or a plain object containing named URLs. In case the string is an already used name, the image object from the named preloaded images cache is returned.
* @returns {Basic.Deferred<Image|Image[]>|Image} either a Deferred, resolving after images are preloaded, or a requested cached image
*
* @memberof Images:preload
* @alias preload
* @example
* preload([url1, url2, url3]).then(images => { alert(`loaded ${images.length} images`); });
* const provisionalImage preload({name1 : url1, name2 : url2}}).provision.name1;
* const preloadedImage = preload('name1');
*/
export function preload(images){
const
preloadedImages = [],
deferred = new Deferred()
;
let newImages;
if( !isPlainObject(images) && !isArray(images) ){
images = `${images}`;
if( hasValue(PRELOADED_IMAGES.named[images]) ){
return PRELOADED_IMAGES.named[images];
} else {
images = [images];
}
}
if( isPlainObject(images) ){
newImages = {};
Object.entries(images).forEach(([key, value]) => {
key = `${key}`;
value = `${value}`;
if( !hasValue(PRELOADED_IMAGES.named[key]) ){
newImages[key] = new Image();
newImages[key].src = value;
preloadedImages.push(newImages[key]);
}
});
PRELOADED_IMAGES.named = {...PRELOADED_IMAGES.named, ...newImages};
} else if( isArray(images) ){
newImages = [];
images.forEach(value => {
const newImage = new Image();
newImage.src = `${value}`;
newImages.push(newImage);
preloadedImages.push(newImage);
});
PRELOADED_IMAGES.unnamed = Array.from(new Set(PRELOADED_IMAGES.unnamed.concat(newImages)));
}
deferred.provision = (isArray(newImages) && (newImages.length === 1)) ? newImages[0] : newImages;
loaded(preloadedImages)
.then(deferred.resolve)
.catch(deferred.reject)
;
return deferred;
}
/**
* @namespace Images:loaded
*/
/**
* Fixes problems with image "load" events and fires the event even in case the image is already loaded or served from
* browser cache. So repeated calls to this method on the same loaded image will actually work.
*
* Also supports imgs inside picture elements, while automatically handling the polyfills respimage and picturefill if
* present in window. Make sure to apply this method to the img _inside_ the picture and _not_ on the picture itself!
*
* Define "dimensionsNeeded" if your definition of "loaded" includes, that the loaded image should already have usable
* image dimensions for layouting. Use this, if you need to do calculations based on image dimensions after load.
* Dimensions are determined using the images "naturalWidth".
*
* The function returns a Deferred, which resolves, after the images have loaded, with either an array of loaded
* images or a single image, if only one has been defined. The Deferred contains all initially given images on the
* provision property before the Deferred resolves.
*
* @param {Image|Array<Image>} images - an image or an array of images
* @param {?Boolean} [dimensionsNeeded=false] - tells the check if we expect the loaded image to have readable dimensions
* @returns {Basic.Deferred<Image|Image[]>} a Deferred, resolving after all given images have loaded
*
* @memberof Images:loaded
* @alias loaded
* @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/naturalWidth
* @example
* loaded(image).then(image => { image.classList.remove('hidden'); });
* loaded([image1, image2, image3]).then(images => { alert(`all ${images.length} images have loaded`); })
*/
export function loaded(images, dimensionsNeeded=false){
const __methodName__ = 'loaded';
images = orDefault(images, [], 'arr').filter(image => {
return Object.prototype.toString.call(image).slice(8, -1).toLowerCase() === 'htmlimageelement';
});
dimensionsNeeded = orDefault(dimensionsNeeded, false, 'bool');
function onLoad(e){
const image = e.currentTarget;
if( !dimensionsNeeded || (dimensionsNeeded && (image.naturalWidth > 0)) ){
loadCount--;
if( loadCount <= 0 ){
images.map(image => {
image.removeEventListener('load', onLoad);
image.removeEventListener('error', onError);
});
loaderImages.map(image => {
image.removeEventListener('load', onLoad);
image.removeEventListener('error', onError);
});
deferred.resolve((images.length === 1) ? images[0] : images);
}
} else {
waitForRepaint(() => { onLoad(e); });
}
}
function onError(error){
images.map(image => {
image.removeEventListener('load', onLoad);
image.removeEventListener('error', onError);
});
loaderImages.map(image => {
image.removeEventListener('load', onLoad);
image.removeEventListener('error', onError);
});
deferred.reject(error);
}
const
deferred = new Deferred(),
loaderImages = []
;
let loadCount = images.length;
deferred.provision = (images.length === 1) ? images[0] : images;
images.forEach(image => {
image.removeEventListener('load', onLoad);
image.addEventListener('load', onLoad);
image.removeEventListener('error', onError);
image.addEventListener('error', onError);
const
src = image.src,
parent = image.parentNode,
isPicture = isElement(image.parentNode) ? (parent.nodeName.toLowerCase() === 'picture') : false
;
assert(!isEmpty(src), `${MODULE_NAME}:${__methodName__} | image has no src`);
if( isPicture || !!image.complete ){
let img;
if( isPicture ){
if( window.respimage ){
window.respimage({elements : [parent]});
img = parent.querySelector('img');
} else if( window.picturefill ){
window.picturefill({elements : [parent]});
img = parent.querySelector('img');
} else {
img = image;
}
if( !!img.complete ){
img = new Image();
img.addEventListener('load', onLoad);
img.addEventListener('error', onError);
img.src = src;
loaderImages.push(img);
} else {
img.removeEventListener('load', onLoad);
img.addEventListener('load', onLoad);
img.removeEventListener('error', onError);
img.addEventListener('error', onError);
}
} else {
img = new Image();
img.addEventListener('load', onLoad);
img.addEventListener('error', onError);
img.src = src;
loaderImages.push(img);
}
}
});
return deferred;
}