PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.2
Jetpack – WP Security, Backup, Speed, & Growth v16.2
16.2 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 All 502 releases
jetpack / modules / likes / queuehandler.js

queuehandler.js in Jetpack – WP Security, Backup, Speed, & Growth 16.2, at modules/likes/queuehandler.js

479 lines 14.6 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 {
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 {
42 // Ignore error
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 {
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 // enable reblogs if we're on a single post page
129 if ( document.body.classList.contains( 'single' ) ) {
130 JetpackLikesPostMessage( { event: 'reblogsEnabled' }, window.frames[ 'likes-master' ] );
131 }
132
133 stylesData.textStyles = {
134 color: sdTextColorStyles.color,
135 fontFamily: sdTextColorStyles[ 'font-family' ],
136 fontSize: sdTextColorStyles[ 'font-size' ],
137 direction: sdTextColorStyles.direction,
138 fontWeight: sdTextColorStyles[ 'font-weight' ],
139 fontStyle: sdTextColorStyles[ 'font-style' ],
140 textDecoration: sdTextColorStyles[ 'text-decoration' ],
141 };
142
143 stylesData.linkStyles = {
144 color: sdLinkColorStyles.color,
145 fontFamily: sdLinkColorStyles[ 'font-family' ],
146 fontSize: sdLinkColorStyles[ 'font-size' ],
147 textDecoration: sdLinkColorStyles[ 'text-decoration' ],
148 fontWeight: sdLinkColorStyles[ 'font-weight' ],
149 fontStyle: sdLinkColorStyles[ 'font-style' ],
150 };
151
152 JetpackLikesPostMessage( stylesData, window.frames[ 'likes-master' ] );
153
154 JetpackLikesBatchHandler();
155 } );
156
157 break;
158
159 // We're keeping this for planned future follow ups.
160 // @see: https://github.com/Automattic/jetpack/pull/42361#discussion_r1995338815
161 case 'showLikeWidget':
162 break;
163
164 // We're keeping this for planned future follow ups.
165 // @see: https://github.com/Automattic/jetpack/pull/42361#discussion_r1995338815
166 case 'showCommentLikeWidget':
167 break;
168
169 case 'killCommentLikes':
170 // If kill switch for comment likes is enabled remove all widgets wrappers and `Loading...` placeholders.
171 document
172 .querySelectorAll( '.jetpack-comment-likes-widget-wrapper' )
173 .forEach( wrapper => wrapper.remove() );
174 break;
175
176 case 'clickReblogFlair':
177 if ( wpcom_reblog && typeof wpcom_reblog.toggle_reblog_box_flair === 'function' ) {
178 wpcom_reblog.toggle_reblog_box_flair( data.obj_id );
179 }
180 break;
181
182 case 'hideOtherGravatars': {
183 hideLikersPopover();
184 break;
185 }
186
187 case 'showOtherGravatars': {
188 const container = document.querySelector( '#likes-other-gravatars' );
189
190 if ( ! container ) {
191 break;
192 }
193
194 const list = container.querySelector( 'ul' );
195
196 container.style.display = 'none';
197 list.innerHTML = '';
198
199 container
200 .querySelectorAll( '.likes-text span' )
201 .forEach( item => ( item.textContent = data.totalLikesLabel ) );
202
203 ( data.likers || [] ).forEach( async ( liker, index ) => {
204 if ( liker.profile_URL.substr( 0, 4 ) !== 'http' ) {
205 // We only display gravatars with http or https schema
206 return;
207 }
208
209 const element = document.createElement( 'li' );
210 list.append( element );
211
212 const profileLink = encodeURI( liker.profile_URL );
213 const avatarLink = encodeURI( liker.avatar_URL );
214 element.innerHTML = `<a href="${ profileLink }" rel="nofollow" target="_parent" class="wpl-liker">
215 <img src="${ avatarLink }"
216 alt=""
217 style="width: 28px; height: 28px;" />
218 <span></span>
219 </a>`;
220
221 // Add some extra attributes through native methods, to ensure strings are sanitized.
222 element.classList.add( liker.css_class );
223 element.querySelector( 'img' ).alt = data.avatarAltTitle.replace( '%s', liker.name );
224 element.querySelector( 'span' ).innerText = liker.name;
225
226 if ( index === data.likers.length - 1 ) {
227 element.addEventListener( 'keydown', e => {
228 if ( e.key === 'Tab' && ! e.shiftKey ) {
229 e.preventDefault();
230 hideLikersPopover();
231
232 JetpackLikesPostMessage(
233 { event: 'focusLikesCount', parent: data.parent },
234 window.frames[ 'likes-master' ]
235 );
236 }
237 } );
238 }
239 } );
240
241 const positionPopup = function () {
242 const containerStyle = getComputedStyle( container );
243 const isRtl = containerStyle.direction === 'rtl';
244
245 const el = document.querySelector( `*[name='${ data.parent }']` );
246 const rect = el.getBoundingClientRect();
247 const win = el.ownerDocument.defaultView;
248 const offset = {
249 top: rect.top + win.pageYOffset,
250 left: rect.left + win.pageXOffset,
251 };
252
253 let containerLeft;
254 container.style.top = offset.top + data.position.top - 1 + 'px';
255
256 if ( isRtl ) {
257 const visibleAvatarsCount = data && data.likers ? Math.min( data.likers.length, 5 ) : 0;
258 // 24px is the width of the avatar + 4px is the padding between avatars
259 containerLeft = offset.left + data.position.left + 24 * visibleAvatarsCount + 4;
260 container.style.transform = 'translateX(-100%)';
261 } else {
262 containerLeft = offset.left + data.position.left;
263 }
264 container.style.left = containerLeft + 'px';
265
266 // If the popup overflows viewport width, we should show it on the next line.
267 // Push it offscreen to calculated rendered width.
268 container.style.left = '-9999px';
269 container.style.display = 'block';
270
271 // If the popup exceeds the viewport width,
272 // flip the position of the popup.
273 const containerWidth = container.offsetWidth;
274 const containerRight = containerLeft + containerWidth;
275 if ( containerRight > win.innerWidth ) {
276 containerLeft = rect.right - containerWidth;
277 }
278
279 // Set the container left
280 container.style.left = containerLeft + 'px';
281 container.setAttribute( 'aria-hidden', 'false' );
282 };
283
284 positionPopup();
285 container.focus();
286
287 const debounce = function ( func, wait ) {
288 var timeout;
289 return function () {
290 var context = this;
291 var args = arguments;
292 clearTimeout( timeout );
293 timeout = setTimeout( function () {
294 func.apply( context, args );
295 }, wait );
296 };
297 };
298
299 const debouncedPositionPopup = debounce( positionPopup, 100 );
300
301 // Keep a reference of this function in the element itself
302 // so that we can destroy it later
303 container.__resizeHandler = debouncedPositionPopup;
304
305 // When window is resized, resize the popup.
306 window.addEventListener( 'resize', debouncedPositionPopup );
307
308 container.focus();
309 }
310 }
311 }
312
313 window.addEventListener( 'message', JetpackLikesMessageListener );
314
315 function hideLikersPopover() {
316 const container = document.querySelector( '#likes-other-gravatars' );
317
318 if ( container ) {
319 container.style.display = 'none';
320 container.setAttribute( 'aria-hidden', 'true' );
321
322 // Remove the resize event listener and cleanup.
323 const resizeHandler = container.__resizeHandler;
324 if ( resizeHandler ) {
325 window.removeEventListener( 'resize', resizeHandler );
326 delete container.__resizeHandler;
327 }
328 }
329 }
330
331 document.addEventListener( 'click', hideLikersPopover );
332
333 function JetpackLikesWidgetQueueHandler() {
334 var wrapperID;
335
336 if ( ! jetpackLikesMasterReady ) {
337 // The master iframe emits `masterReady` a single time, when it finishes loading. On
338 // script-heavy pages it can finish loading — and emit — before this script attaches its
339 // message listener above, so that one event is missed and the queue never starts. Ping the
340 // master iframe while we wait so it re-emits `masterReady` once both it and our listener
341 // are ready.
342 JetpackLikesPostMessage( { event: 'queryMasterReady' }, window.frames[ 'likes-master' ] );
343 setTimeout( JetpackLikesWidgetQueueHandler, 500 );
344 return;
345 }
346
347 // Restore widgets to initial unloaded state when they are scrolled out of view.
348 jetpackUnloadScrolledOutWidgets();
349
350 var unloadedWidgetsInView = jetpackGetUnloadedWidgetsInView();
351
352 if ( unloadedWidgetsInView.length > 0 ) {
353 // Grab any unloaded widgets for a batch request
354 JetpackLikesBatchHandler();
355 }
356
357 for ( var i = 0, length = unloadedWidgetsInView.length; i <= length - 1; i++ ) {
358 wrapperID = unloadedWidgetsInView[ i ].id;
359
360 if ( ! wrapperID ) {
361 continue;
362 }
363
364 jetpackLoadLikeWidgetIframe( wrapperID );
365 }
366 }
367
368 function jetpackLoadLikeWidgetIframe( wrapperID ) {
369 if ( typeof wrapperID === 'undefined' ) {
370 return;
371 }
372
373 const wrapper = document.querySelector( '#' + wrapperID );
374 wrapper.querySelectorAll( 'iframe' ).forEach( iFrame => iFrame.remove() );
375
376 const placeholder = wrapper.querySelector( '.likes-widget-placeholder' );
377
378 // Post like iframe
379 if ( placeholder && placeholder.classList.contains( 'post-likes-widget-placeholder' ) ) {
380 const postLikesFrame = document.createElement( 'iframe' );
381
382 postLikesFrame.classList.add( 'post-likes-widget', 'jetpack-likes-widget' );
383 postLikesFrame.name = wrapper.dataset.name;
384 postLikesFrame.src = wrapper.dataset.src;
385 postLikesFrame.height = '55px';
386 postLikesFrame.width = '100%';
387 postLikesFrame.frameBorder = '0';
388 postLikesFrame.scrolling = 'no';
389 postLikesFrame.title = wrapper.dataset.title;
390
391 placeholder.after( postLikesFrame );
392 }
393
394 // Comment like iframe
395 if ( placeholder.classList.contains( 'comment-likes-widget-placeholder' ) ) {
396 const commentLikesFrame = document.createElement( 'iframe' );
397
398 commentLikesFrame.class = 'comment-likes-widget-frame jetpack-likes-widget-frame';
399 commentLikesFrame.name = wrapper.dataset.name;
400 commentLikesFrame.src = wrapper.dataset.src;
401 commentLikesFrame.height = '18px';
402 commentLikesFrame.width = '100%';
403 commentLikesFrame.frameBorder = '0';
404 commentLikesFrame.scrolling = 'no';
405
406 wrapper.querySelector( '.comment-like-feedback' ).after( commentLikesFrame );
407
408 jetpackCommentLikesLoadedWidgets.push( commentLikesFrame );
409 }
410
411 wrapper.classList.remove( 'jetpack-likes-widget-unloaded' );
412 wrapper.classList.add( 'jetpack-likes-widget-loading' );
413
414 wrapper.querySelector( 'iframe' ).addEventListener( 'load', e => {
415 JetpackLikesPostMessage(
416 { event: 'loadLikeWidget', name: e.target.name, width: e.target.width },
417 window.frames[ 'likes-master' ]
418 );
419
420 wrapper.classList.remove( 'jetpack-likes-widget-loading' );
421 wrapper.classList.add( 'jetpack-likes-widget-loaded' );
422 } );
423 }
424
425 function jetpackGetUnloadedWidgetsInView() {
426 const unloadedWidgets = document.querySelectorAll( 'div.jetpack-likes-widget-unloaded' );
427
428 return [ ...unloadedWidgets ].filter( item => jetpackIsScrolledIntoView( item ) );
429 }
430
431 function jetpackIsScrolledIntoView( element ) {
432 const top = element.getBoundingClientRect().top;
433 const bottom = element.getBoundingClientRect().bottom;
434
435 // Allow some slack above and bellow the fold with jetpackLikesLookAhead,
436 // with the aim of hiding the transition from unloaded to loaded widget from users.
437 return top + jetpackLikesLookAhead >= 0 && bottom <= window.innerHeight + jetpackLikesLookAhead;
438 }
439
440 function jetpackUnloadScrolledOutWidgets() {
441 for ( let i = jetpackCommentLikesLoadedWidgets.length - 1; i >= 0; i-- ) {
442 const currentWidgetIframe = jetpackCommentLikesLoadedWidgets[ i ];
443
444 if ( ! jetpackIsScrolledIntoView( currentWidgetIframe ) ) {
445 const widgetWrapper =
446 currentWidgetIframe &&
447 currentWidgetIframe.parentElement &&
448 currentWidgetIframe.parentElement.parentElement;
449
450 // Restore parent class to 'unloaded' so this widget can be picked up by queue manager again if needed.
451 widgetWrapper.classList.remove( 'jetpack-likes-widget-loaded' );
452 widgetWrapper.classList.remove( 'jetpack-likes-widget-loading' );
453 widgetWrapper.classList.add( 'jetpack-likes-widget-unloaded' );
454
455 // Remove it from the list of loaded widgets.
456 jetpackCommentLikesLoadedWidgets.splice( i, 1 );
457
458 // Remove comment like widget iFrame.
459 currentWidgetIframe.remove();
460 }
461 }
462 }
463
464 var jetpackWidgetsDelayedExec = function ( after, fn ) {
465 var timer;
466 return function () {
467 clearTimeout( timer );
468 timer = setTimeout( fn, after );
469 };
470 };
471
472 var jetpackOnScrollStopped = jetpackWidgetsDelayedExec( 250, JetpackLikesWidgetQueueHandler );
473
474 // Load initial batch of widgets, prior to any scrolling events.
475 JetpackLikesWidgetQueueHandler();
476
477 // Add event listener to execute queue handler after scroll.
478 window.addEventListener( 'scroll', jetpackOnScrollStopped, true );
479