Skip to content

Commit 8d4fc36

Browse files
Merge pull request #221 from cloudy408/responsive-plugin-behavior-2
feat: added functionality to avoid rendering responsive plugin when placeholder is added
2 parents 4ba4d98 + 2024517 commit 8d4fc36

File tree

9 files changed

+97
-10
lines changed

9 files changed

+97
-10
lines changed

packages/html/__tests__/HtmlImageLayer.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,4 +127,32 @@ describe('HtmlImageLayer tests', function () {
127127
// test that the src which set by HtmlImageLayer contains last character "B" which is the character of placeholder plugin
128128
expect(imgSetAttributeSpy.mock.calls[0][1]).toEqualAnalyticsToken('BAXAABABB');
129129
});
130+
131+
it('should verfiy no responsive image request is fired with placeholder plugin', async function () {
132+
const OriginalImage = Image;
133+
// mocking Image constructor in order to simulate firing 'load' event
134+
jest.spyOn(global, "Image").mockImplementation(() => {
135+
const img = new OriginalImage();
136+
setTimeout(() => {
137+
img.dispatchEvent(new Event("load"));
138+
}, 10)
139+
return img;
140+
141+
})
142+
const parentElement = document.createElement('div');
143+
const img = document.createElement('img');
144+
parentElement.append(img);
145+
const imgSrcSpy = jest.spyOn(img, 'src', 'set');
146+
const imgSetAttributeSpy = jest.spyOn(img, 'setAttribute');
147+
new HtmlImageLayer(img, cldImage, [responsive({steps: 200}),placeholder()], sdkAnalyticsTokens);
148+
await flushPromises();
149+
expect(imgSrcSpy).toHaveBeenCalledTimes(1);
150+
// test that the initial src is set to a token contains last character "B" which is the character of placeholder plugin
151+
const imgSrcSpyAnalyticsToken = imgSrcSpy.mock.calls[0][0];
152+
expect(imgSrcSpyAnalyticsToken).toEqualAnalyticsToken('BAXAABABB');
153+
await flushPromises();
154+
await flushPromises();
155+
//TODO we want to check the second image that is loaded for the presence of a w_ paramter
156+
expect(img.src).toBe("abc");
157+
});
130158
});

