PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 12.9.3
Jetpack – WP Security, Backup, Speed, & Growth v12.9.3
16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 13.9.2 14.0.1 14.1.1 14.2.2 14.3.1 All 501 releases
jetpack / modules / likes / queuehandler.js
queuehandler.js
472 lines 14.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /* global wpcom_reblog */
2
3 var jetpackLikesWidgetBatch = [];
4 var jetpackLikesMasterReady = false;
5
6 // Due to performance problems on pages with a large number of widget iframes that need to be loaded,
7 // we are limiting the processing at any instant to unloaded widgets that are currently in viewport,
8 // plus this constant that will allow processing of widgets above and bellow the current fold.
9 // This aim of it is to improve the UX and hide the transition from unloaded to loaded state from users.
10 var jetpackLikesLookAhead = 2000; // pixels
11
12 // Keeps track of loaded comment likes widget so we can unload them when they are scrolled out of view.
13 var jetpackCommentLikesLoadedWidgets = [];
14
15 var jetpackLikesDocReadyPromise = new Promise( resolve => {
16 if ( document.readyState !== 'loading' ) {
17 resolve();
18 } else {
19 window.addEventListener( 'DOMContentLoaded', () => resolve() );
20 }
21 } );
22
23 function JetpackLikesPostMessage( message, target ) {
24 if ( typeof message === 'string' ) {
25 try {
26 message = JSON.parse( message );
27 } catch ( e ) {
28 return;
29 }
30 }
31
32 if ( target && typeof target.postMessage === 'function' ) {
33 try {
34 target.postMessage(
35 JSON.stringify( {
36 type: 'likesMessage',
37 data: message,
38 } ),
39 '*'
40 );
41 } catch ( e ) {
42 return;
43 }
44 }
45 }
46
47 function JetpackLikesBatchHandler() {
48 const requests = [];
49 document.querySelectorAll( 'div.jetpack-likes-widget-unloaded' ).forEach( widget => {
50 if ( jetpackLikesWidgetBatch.indexOf( widget.id ) > -1 ) {
51 return;
52 }
53
54 if ( ! jetpackIsScrolledIntoView( widget ) ) {
55 return;
56 }
57
58 jetpackLikesWidgetBatch.push( widget.id );
59
60 var regex = /like-(post|comment)-wrapper-(\d+)-(\d+)-(\w+)/,
61 match = regex.exec( widget.id ),
62 info;
63
64 if ( ! match || match.length !== 5 ) {
65 return;
66 }
67
68 info = {
69 blog_id: match[ 2 ],
70 width: widget.width,
71 };
72
73 if ( 'post' === match[ 1 ] ) {
74 info.post_id = match[ 3 ];
75 } else if ( 'comment' === match[ 1 ] ) {
76 info.comment_id = match[ 3 ];
77 }
78
79 info.obj_id = match[ 4 ];
80
81 requests.push( info );
82 } );
83
84 if ( requests.length > 0 ) {
85 JetpackLikesPostMessage(
86 { event: 'initialBatch', requests: requests },
87 window.frames[ 'likes-master' ]
88 );
89 }
90 }
91
92 function JetpackLikesMessageListener( event ) {
93 let message = event && event.data;
94 if ( typeof message === 'string' ) {
95 try {
96 message = JSON.parse( message );
97 } catch ( err ) {
98 return;
99 }
100 }
101
102 const type = message && message.type;
103 const data = message && message.data;
104
105 if ( type !== 'likesMessage' || typeof data.event === 'undefined' ) {
106 return;
107 }
108
109 // We only allow messages from one origin
110 const allowedOrigin = 'https://widgets.wp.com';
111 if ( allowedOrigin !== event.origin ) {
112 return;
113 }
114
115 switch ( data.event ) {
116 case 'masterReady':
117 jetpackLikesDocReadyPromise.then( () => {
118 jetpackLikesMasterReady = true;
119
120 const stylesData = {
121 event: 'injectStyles',
122 };
123 const sdTextColor = document.querySelector( '.sd-text-color' );
124 const sdLinkColor = document.querySelector( '.sd-link-color' );
125 const sdTextColorStyles = ( sdTextColor && getComputedStyle( sdTextColor ) ) || {};
126 const sdLinkColorStyles = ( sdLinkColor && getComputedStyle( sdLinkColor ) ) || {};
127
128 if ( document.querySelectorAll( 'iframe.admin-bar-likes-widget' ).length > 0 ) {
129 JetpackLikesPostMessage( { event: 'adminBarEnabled' }, window.frames[ 'likes-master' ] );
130
131 const bgSource = document.querySelector(
132 '#wpadminbar .quicklinks li#wp-admin-bar-wpl-like > a'
133 );
134
135 const wpAdminBar = document.querySelector( '#wpadminbar' );
136
137 stylesData.adminBarStyles = {
138 background: bgSource && getComputedStyle( bgSource ).background,
139 isRtl: wpAdminBar && getComputedStyle( wpAdminBar ).direction === 'rtl',
140 };
141 }
142
143 // enable reblogs if we're on a single post page
144 if ( document.body.classList.contains( 'single' ) ) {
145 JetpackLikesPostMessage( { event: 'reblogsEnabled' }, window.frames[ 'likes-master' ] );
146 }
147
148 stylesData.textStyles = {
149 color: sdTextColorStyles[ 'color' ],
150 fontFamily: sdTextColorStyles[ 'font-family' ],
151 fontSize: sdTextColorStyles[ 'font-size' ],
152 direction: sdTextColorStyles[ 'direction' ],
153 fontWeight: sdTextColorStyles[ 'font-weight' ],
154 fontStyle: sdTextColorStyles[ 'font-style' ],
155 textDecoration: sdTextColorStyles[ 'text-decoration' ],
156 };
157
158 stylesData.linkStyles = {
159 color: sdLinkColorStyles[ 'color' ],
160 fontFamily: sdLinkColorStyles[ 'font-family' ],
161 fontSize: sdLinkColorStyles[ 'font-size' ],
162 textDecoration: sdLinkColorStyles[ 'text-decoration' ],
163 fontWeight: sdLinkColorStyles[ 'font-weight' ],
164 fontStyle: sdLinkColorStyles[ 'font-style' ],
165 };
166
167 JetpackLikesPostMessage( stylesData, window.frames[ 'likes-master' ] );
168
169 JetpackLikesBatchHandler();
170 } );
171
172 break;
173
174 case 'showLikeWidget': {
175 const placeholder = document.querySelector( `#${ data.id } .likes-widget-placeholder` );
176 if ( placeholder ) {
177 placeholder.style.display = 'none';
178 }
179 break;
180 }
181
182 case 'showCommentLikeWidget': {
183 const placeholder = document.querySelector( `#${ data.id } .likes-widget-placeholder` );
184 if ( placeholder ) {
185 placeholder.style.display = 'none';
186 }
187 break;
188 }
189
190 case 'killCommentLikes':
191 // If kill switch for comment likes is enabled remove all widgets wrappers and `Loading...` placeholders.
192 document
193 .querySelectorAll( '.jetpack-comment-likes-widget-wrapper' )
194 .forEach( wrapper => wrapper.remove() );
195 break;
196
197 case 'clickReblogFlair':
198 if ( wpcom_reblog && typeof wpcom_reblog.toggle_reblog_box_flair === 'function' ) {
199 wpcom_reblog.toggle_reblog_box_flair( data.obj_id );
200 }
201 break;
202
203 case 'showOtherGravatars': {
204 const container = document.querySelector( '#likes-other-gravatars' );
205 if ( ! container ) {
206 break;
207 }
208
209 const newLayout = container.classList.contains( 'wpl-new-layout' );
210
211 const list = container.querySelector( 'ul' );
212
213 container.style.display = 'none';
214 list.innerHTML = '';
215
216 if ( newLayout ) {
217 container
218 .querySelectorAll( '.likes-text span' )
219 .forEach( item => ( item.textContent = data.totalLikesLabel ) );
220 } else {
221 container
222 .querySelectorAll( '.likes-text span' )
223 .forEach( item => ( item.textContent = data.total ) );
224 }
225
226 ( data.likers || [] ).forEach( liker => {
227 if ( liker.profile_URL.substr( 0, 4 ) !== 'http' ) {
228 // We only display gravatars with http or https schema
229 return;
230 }
231
232 const element = document.createElement( 'li' );
233 if ( newLayout ) {
234 element.innerHTML = `
235 <a href="${ encodeURI( liker.profile_URL ) }" rel="nofollow" target="_parent" class="wpl-liker">
236 <img src="${ encodeURI( liker.avatar_URL ) }"
237 alt=""
238 style="width: 28px; height: 28px;" />
239 <span></span>
240 </a>
241 `;
242 } else {
243 element.innerHTML = `
244 <a href="${ encodeURI( liker.profile_URL ) }" rel="nofollow" target="_parent" class="wpl-liker">
245 <img src="${ encodeURI( liker.avatar_URL ) }"
246 alt=""
247 style="width: 30px; height: 30px; padding-right: 3px;" />
248 </a>
249 `;
250 }
251
252 list.append( element );
253
254 // Add some extra attributes through native methods, to ensure strings are sanitized.
255 element.classList.add( liker.css_class );
256 element.querySelector( 'img' ).alt = liker.name;
257 if ( newLayout ) {
258 element.querySelector( 'span' ).innerText = liker.name;
259 }
260 } );
261
262 const containerStyle = getComputedStyle( container );
263 const isRtl = containerStyle.direction === 'rtl';
264
265 const el = document.querySelector( `*[name='${ data.parent }']` );
266 const rect = el.getBoundingClientRect();
267 const win = el.ownerDocument.defaultView;
268 const offset = {
269 top: rect.top + win.pageYOffset,
270 left: rect.left + win.pageXOffset,
271 };
272
273 if ( newLayout ) {
274 container.style.top = offset.top + data.position.top - 1 + 'px';
275
276 if ( isRtl ) {
277 const visibleAvatarsCount = data && data.likers ? Math.min( data.likers.length, 5 ) : 0;
278 // 24px is the width of the avatar + 4px is the padding between avatars
279 container.style.left =
280 offset.left + data.position.left + 24 * visibleAvatarsCount + 4 + 'px';
281 container.style.transform = 'translateX(-100%)';
282 } else {
283 container.style.left = offset.left + data.position.left + 'px';
284 }
285 } else {
286 container.style.left = offset.left + data.position.left - 10 + 'px';
287 container.style.top = offset.top + data.position.top - 33 + 'px';
288 }
289
290 const rowLength = Math.floor( data.width / 37 );
291 let height = Math.ceil( data.likers.length / rowLength ) * 37 + 13;
292 if ( height > 204 ) {
293 height = 204;
294 }
295
296 if ( ! newLayout ) {
297 // Avatars + padding
298 const containerWidth = rowLength * 37 - 7;
299 container.style.height = height + 'px';
300 container.style.width = containerWidth + 'px';
301
302 const listWidth = rowLength * 37;
303 list.style.width = listWidth + 'px';
304
305 const scrollbarWidth = list.offsetWidth - list.clientWidth;
306 if ( scrollbarWidth > 0 ) {
307 container.style.width = containerWidth + scrollbarWidth + 'px';
308 list.style.width = listWidth + scrollbarWidth + 'px';
309 }
310 }
311
312 container.style.display = 'block';
313 }
314 }
315 }
316
317 window.addEventListener( 'message', JetpackLikesMessageListener );
318
319 document.addEventListener( 'click', e => {
320 const container = document.querySelector( '#likes-other-gravatars' );
321
322 if ( container && ! container.contains( e.target ) ) {
323 container.style.display = 'none';
324 }
325 } );
326
327 function JetpackLikesWidgetQueueHandler() {
328 var wrapperID;
329
330 if ( ! jetpackLikesMasterReady ) {
331 setTimeout( JetpackLikesWidgetQueueHandler, 500 );
332 return;
333 }
334
335 // Restore widgets to initial unloaded state when they are scrolled out of view.
336 jetpackUnloadScrolledOutWidgets();
337
338 var unloadedWidgetsInView = jetpackGetUnloadedWidgetsInView();
339
340 if ( unloadedWidgetsInView.length > 0 ) {
341 // Grab any unloaded widgets for a batch request
342 JetpackLikesBatchHandler();
343 }
344
345 for ( var i = 0, length = unloadedWidgetsInView.length; i <= length - 1; i++ ) {
346 wrapperID = unloadedWidgetsInView[ i ].id;
347
348 if ( ! wrapperID ) {
349 continue;
350 }
351
352 jetpackLoadLikeWidgetIframe( wrapperID );
353 }
354 }
355
356 function jetpackLoadLikeWidgetIframe( wrapperID ) {
357 if ( typeof wrapperID === 'undefined' ) {
358 return;
359 }
360
361 const wrapper = document.querySelector( '#' + wrapperID );
362 wrapper.querySelectorAll( 'iframe' ).forEach( iFrame => iFrame.remove() );
363
364 const placeholder = wrapper.querySelector( '.likes-widget-placeholder' );
365
366 // Post like iframe
367 if ( placeholder && placeholder.classList.contains( 'post-likes-widget-placeholder' ) ) {
368 const postLikesFrame = document.createElement( 'iframe' );
369
370 postLikesFrame.classList.add( 'post-likes-widget', 'jetpack-likes-widget' );
371 postLikesFrame.name = wrapper.dataset.name;
372 postLikesFrame.src = wrapper.dataset.src;
373 postLikesFrame.height = '55px';
374 postLikesFrame.width = '100%';
375 postLikesFrame.frameBorder = '0';
376 postLikesFrame.scrolling = 'no';
377 postLikesFrame.title = wrapper.dataset.title;
378
379 placeholder.after( postLikesFrame );
380 }
381
382 // Comment like iframe
383 if ( placeholder.classList.contains( 'comment-likes-widget-placeholder' ) ) {
384 const commentLikesFrame = document.createElement( 'iframe' );
385
386 commentLikesFrame.class = 'comment-likes-widget-frame jetpack-likes-widget-frame';
387 commentLikesFrame.name = wrapper.dataset.name;
388 commentLikesFrame.src = wrapper.dataset.src;
389 commentLikesFrame.height = '18px';
390 commentLikesFrame.width = '100%';
391 commentLikesFrame.frameBorder = '0';
392 commentLikesFrame.scrolling = 'no';
393
394 wrapper.querySelector( '.comment-like-feedback' ).after( commentLikesFrame );
395
396 jetpackCommentLikesLoadedWidgets.push( commentLikesFrame );
397 }
398
399 wrapper.classList.remove( 'jetpack-likes-widget-unloaded' );
400 wrapper.classList.add( 'jetpack-likes-widget-loading' );
401
402 wrapper.querySelector( 'iframe' ).addEventListener( 'load', e => {
403 JetpackLikesPostMessage(
404 { event: 'loadLikeWidget', name: e.target.name, width: e.target.width },
405 window.frames[ 'likes-master' ]
406 );
407
408 wrapper.classList.remove( 'jetpack-likes-widget-loading' );
409 wrapper.classList.add( 'jetpack-likes-widget-loaded' );
410 } );
411 }
412
413 function jetpackGetUnloadedWidgetsInView() {
414 const unloadedWidgets = document.querySelectorAll( 'div.jetpack-likes-widget-unloaded' );
415
416 return [ ...unloadedWidgets ].filter( item => jetpackIsScrolledIntoView( item ) );
417 }
418
419 function jetpackIsScrolledIntoView( element ) {
420 const top = element.getBoundingClientRect().top;
421 const bottom = element.getBoundingClientRect().bottom;
422
423 // Allow some slack above and bellow the fold with jetpackLikesLookAhead,
424 // with the aim of hiding the transition from unloaded to loaded widget from users.
425 return top + jetpackLikesLookAhead >= 0 && bottom <= window.innerHeight + jetpackLikesLookAhead;
426 }
427
428 function jetpackUnloadScrolledOutWidgets() {
429 for ( let i = jetpackCommentLikesLoadedWidgets.length - 1; i >= 0; i-- ) {
430 const currentWidgetIframe = jetpackCommentLikesLoadedWidgets[ i ];
431
432 if ( ! jetpackIsScrolledIntoView( currentWidgetIframe ) ) {
433 const widgetWrapper =
434 currentWidgetIframe &&
435 currentWidgetIframe.parentElement &&
436 currentWidgetIframe.parentElement.parentElement;
437
438 // Restore parent class to 'unloaded' so this widget can be picked up by queue manager again if needed.
439 widgetWrapper.classList.remove( 'jetpack-likes-widget-loaded' );
440 widgetWrapper.classList.remove( 'jetpack-likes-widget-loading' );
441 widgetWrapper.classList.add( 'jetpack-likes-widget-unloaded' );
442
443 // Bring back the loading placeholder into view.
444 widgetWrapper
445 .querySelectorAll( '.comment-likes-widget-placeholder' )
446 .forEach( item => ( item.style.display = 'block' ) );
447
448 // Remove it from the list of loaded widgets.
449 jetpackCommentLikesLoadedWidgets.splice( i, 1 );
450
451 // Remove comment like widget iFrame.
452 currentWidgetIframe.remove();
453 }
454 }
455 }
456
457 var jetpackWidgetsDelayedExec = function ( after, fn ) {
458 var timer;
459 return function () {
460 clearTimeout( timer );
461 timer = setTimeout( fn, after );
462 };
463 };
464
465 var jetpackOnScrollStopped = jetpackWidgetsDelayedExec( 250, JetpackLikesWidgetQueueHandler );
466
467 // Load initial batch of widgets, prior to any scrolling events.
468 JetpackLikesWidgetQueueHandler();
469
470 // Add event listener to execute queue handler after scroll.
471 window.addEventListener( 'scroll', jetpackOnScrollStopped, true );
472