diff --git a/packages/html/__tests__/HtmlImageLayer.test.ts b/packages/html/__tests__/HtmlImageLayer.test.ts index 66f49ed7..cb013f60 100644 --- a/packages/html/__tests__/HtmlImageLayer.test.ts +++ b/packages/html/__tests__/HtmlImageLayer.test.ts @@ -127,4 +127,32 @@ describe('HtmlImageLayer tests', function () { // test that the src which set by HtmlImageLayer contains last character "B" which is the character of placeholder plugin expect(imgSetAttributeSpy.mock.calls[0][1]).toEqualAnalyticsToken('BAXAABABB'); }); + + it('should verfiy no responsive image request is fired with placeholder plugin', async function () { + const OriginalImage = Image; + // mocking Image constructor in order to simulate firing 'load' event + jest.spyOn(global, "Image").mockImplementation(() => { + const img = new OriginalImage(); + setTimeout(() => { + img.dispatchEvent(new Event("load")); + }, 10) + return img; + + }) + const parentElement = document.createElement('div'); + const img = document.createElement('img'); + parentElement.append(img); + const imgSrcSpy = jest.spyOn(img, 'src', 'set'); + const imgSetAttributeSpy = jest.spyOn(img, 'setAttribute'); + new HtmlImageLayer(img, cldImage, [responsive({steps: 200}),placeholder()], sdkAnalyticsTokens); + await flushPromises(); + expect(imgSrcSpy).toHaveBeenCalledTimes(1); + // test that the initial src is set to a token contains last character "B" which is the character of placeholder plugin + const imgSrcSpyAnalyticsToken = imgSrcSpy.mock.calls[0][0]; + expect(imgSrcSpyAnalyticsToken).toEqualAnalyticsToken('BAXAABABB'); + await flushPromises(); + await flushPromises(); + //TODO we want to check the second image that is loaded for the presence of a w_ paramter + expect(img.src).toBe("abc"); + }); }); diff --git a/packages/html/src/plugins/accessibility.ts b/packages/html/src/plugins/accessibility.ts index db6862f0..ea3ceb7a 100644 --- a/packages/html/src/plugins/accessibility.ts +++ b/packages/html/src/plugins/accessibility.ts @@ -22,7 +22,7 @@ export function accessibility({mode = 'darkmode'}: { mode?: string; }={}): Plugi * @param pluginCloudinaryImage {CloudinaryImage} * @param htmlPluginState {htmlPluginState} Holds cleanup callbacks and event subscriptions. */ -export function accessibilityPlugin(mode: AccessibilityMode, element: HTMLImageElement, pluginCloudinaryImage: CloudinaryImage, htmlPluginState: HtmlPluginState): Promise | boolean { +export function accessibilityPlugin(mode: AccessibilityMode, element: HTMLImageElement, pluginCloudinaryImage: CloudinaryImage, htmlPluginState: HtmlPluginState, plugins?: Plugin[]): Promise | boolean { if(isBrowser()){ if(!isImage(element)) return; diff --git a/packages/html/src/plugins/lazyload.ts b/packages/html/src/plugins/lazyload.ts index c3a5d1c8..a0ba9296 100644 --- a/packages/html/src/plugins/lazyload.ts +++ b/packages/html/src/plugins/lazyload.ts @@ -29,7 +29,7 @@ export function lazyload({rootMargin='0px', threshold=0.1}:{rootMargin?: string, * @param cloudinaryImage {CloudinaryImage} * @param htmlPluginState {HtmlPluginState} Holds cleanup callbacks and event subscriptions. */ -function lazyloadPlugin(rootMargin='0px', threshold=0.1 , element: HTMLImageElement | HTMLVideoElement, cloudinaryImage: CloudinaryImage, htmlPluginState: HtmlPluginState): Promise | boolean { +function lazyloadPlugin(rootMargin='0px', threshold=0.1 , element: HTMLImageElement | HTMLVideoElement, cloudinaryImage: CloudinaryImage, htmlPluginState: HtmlPluginState, plugins?: Plugin[]): Promise | boolean { // if SSR skip plugin if(!isBrowser()) return false; diff --git a/packages/html/src/plugins/placeholder.ts b/packages/html/src/plugins/placeholder.ts index 3754e11c..d817b90d 100644 --- a/packages/html/src/plugins/placeholder.ts +++ b/packages/html/src/plugins/placeholder.ts @@ -28,7 +28,9 @@ export function placeholder({mode='vectorize'}:{mode?: string}={}): Plugin{ * @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 | boolean { +// TODO: Optionally we might want to hold of with rendering +// Maybe there is something in the responsive plugin already that should be moved here too? +function placeholderPlugin(mode: PlaceholderMode, element: HTMLImageElement, pluginCloudinaryImage: CloudinaryImage, htmlPluginState: HtmlPluginState, baseAnalyticsOptions?: BaseAnalyticsOptions, plugins?: Plugin[]): Promise | boolean { // @ts-ignore // If we're using an invalid mode, we default to vectorize if(!PLACEHOLDER_IMAGE_OPTIONS[mode]){ diff --git a/packages/html/src/plugins/responsive.ts b/packages/html/src/plugins/responsive.ts index 24092aca..f6b55efe 100644 --- a/packages/html/src/plugins/responsive.ts +++ b/packages/html/src/plugins/responsive.ts @@ -29,7 +29,9 @@ export function responsive({steps}:{steps?: number | number[]}={}): Plugin{ * @param htmlPluginState {HtmlPluginState} holds cleanup callbacks and event subscriptions * @param analyticsOptions {BaseAnalyticsOptions} analytics options for the url to be created */ -function responsivePlugin(steps?: number | number[], element?:HTMLImageElement, responsiveImage?: CloudinaryImage, htmlPluginState?: HtmlPluginState, baseAnalyticsOptions?: BaseAnalyticsOptions): Promise | boolean { +function responsivePlugin(steps?: number | number[], element?:HTMLImageElement, responsiveImage?: CloudinaryImage, htmlPluginState?: HtmlPluginState, baseAnalyticsOptions?: BaseAnalyticsOptions, plugins?: Plugin[]): Promise | boolean { + + console.debug(plugins); if(!isBrowser()) return true; @@ -46,7 +48,28 @@ function responsivePlugin(steps?: number | number[], element?:HTMLImageElement, // Use a tagged generic action that can be later searched and replaced. responsiveImage.addAction(new Action().setActionTag('responsive')); // Immediately run the resize plugin, ensuring that first render gets a responsive image. - onResize(steps, element, responsiveImage, analyticsOptions); + // If we disable initial run entirely the placeholder will load non-resposive image + + const regex = /.*placeholder.*/gm; + let shouldRunImmediately = true // TODO: logic to test if there is a placeholder plugin + + plugins.forEach((p)=>{ + if (regex.exec(p.name) !== null){ + console.debug("found the placeholder plugin!!!"); + shouldRunImmediately = false; + } + }); + + + console.debug("analyticsOptions: ",analyticsOptions); + console.debug("analyticsOptinos name: ",analyticsOptions.trackedAnalytics); + + if(shouldRunImmediately) { + onResize(steps, element, responsiveImage, analyticsOptions); + } else { + // Probably this has to run on else, see comments in line 83 + updateByContainerWidth(steps, element, responsiveImage); + } let resizeRef: any; htmlPluginState.pluginEventSubscription.push(()=>{ @@ -67,8 +90,20 @@ function responsivePlugin(steps?: number | number[], element?:HTMLImageElement, * @param analyticsOptions {AnalyticsOptions} analytics options for the url to be created */ function onResize(steps?: number | number[], element?:HTMLImageElement, responsiveImage?: CloudinaryImage, analyticsOptions?: AnalyticsOptions){ + updateByContainerWidth(steps, element, responsiveImage); + // TODO: 1. Responsive should not load the image if placeholder is running next + // It has to know, the placeholder is in the plugins + // A. Get plugins as a new fifth argument of the `responsivePlugin` function + // B. Loop over plugins and check if any of plugin.name is equal to "bound placeholderPlugin" + + // If we disable each onResize then placeholder will render original image (!) + // So this cannot be conditional because we want run it always on window resize later element.src = responsiveImage.toURL(analyticsOptions); + + // So the magic to make sure placeholder loads large image with responsive + // Is done by the updateByContainerWidth function call + // ... and we might need to make sure it's called in line 53 if shouldRunImmediately is false } /** diff --git a/packages/html/src/types.ts b/packages/html/src/types.ts index 73314a18..c562be0c 100644 --- a/packages/html/src/types.ts +++ b/packages/html/src/types.ts @@ -4,7 +4,7 @@ import { ITrackedPropertiesThroughAnalytics } from "@cloudinary/url-gen/sdkAnalytics/interfaces/ITrackedPropertiesThroughAnalytics"; -export type Plugin = (element: HTMLImageElement|HTMLVideoElement, cloudinaryImage: CloudinaryImage, htmlPluginState?: HtmlPluginState, baseAnalyticsOptions?: BaseAnalyticsOptions) => Promise; +export type Plugin = (element: HTMLImageElement|HTMLVideoElement, cloudinaryImage: CloudinaryImage, htmlPluginState?: HtmlPluginState, baseAnalyticsOptions?: BaseAnalyticsOptions,plugins?:Plugins) => Promise; export type Plugins = Plugin[]; diff --git a/packages/html/src/utils/render.ts b/packages/html/src/utils/render.ts index fc8825d5..246d8669 100644 --- a/packages/html/src/utils/render.ts +++ b/packages/html/src/utils/render.ts @@ -1,3 +1,4 @@ +import { lazyload } from 'plugins/lazyload'; import {Plugins, HtmlPluginState, BaseAnalyticsOptions, PluginResponse} from '../types' import {CloudinaryVideo, CloudinaryImage} from "@cloudinary/url-gen"; @@ -14,10 +15,24 @@ export async function render(element: HTMLImageElement | HTMLVideoElement, plugi if (plugins === undefined) return; let response: PluginResponse; for (let i = 0; i < plugins.length; i++) { - response = await plugins[i](element, pluginCloudinaryAsset, pluginState, analyticsOptions); + // TODO: We have to pass all plugins to each plugin (so that each can determine what to do) + // 1. Pass plugins as a fifth parameter below (after analyticsOptions) + // + // 2. Inside each plugin we can use `name` property of the plugin functions to see what was added + // + // Example given the plugins look like below: + // const plugins = [lazyload(), responsive()] + // Then if you console log plugins[0].name you should get 'bound lazyloadPlugin' + // plugins[0].name === 'bound lazyloadPlugin' + const pluginResponse = await plugins[i](element, pluginCloudinaryAsset, pluginState, analyticsOptions, plugins); if (response === 'canceled') { break; } + if(typeof pluginResponse === 'object') { + response = {...response, ...pluginResponse}; + } else { + response = pluginResponse + } } if (response !== 'canceled') { return response; diff --git a/playground/html/index.html b/playground/html/index.html index 0f3224ad..bb633a43 100644 --- a/playground/html/index.html +++ b/playground/html/index.html @@ -4,9 +4,15 @@ @Cloudinary/html Playground + -
+

Test Environment

+
diff --git a/playground/html/src/main.ts b/playground/html/src/main.ts index 8977fcf2..26229885 100644 --- a/playground/html/src/main.ts +++ b/playground/html/src/main.ts @@ -1,13 +1,14 @@ import {CloudinaryImage} from "@cloudinary/url-gen"; import {HtmlImageLayer} from "@cloudinary/html"; -import {responsive} from "@cloudinary/html"; +import {responsive,placeholder} from "@cloudinary/html"; const app = document.querySelector('#app'); if (app) { const img = document.createElement('img'); const cldImg = new CloudinaryImage('sample', {cloudName: 'demo'}); + console.debug("img.URL: ", cldImg.toURL()); app.append(img); - new HtmlImageLayer(img, cldImg, [responsive({steps: [100]})], { + new HtmlImageLayer(img, cldImg, [responsive({steps: 200}),placeholder()], { sdkCode: 'X', sdkSemver: '1.0.0', techVersion: '2.0.0'