PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / trunk
Jetpack – WP Security, Backup, Speed, & Growth vtrunk
16.2-a.3 16.1.2 16.2-a.1 16.1.1 16.1 16.1-beta 16.1-beta.2 16.1-beta.3 16.1-a.5 16.1-a.3 16.0.1 16.1-a.1 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 3 months ago jetpack-likes-settings.php 1 week ago post-count-jetpack.js 4 years ago post-count.js 1 year ago queuehandler.js 1 week ago style.css 8 months ago
queuehandler.js
479 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 // 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