import cloneDeep from 'lodash.clonedeep'
import {CloudinaryImage} from "@cloudinary/url-gen/assets/CloudinaryImage";
import {Plugin, HtmlPluginState, BaseAnalyticsOptions, PluginResponse} from "../types";
import {PLACEHOLDER_IMAGE_OPTIONS, singleTransparentPixel} from '../utils/internalConstants';
import {PlaceholderMode} from '../types';
import {isBrowser} from "../utils/isBrowser";
import {Action} from "@cloudinary/url-gen/internal/Action";
import {isImage} from "../utils/isImage";
import {getAnalyticsOptions} from "../utils/analytics";
/**
* @namespace
* @description Displays a placeholder image until the original image loads.
* @param mode {PlaceholderMode} The type of placeholder image to display. Possible modes: 'vectorize' | 'pixelate' | 'blur' | 'predominant-color'. Default: 'vectorize'.
* @return {Plugin}
* @example <caption>NOTE: The following is in React. For further examples, see the Packages tab.</caption>
* <AdvancedImage cldImg={img} plugins={[placeholder({mode: 'blur'})]} />
*/
export function placeholder({mode='vectorize'}:{mode?: string}={}): Plugin{
return placeholderPlugin.bind(null, mode);
}
/**
* @description Placeholder plugin
* @param mode {PlaceholderMode} The type of placeholder image to display. Possible modes: 'vectorize' | 'pixelate' | 'blur' | 'predominant-color'. Default: 'vectorize'.
* @param element {HTMLImageElement} The image element.
* @param pluginCloudinaryImage {CloudinaryImage}
* @param htmlPluginState {htmlPluginState} Holds cleanup callbacks and event subscriptions.
* @param baseAnalyticsOptions {BaseAnalyticsOptions} analytics options for the url to be created
*/
function placeholderPlugin(mode: PlaceholderMode, element: HTMLImageElement, pluginCloudinaryImage: CloudinaryImage, htmlPluginState: HtmlPluginState, baseAnalyticsOptions?: BaseAnalyticsOptions): Promise<PluginResponse> | boolean {
// @ts-ignore
// If we're using an invalid mode, we default to vectorize
if(!PLACEHOLDER_IMAGE_OPTIONS[mode]){
mode = 'vectorize'
}
// A placeholder mode maps to an array of transformations
const PLACEHOLDER_ACTIONS = PLACEHOLDER_IMAGE_OPTIONS[mode].actions;
// Before proceeding, clone the original image
// We clone because we don't want to pollute the state of the image
// Future renders (after the placeholder is loaded) should not load placeholder transformations
const placeholderClonedImage = cloneDeep(pluginCloudinaryImage);
//appends a placeholder transformation on the clone
// @ts-ignore
PLACEHOLDER_ACTIONS.forEach(function(transformation:Action){
placeholderClonedImage.addAction(transformation);
});
if(!isBrowser()) {
// in SSR, we copy the transformations of the clone to the user provided CloudinaryImage
// We return here, since we don't have HTML elements to work with.
pluginCloudinaryImage.transformation = placeholderClonedImage.transformation;
return true;
}
// Client side rendering, if an image was not provided we don't perform any action
if(!isImage(element)) return;
// For the cloned placeholder image, we remove the responsive action.
// There's no need to load e_pixelate,w_{responsive} beacuse that image is temporary as-is
// and it just causes another image to load.
// This also means that the de-facto way to use responsive in SSR is WITH placeholder.
// This also means that the user must provide dimensions for the responsive plugin on the img tag.
placeholderClonedImage.transformation.actions.forEach((action, index) => {
if (action instanceof Action && action.getActionTag() === 'responsive') {
delete placeholderClonedImage.transformation.actions[index];
}
});
const analyticsOptions = getAnalyticsOptions(baseAnalyticsOptions, {placeholder: true});
// Set the SRC of the imageElement to the URL of the placeholder Image
element.src = placeholderClonedImage.toURL(analyticsOptions);
//Fallback, if placeholder errors, load a single transparent pixel
element.onerror = () => {
element.src = singleTransparentPixel;
};
/*
* This plugin loads two images:
* - The first image is loaded as a placeholder
* - The second image is loaded after the placeholder is loaded
*
* Placeholder image loads first. Once it loads, the promise is resolved and the
* larger image will load. Once the larger image loads, promised and plugin is resolved.
*/
return new Promise((resolve: any) => {
element.onload = () => {
resolve();
};
}).then(()=>{
return new Promise((resolve: any) => {
htmlPluginState.cleanupCallbacks.push(()=>{
element.src = singleTransparentPixel;
resolve('canceled');
});
// load image once placeholder is done loading
const largeImage = new Image();
largeImage.src = pluginCloudinaryImage.toURL(analyticsOptions);
largeImage.onload = () => {
resolve({placeholder: true});
};
// image does not load, resolve
largeImage.onerror = () => {
resolve({placeholder: true});
};
});
});
}