PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 16.0-beta
Jetpack – WP Security, Backup, Speed, & Growth v16.0-beta
16.0 16.0-beta 16.0-a.7 16.0-a.5 15.9.1 16.0-a.3 16.0-a.1 15.9 15.9-beta 15.9-a.7 15.9-a.5 15.9-a.3 15.9-a.1 15.8 15.8-beta 15.8-a.7 15.8-a.5 5.2.5 5.3.4 5.4.4 5.5.5 5.6.5 5.7.5 5.8.4 5.9.4 6.0.4 6.1 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.2 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.3 6.3.1 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.7 6.4 6.4.1 6.4.2 6.4.3 6.4.4 6.4.5 6.4.6 6.5 6.5.1 6.5.2 6.5.3 6.5.4 6.6 6.6.1 6.6.2 6.6.3 6.6.4 6.6.5 6.7 6.7.1 6.7.2 6.7.3 6.7.4 6.8 6.8.1 6.8.2 6.8.3 6.8.4 6.8.5 6.9 6.9.1 6.9.2 6.9.3 6.9.4 7.0 7.0.1 7.0.2 7.0.3 7.0.4 7.0.5 7.1 7.1.1 7.1.2 7.1.3 7.1.4 7.1.5 7.2 7.2.1 7.2.1.1 7.2.2 7.2.3 7.2.4 7.2.5 7.3 7.3.0.1 7.3.1 7.3.1.1 7.3.2 7.3.3 7.3.4 7.3.5 7.4 7.4.1 7.4.2 7.4.3 7.4.4 7.4.5 7.5 7.5.0.1 7.5.1 7.5.2 7.5.3 7.5.4 7.5.5 7.5.6 7.5.7 7.6 7.6.1 7.6.2 7.6.3 7.6.4 7.7 7.7.1 7.7.2 7.7.3 7.7.4 7.7.5 7.7.6 7.8 7.8.1 7.8.2 7.8.3 7.8.4 7.9 7.9.1 7.9.2 7.9.3 7.9.4 8.0 8.0.1 8.0.2 8.0.3 8.1 8.1.1 8.1.2 8.1.3 8.1.4 8.2 8.2.0.1 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.2.6 8.3 8.3.1 8.3.2 8.3.3 8.4 8.4.1 8.4.2 8.4.3 8.4.4 8.4.5 8.5 8.5.1 8.5.2 8.5.3 8.6 8.6.1 8.6.2 8.6.3 8.6.4 8.7 8.7.0.1 8.7.1 8.7.2 8.7.3 8.7.4 8.8 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.9 8.9.1 8.9.2 8.9.3 8.9.4 9.0 9.0.1 9.0.2 9.0.3 9.0.4 9.0.5 9.1 9.1.1 9.1.2 9.1.3 9.2 9.2.1 9.2.2 9.2.3 9.2.4 9.3 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.4 9.4.1 9.4.2 9.4.3 9.4.4 9.5 9.5.1 9.5.2 9.5.3 9.5.4 9.5.5 9.6 9.6.1 9.6.2 9.6.3 9.6.4 9.7 9.7.1 9.7.2 15.7-beta.2 9.7.3 15.7.1 9.8 15.8-a.1 9.8.1 15.8-a.3 9.8.2 2.0.9 9.8.3 2.1.7 9.9 2.2.10 9.9.1 2.3.10 9.9.2 2.4.7 9.9.3 2.5.5 2.6.6 2.7.5 2.8.5 2.9.6 3.0.6 3.1.5 3.2.5 3.3.6 3.4.6 3.5.6 3.6.4 3.7.5 3.8.5 3.9.10 4.0.7 4.1.4 4.2.5 4.3.5 4.4.5 4.5.3 4.6.3 4.7.4 4.8.5 4.9.3 5.0.3 5.1.4 trunk 10.0 10.0.1 10.0.2 10.1 10.1.1 10.1.2 10.2 10.2.1 10.2.2 10.2.3 10.3 10.3.1 10.3.2 10.4 10.4.1 10.4.2 10.5 10.5.1 10.5.2 10.5.3 10.6 10.6.1 10.6.2 10.7 10.7.1 10.7.2 10.8 10.8.1 10.8.2 10.9 10.9.1 10.9.2 10.9.3 11.0 11.0.1 11.0.2 11.1 11.1.1 11.1.2 11.1.3 11.1.4 11.2 11.2.1 11.2.2 11.3 11.3.1 11.3.2 11.3.3 11.3.4 11.4 11.4.1 11.4.2 11.5 11.5.1 11.5.2 11.5.3 11.6 11.6.1 11.6.2 11.7 11.7.1 11.7.2 11.7.3 11.8 11.8.3 11.8.4 11.8.5 11.8.6 11.9 11.9.1 11.9.2 11.9.3 12.0 12.0.1 12.0.2 12.1 12.1.1 12.1.2 12.2 12.2.1 12.2.2 12.3 12.3.1 12.4 12.4.1 12.5 12.5.1 12.6 12.6.1 12.6.2 12.6.3 12.7 12.7.1 12.7.2 12.8 12.8.1 12.8.2 12.9 12.9.1 12.9.2 12.9.3 12.9.4 13.0 13.0.1 13.1 13.1.1 13.1.2 13.1.3 13.1.4 13.2 13.2.1 13.2.2 13.2.3 13.3 13.3.1 13.3.2 13.4 13.4.1 13.4.2 13.4.3 13.4.4 13.5 13.5.1 13.6 13.6.1 13.7 13.7.1 13.8 13.8.1 13.8.2 13.9 13.9.1 14.0 14.1 14.2 14.2.1 14.3 14.4 14.4.1 14.5 14.6 14.7 14.8 14.9 14.9.1 15.0 15.0.1 15.0.2 15.1 15.1.1 15.2 15.3 15.3.1 15.4 15.5 15.6 15.7 15.7-a.1 15.7-a.3 15.7-a.5 15.7-a.7 15.7-beta
jetpack / modules / likes / queuehandler.js
jetpack / modules / likes Last commit date
jetpack-likes-master-iframe.php 1 month ago jetpack-likes-settings.php 2 weeks ago post-count-jetpack.js 4 years ago post-count.js 1 year ago queuehandler.js 1 month ago style.css 6 months ago
queuehandler.js
473 lines
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 setTimeout( JetpackLikesWidgetQueueHandler, 500 );
338 return;
339 }
340
341 // Restore widgets to initial unloaded state when they are scrolled out of view.
342 jetpackUnloadScrolledOutWidgets();
343
344 var unloadedWidgetsInView = jetpackGetUnloadedWidgetsInView();
345
346 if ( unloadedWidgetsInView.length > 0 ) {
347 // Grab any unloaded widgets for a batch request
348 JetpackLikesBatchHandler();
349 }
350
351 for ( var i = 0, length = unloadedWidgetsInView.length; i <= length - 1; i++ ) {
352 wrapperID = unloadedWidgetsInView[ i ].id;
353
354 if ( ! wrapperID ) {
355 continue;
356 }
357
358 jetpackLoadLikeWidgetIframe( wrapperID );
359 }
360 }
361
362 function jetpackLoadLikeWidgetIframe( wrapperID ) {
363 if ( typeof wrapperID === 'undefined' ) {
364 return;
365 }
366
367 const wrapper = document.querySelector( '#' + wrapperID );
368 wrapper.querySelectorAll( 'iframe' ).forEach( iFrame => iFrame.remove() );
369
370 const placeholder = wrapper.querySelector( '.likes-widget-placeholder' );
371
372 // Post like iframe
373 if ( placeholder && placeholder.classList.contains( 'post-likes-widget-placeholder' ) ) {
374 const postLikesFrame = document.createElement( 'iframe' );
375
376 postLikesFrame.classList.add( 'post-likes-widget', 'jetpack-likes-widget' );
377 postLikesFrame.name = wrapper.dataset.name;
378 postLikesFrame.src = wrapper.dataset.src;
379 postLikesFrame.height = '55px';
380 postLikesFrame.width = '100%';
381 postLikesFrame.frameBorder = '0';
382 postLikesFrame.scrolling = 'no';
383 postLikesFrame.title = wrapper.dataset.title;
384
385 placeholder.after( postLikesFrame );
386 }
387
388 // Comment like iframe
389 if ( placeholder.classList.contains( 'comment-likes-widget-placeholder' ) ) {
390 const commentLikesFrame = document.createElement( 'iframe' );
391
392 commentLikesFrame.class = 'comment-likes-widget-frame jetpack-likes-widget-frame';
393 commentLikesFrame.name = wrapper.dataset.name;
394 commentLikesFrame.src = wrapper.dataset.src;
395 commentLikesFrame.height = '18px';
396 commentLikesFrame.width = '100%';
397 commentLikesFrame.frameBorder = '0';
398 commentLikesFrame.scrolling = 'no';
399
400 wrapper.querySelector( '.comment-like-feedback' ).after( commentLikesFrame );
401
402 jetpackCommentLikesLoadedWidgets.push( commentLikesFrame );
403 }
404
405 wrapper.classList.remove( 'jetpack-likes-widget-unloaded' );
406 wrapper.classList.add( 'jetpack-likes-widget-loading' );
407
408 wrapper.querySelector( 'iframe' ).addEventListener( 'load', e => {
409 JetpackLikesPostMessage(
410 { event: 'loadLikeWidget', name: e.target.name, width: e.target.width },
411 window.frames[ 'likes-master' ]
412 );
413
414 wrapper.classList.remove( 'jetpack-likes-widget-loading' );
415 wrapper.classList.add( 'jetpack-likes-widget-loaded' );
416 } );
417 }
418
419 function jetpackGetUnloadedWidgetsInView() {
420 const unloadedWidgets = document.querySelectorAll( 'div.jetpack-likes-widget-unloaded' );
421
422 return [ ...unloadedWidgets ].filter( item => jetpackIsScrolledIntoView( item ) );
423 }
424
425 function jetpackIsScrolledIntoView( element ) {
426 const top = element.getBoundingClientRect().top;
427 const bottom = element.getBoundingClientRect().bottom;
428
429 // Allow some slack above and bellow the fold with jetpackLikesLookAhead,
430 // with the aim of hiding the transition from unloaded to loaded widget from users.
431 return top + jetpackLikesLookAhead >= 0 && bottom <= window.innerHeight + jetpackLikesLookAhead;
432 }
433
434 function jetpackUnloadScrolledOutWidgets() {
435 for ( let i = jetpackCommentLikesLoadedWidgets.length - 1; i >= 0; i-- ) {
436 const currentWidgetIframe = jetpackCommentLikesLoadedWidgets[ i ];
437
438 if ( ! jetpackIsScrolledIntoView( currentWidgetIframe ) ) {
439 const widgetWrapper =
440 currentWidgetIframe &&
441 currentWidgetIframe.parentElement &&
442 currentWidgetIframe.parentElement.parentElement;
443
444 // Restore parent class to 'unloaded' so this widget can be picked up by queue manager again if needed.
445 widgetWrapper.classList.remove( 'jetpack-likes-widget-loaded' );
446 widgetWrapper.classList.remove( 'jetpack-likes-widget-loading' );
447 widgetWrapper.classList.add( 'jetpack-likes-widget-unloaded' );
448
449 // Remove it from the list of loaded widgets.
450 jetpackCommentLikesLoadedWidgets.splice( i, 1 );
451
452 // Remove comment like widget iFrame.
453 currentWidgetIframe.remove();
454 }
455 }
456 }
457
458 var jetpackWidgetsDelayedExec = function ( after, fn ) {
459 var timer;
460 return function () {
461 clearTimeout( timer );
462 timer = setTimeout( fn, after );
463 };
464 };
465
466 var jetpackOnScrollStopped = jetpackWidgetsDelayedExec( 250, JetpackLikesWidgetQueueHandler );
467
468 // Load initial batch of widgets, prior to any scrolling events.
469 JetpackLikesWidgetQueueHandler();
470
471 // Add event listener to execute queue handler after scroll.
472 window.addEventListener( 'scroll', jetpackOnScrollStopped, true );
473