packages/html/src/plugins/accessibility.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export function accessibility({mode = 'darkmode'}: { mode?: string; }={}): Plugi
2222
* @param pluginCloudinaryImage {CloudinaryImage}
2323
* @param htmlPluginState {htmlPluginState} Holds cleanup callbacks and event subscriptions.
2424
*/
25-
export function accessibilityPlugin(mode: AccessibilityMode, element: HTMLImageElement, pluginCloudinaryImage: CloudinaryImage, htmlPluginState: HtmlPluginState): Promise<PluginResponse> | boolean {
25+
export function accessibilityPlugin(mode: AccessibilityMode, element: HTMLImageElement, pluginCloudinaryImage: CloudinaryImage, htmlPluginState: HtmlPluginState, plugins?: Plugin[]): Promise<PluginResponse> | boolean {
2626
if(isBrowser()){
2727

2828
if(!isImage(element)) return;

packages/html/src/plugins/lazyload.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export function lazyload({rootMargin='0px', threshold=0.1}:{rootMargin?: string,
2929
* @param cloudinaryImage {CloudinaryImage}
3030
* @param htmlPluginState {HtmlPluginState} Holds cleanup callbacks and event subscriptions.
3131
*/
32-
function lazyloadPlugin(rootMargin='0px', threshold=0.1 , element: HTMLImageElement | HTMLVideoElement, cloudinaryImage: CloudinaryImage, htmlPluginState: HtmlPluginState): Promise<PluginResponse> | boolean {
32+
function lazyloadPlugin(rootMargin='0px', threshold=0.1 , element: HTMLImageElement | HTMLVideoElement, cloudinaryImage: CloudinaryImage, htmlPluginState: HtmlPluginState, plugins?: Plugin[]): Promise<PluginResponse> | boolean {
3333
// if SSR skip plugin
3434
if(!isBrowser()) return false;
3535

packages/html/src/plugins/placeholder.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ export function placeholder({mode='vectorize'}:{mode?: string}={}): Plugin{
2828
* @param htmlPluginState {htmlPluginState} Holds cleanup callbacks and event subscriptions.
2929
* @param baseAnalyticsOptions {BaseAnalyticsOptions} analytics options for the url to be created
3030
*/
31-
function placeholderPlugin(mode: PlaceholderMode, element: HTMLImageElement, pluginCloudinaryImage: CloudinaryImage, htmlPluginState: HtmlPluginState, baseAnalyticsOptions?: BaseAnalyticsOptions): Promise<PluginResponse> | boolean {
31+
// TODO: Optionally we might want to hold of with rendering
32+
// Maybe there is something in the responsive plugin already that should be moved here too?
33+
function placeholderPlugin(mode: PlaceholderMode, element: HTMLImageElement, pluginCloudinaryImage: CloudinaryImage, htmlPluginState: HtmlPluginState, baseAnalyticsOptions?: BaseAnalyticsOptions, plugins?: Plugin[]): Promise<PluginResponse> | boolean {
3234
// @ts-ignore
3335
// If we're using an invalid mode, we default to vectorize
3436
if(!PLACEHOLDER_IMAGE_OPTIONS[mode]){

packages/html/src/plugins/responsive.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ export function responsive({steps}:{steps?: number | number[]}={}): Plugin{
2929
* @param htmlPluginState {HtmlPluginState} holds cleanup callbacks and event subscriptions
3030
* @param analyticsOptions {BaseAnalyticsOptions} analytics options for the url to be created
3131
*/
32-
function responsivePlugin(steps?: number | number[], element?:HTMLImageElement, responsiveImage?: CloudinaryImage, htmlPluginState?: HtmlPluginState, baseAnalyticsOptions?: BaseAnalyticsOptions): Promise<PluginResponse> | boolean {
32+
function responsivePlugin(steps?: number | number[], element?:HTMLImageElement, responsiveImage?: CloudinaryImage, htmlPluginState?: HtmlPluginState, baseAnalyticsOptions?: BaseAnalyticsOptions, plugins?: Plugin[]): Promise<PluginResponse> | boolean {
33+
34+
console.debug(plugins);
3335

3436
if(!isBrowser()) return true;
3537

@@ -46,7 +48,28 @@ function responsivePlugin(steps?: number | number[], element?:HTMLImageElement,
4648
// Use a tagged generic action that can be later searched and replaced.
4749
responsiveImage.addAction(new Action().setActionTag('responsive'));
4850
// Immediately run the resize plugin, ensuring that first render gets a responsive image.
49-
onResize(steps, element, responsiveImage, analyticsOptions);
51+
// If we disable initial run entirely the placeholder will load non-resposive image
52+
53+
const regex = /.*placeholder.*/gm;
54+
let shouldRunImmediately = true // TODO: logic to test if there is a placeholder plugin
55+
56+
plugins.forEach((p)=>{
57+
if (regex.exec(p.name) !== null){
58+
console.debug("found the placeholder plugin!!!");
59+
shouldRunImmediately = false;
60+
}
61+
});
62+
63+
64+
console.debug("analyticsOptions: ",analyticsOptions);
65+
console.debug("analyticsOptinos name: ",analyticsOptions.trackedAnalytics);
66+
67+
if(shouldRunImmediately) {
68+
onResize(steps, element, responsiveImage, analyticsOptions);
69+
} else {
70+
// Probably this has to run on else, see comments in line 83
71+
updateByContainerWidth(steps, element, responsiveImage);
72+
}
5073

5174
let resizeRef: any;
5275
htmlPluginState.pluginEventSubscription.push(()=>{
@@ -67,8 +90,20 @@ function responsivePlugin(steps?: number | number[], element?:HTMLImageElement,
6790
* @param analyticsOptions {AnalyticsOptions} analytics options for the url to be created
6891
*/
6992
function onResize(steps?: number | number[], element?:HTMLImageElement, responsiveImage?: CloudinaryImage, analyticsOptions?: AnalyticsOptions){
93+
7094
updateByContainerWidth(steps, element, responsiveImage);
95+
// TODO: 1. Responsive should not load the image if placeholder is running next
96+
// It has to know, the placeholder is in the plugins
97+
// A. Get plugins as a new fifth argument of the `responsivePlugin` function
98+
// B. Loop over plugins and check if any of plugin.name is equal to "bound placeholderPlugin"
99+
100+
// If we disable each onResize then placeholder will render original image (!)
101+
// So this cannot be conditional because we want run it always on window resize later
71102
element.src = responsiveImage.toURL(analyticsOptions);
103+
104+
// So the magic to make sure placeholder loads large image with responsive
105+
// Is done by the updateByContainerWidth function call
106+
// ... and we might need to make sure it's called in line 53 if shouldRunImmediately is false
72107
}
73108

74109
/**

packages/html/src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
ITrackedPropertiesThroughAnalytics
55
} from "@cloudinary/url-gen/sdkAnalytics/interfaces/ITrackedPropertiesThroughAnalytics";
66

7-
export type Plugin = (element: HTMLImageElement|HTMLVideoElement, cloudinaryImage: CloudinaryImage, htmlPluginState?: HtmlPluginState, baseAnalyticsOptions?: BaseAnalyticsOptions) => Promise<PluginResponse>;
7+
export type Plugin = (element: HTMLImageElement|HTMLVideoElement, cloudinaryImage: CloudinaryImage, htmlPluginState?: HtmlPluginState, baseAnalyticsOptions?: BaseAnalyticsOptions,plugins?:Plugins) => Promise<PluginResponse>;
88

99
export type Plugins = Plugin[];
1010

packages/html/src/utils/render.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { lazyload } from 'plugins/lazyload';
12
import {Plugins, HtmlPluginState, BaseAnalyticsOptions, PluginResponse} from '../types'
23
import {CloudinaryVideo, CloudinaryImage} from "@cloudinary/url-gen";
34

@@ -14,10 +15,24 @@ export async function render(element: HTMLImageElement | HTMLVideoElement, plugi
1415
if (plugins === undefined) return;
1516
let response: PluginResponse;
1617
for (let i = 0; i < plugins.length; i++) {
17-
response = await plugins[i](element, pluginCloudinaryAsset, pluginState, analyticsOptions);
18+
// TODO: We have to pass all plugins to each plugin (so that each can determine what to do)
19+
// 1. Pass plugins as a fifth parameter below (after analyticsOptions)
20+
//
21+
// 2. Inside each plugin we can use `name` property of the plugin functions to see what was added
22+
//
23+
// Example given the plugins look like below:
24+
// const plugins = [lazyload(), responsive()]
25+
// Then if you console log plugins[0].name you should get 'bound lazyloadPlugin'
26+
// plugins[0].name === 'bound lazyloadPlugin'
27+
const pluginResponse = await plugins[i](element, pluginCloudinaryAsset, pluginState, analyticsOptions, plugins);
1828
if (response === 'canceled') {
1929
break;
2030
}
31+
if(typeof pluginResponse === 'object') {
32+
response = {...response, ...pluginResponse};
33+
} else {
34+
response = pluginResponse
35+
}
2136
}
2237
if (response !== 'canceled') {
2338
return response;

playground/html/index.html

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,15 @@
44
<meta charset="UTF-8" />
55
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
66
<title>@Cloudinary/html Playground</title>
7+
<style>
8+
img {
9+
width: 100%;
10+
}
11+
</style>
712
</head>
813
<body>
9-
<div id="app"></div>
14+
<h1>Test Environment</h1>
15+
<div id="app" style="width:500px"></div>
1016
<script type="module" src="/src/main.ts"></script>
1117
</body>
1218
</html>

playground/html/src/main.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import {CloudinaryImage} from "@cloudinary/url-gen";
22
import {HtmlImageLayer} from "@cloudinary/html";
3-
import {responsive} from "@cloudinary/html";
3+
import {responsive,placeholder} from "@cloudinary/html";
44

55
const app = document.querySelector<HTMLDivElement>('#app');
66
if (app) {
77
const img = document.createElement('img');
88
const cldImg = new CloudinaryImage('sample', {cloudName: 'demo'});
9+
console.debug("img.URL: ", cldImg.toURL());
910
app.append(img);
10-
new HtmlImageLayer(img, cldImg, [responsive({steps: [100]})], {
11+
new HtmlImageLayer(img, cldImg, [responsive({steps: 200}),placeholder()], {
1112
sdkCode: 'X',
1213
sdkSemver: '1.0.0',
1314
techVersion: '2.0.0'

0 commit comments

Comments
 (0)