PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.5
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.5
16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 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 All 506 releases
← All changes | modules/likes/queuehandler.js +96 -117 13.2.4 → 16.3-a.5 View file →
@@ -23,9 +23,9 @@
23 23 function JetpackLikesPostMessage( message, target ) {
24 24 if ( typeof message === 'string' ) {
25 25 try {
26 26 message = JSON.parse( message );
27 - } catch ( e ) {
27 + } catch {
28 28 return;
29 29 }
30 30 }
31 31
@@ -37,10 +37,10 @@
37 37 data: message,
38 38 } ),
39 39 '*'
40 40 );
41 - } catch ( e ) {
42 - return;
41 + } catch {
42 + // Ignore error
43 43 }
44 44 }
45 45 }
46 46
@@ -93,9 +93,9 @@
93 93 let message = event && event.data;
94 94 if ( typeof message === 'string' ) {
95 95 try {
96 96 message = JSON.parse( message );
97 - } catch ( err ) {
97 + } catch {
98 98 return;
99 99 }
100 100 }
101 101
@@ -124,23 +124,8 @@
124 124 const sdLinkColor = document.querySelector( '.sd-link-color' );
125 125 const sdTextColorStyles = ( sdTextColor && getComputedStyle( sdTextColor ) ) || {};
126 126 const sdLinkColorStyles = ( sdLinkColor && getComputedStyle( sdLinkColor ) ) || {};
127 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 128 // enable reblogs if we're on a single post page
144 129 if ( document.body.classList.contains( 'single' ) ) {
145 130 JetpackLikesPostMessage( { event: 'reblogsEnabled' }, window.frames[ 'likes-master' ] );
146 131 }
@@ -145,12 +130,12 @@
145 130 JetpackLikesPostMessage( { event: 'reblogsEnabled' }, window.frames[ 'likes-master' ] );
146 131 }
147 132
148 133 stylesData.textStyles = {
149 - color: sdTextColorStyles[ 'color' ],
134 + color: sdTextColorStyles.color,
150 135 fontFamily: sdTextColorStyles[ 'font-family' ],
151 136 fontSize: sdTextColorStyles[ 'font-size' ],
152 - direction: sdTextColorStyles[ 'direction' ],
137 + direction: sdTextColorStyles.direction,
153 138 fontWeight: sdTextColorStyles[ 'font-weight' ],
154 139 fontStyle: sdTextColorStyles[ 'font-style' ],
155 140 textDecoration: sdTextColorStyles[ 'text-decoration' ],
156 141 };
@@ -155,9 +140,9 @@
155 140 textDecoration: sdTextColorStyles[ 'text-decoration' ],
156 141 };
157 142
158 143 stylesData.linkStyles = {
159 - color: sdLinkColorStyles[ 'color' ],
144 + color: sdLinkColorStyles.color,
160 145 fontFamily: sdLinkColorStyles[ 'font-family' ],
161 146 fontSize: sdLinkColorStyles[ 'font-size' ],
162 147 textDecoration: sdLinkColorStyles[ 'text-decoration' ],
163 148 fontWeight: sdLinkColorStyles[ 'font-weight' ],
@@ -170,23 +155,17 @@
170 155 } );
171 156
172 157 break;
173 158
174 - case 'showLikeWidget': {
175 - const placeholder = document.querySelector( `#${ data.id } .likes-widget-placeholder` );
176 - if ( placeholder ) {
177 - placeholder.style.display = 'none';
178 - }
159 + // We're keeping this for planned future follow ups.
160 + // @see: https://github.com/Automattic/jetpack/pull/42361#discussion_r1995338815
161 + case 'showLikeWidget':
179 162 break;
180 - }
181 163
182 - case 'showCommentLikeWidget': {
183 - const placeholder = document.querySelector( `#${ data.id } .likes-widget-placeholder` );
184 - if ( placeholder ) {
185 - placeholder.style.display = 'none';
186 - }
164 + // We're keeping this for planned future follow ups.
165 + // @see: https://github.com/Automattic/jetpack/pull/42361#discussion_r1995338815
166 + case 'showCommentLikeWidget':
187 167 break;
188 - }
189 168
190 169 case 'killCommentLikes':
191 170 // If kill switch for comment likes is enabled remove all widgets wrappers and `Loading...` placeholders.
192 171 document
@@ -211,24 +190,16 @@
211 190 if ( ! container ) {
212 191 break;
213 192 }
214 193
215 - const newLayout = container.classList.contains( 'wpl-new-layout' );
216 -
217 194 const list = container.querySelector( 'ul' );
218 195
219 196 container.style.display = 'none';
220 197 list.innerHTML = '';
221 198
222 - if ( newLayout ) {
223 - container
224 - .querySelectorAll( '.likes-text span' )
225 - .forEach( item => ( item.textContent = data.totalLikesLabel ) );
226 - } else {
227 - container
228 - .querySelectorAll( '.likes-text span' )
229 - .forEach( item => ( item.textContent = data.total ) );
230 - }
199 + container
200 + .querySelectorAll( '.likes-text span' )
201 + .forEach( item => ( item.textContent = data.totalLikesLabel ) );
231 202
232 203 ( data.likers || [] ).forEach( async ( liker, index ) => {
233 204 if ( liker.profile_URL.substr( 0, 4 ) !== 'http' ) {
234 205 // We only display gravatars with http or https schema
@@ -237,35 +208,22 @@
237 208
238 209 const element = document.createElement( 'li' );
239 210 list.append( element );
240 211
241 - if ( newLayout ) {
242 - element.innerHTML = `
243 - <a href="${ encodeURI( liker.profile_URL ) }" rel="nofollow" target="_parent" class="wpl-liker">
244 - <img src="${ encodeURI( liker.avatar_URL ) }"
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 }"
245 216 alt=""
246 217 style="width: 28px; height: 28px;" />
247 218 <span></span>
248 - </a>
249 - `;
250 - } else {
251 - element.innerHTML = `
252 - <a href="${ encodeURI( liker.profile_URL ) }" rel="nofollow" target="_parent" class="wpl-liker">
253 - <img src="${ encodeURI( liker.avatar_URL ) }"
254 - alt=""
255 - style="width: 30px; height: 30px; padding-right: 3px;" />
256 - </a>
257 - `;
258 - }
219 + </a>`;
259 220
260 221 // Add some extra attributes through native methods, to ensure strings are sanitized.
261 222 element.classList.add( liker.css_class );
262 223 element.querySelector( 'img' ).alt = data.avatarAltTitle.replace( '%s', liker.name );
224 + element.querySelector( 'span' ).innerText = liker.name;
263 225
264 - if ( newLayout ) {
265 - element.querySelector( 'span' ).innerText = liker.name;
266 - }
267 -
268 226 if ( index === data.likers.length - 1 ) {
269 227 element.addEventListener( 'keydown', e => {
270 228 if ( e.key === 'Tab' && ! e.shiftKey ) {
271 229 e.preventDefault();
@@ -291,64 +249,36 @@
291 249 top: rect.top + win.pageYOffset,
292 250 left: rect.left + win.pageXOffset,
293 251 };
294 252
295 - let containerLeft = 0;
296 - if ( newLayout ) {
297 - container.style.top = offset.top + data.position.top - 1 + 'px';
253 + let containerLeft;
254 + container.style.top = offset.top + data.position.top - 1 + 'px';
298 255
299 - if ( isRtl ) {
300 - const visibleAvatarsCount = data && data.likers ? Math.min( data.likers.length, 5 ) : 0;
301 - // 24px is the width of the avatar + 4px is the padding between avatars
302 - containerLeft = offset.left + data.position.left + 24 * visibleAvatarsCount + 4;
303 - container.style.transform = 'translateX(-100%)';
304 - } else {
305 - containerLeft = offset.left + data.position.left;
306 - }
307 - container.style.left = containerLeft + 'px';
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%)';
308 261 } else {
309 - container.style.left = offset.left + data.position.left - 10 + 'px';
310 - container.style.top = offset.top + data.position.top - 33 + 'px';
262 + containerLeft = offset.left + data.position.left;
311 263 }
264 + container.style.left = containerLeft + 'px';
312 265
313 - // Container width - padding
314 - const initContainerWidth = data.width - 20;
315 - const rowLength = Math.floor( initContainerWidth / 37 );
316 - // # of rows + (avatar + avatar padding) + text above + container padding
317 - let height = Math.ceil( data.likers.length / rowLength ) * 37 + 17 + 22;
318 - if ( height > 204 ) {
319 - height = 204;
320 - }
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';
321 270
322 - if ( ! newLayout ) {
323 - // Avatars + padding
324 - const containerWidth = rowLength * 37 + 13;
325 - container.style.height = height + 'px';
326 - container.style.width = containerWidth + 'px';
327 -
328 - const listWidth = rowLength * 37;
329 - list.style.width = listWidth + 'px';
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;
330 277 }
331 278
332 - // If the popup is overflows viewport width, we should show it on the next line
333 - if ( newLayout ) {
334 - // Push it offscreen to calculated rendered width
335 - container.style.left = '-9999px';
336 - container.style.display = 'block';
337 -
338 - // If the popup exceeds the viewport width,
339 - // flip the position of the popup.
340 - const containerWidth = container.offsetWidth;
341 - const containerRight = containerLeft + containerWidth;
342 - if ( containerRight > win.innerWidth ) {
343 - containerLeft = rect.right - containerWidth;
344 - }
345 -
346 - // Set the container left
347 - container.style.left = containerLeft + 'px';
348 - } else {
349 - container.style.display = 'block';
350 - }
279 + // Set the container left
280 + container.style.left = containerLeft + 'px';
351 281 container.setAttribute( 'aria-hidden', 'false' );
352 282 };
353 283
354 284 positionPopup();
@@ -403,8 +333,14 @@
403 333 function JetpackLikesWidgetQueueHandler() {
404 334 var wrapperID;
405 335
406 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' ] );
407 343 setTimeout( JetpackLikesWidgetQueueHandler, 500 );
408 344 return;
409 345 }
410 346
@@ -410,8 +346,10 @@
410 346
411 347 // Restore widgets to initial unloaded state when they are scrolled out of view.
412 348 jetpackUnloadScrolledOutWidgets();
413 349
350 + jetpackObserveUnloadedWidgets();
351 +
414 352 var unloadedWidgetsInView = jetpackGetUnloadedWidgetsInView();
415 353
416 354 if ( unloadedWidgetsInView.length > 0 ) {
417 355 // Grab any unloaded widgets for a batch request
@@ -475,13 +413,26 @@
475 413 wrapper.classList.remove( 'jetpack-likes-widget-unloaded' );
476 414 wrapper.classList.add( 'jetpack-likes-widget-loading' );
477 415
478 416 wrapper.querySelector( 'iframe' ).addEventListener( 'load', e => {
417 + // A widget scrolled out of view mid-load has its iframe dropped, and a reloaded one
418 + // replaces it, either of which leaves this closure holding a detached iframe. Acting on
419 + // it would hide a placeholder that is now the only thing the widget has left to show.
420 + if ( ! wrapper.contains( e.target ) ) {
421 + return;
422 + }
423 +
479 424 JetpackLikesPostMessage(
480 425 { event: 'loadLikeWidget', name: e.target.name, width: e.target.width },
481 426 window.frames[ 'likes-master' ]
482 427 );
483 428
429 + // The stylesheet is otherwise the only thing that hides the placeholder, so one that never
430 + // applies leaves "Loading…" sitting over a widget that loaded fine.
431 + if ( placeholder ) {
432 + placeholder.style.display = 'none';
433 + }
434 +
484 435 wrapper.classList.remove( 'jetpack-likes-widget-loading' );
485 436 wrapper.classList.add( 'jetpack-likes-widget-loaded' );
486 437 } );
487 438 }
@@ -515,12 +466,15 @@
515 466 widgetWrapper.classList.remove( 'jetpack-likes-widget-loaded' );
516 467 widgetWrapper.classList.remove( 'jetpack-likes-widget-loading' );
517 468 widgetWrapper.classList.add( 'jetpack-likes-widget-unloaded' );
518 469
519 - // Bring back the loading placeholder into view.
520 - widgetWrapper
521 - .querySelectorAll( '.comment-likes-widget-placeholder' )
522 - .forEach( item => ( item.style.display = 'block' ) );
470 + // An empty string removes the inline declaration rather than setting one, handing the
471 + // placeholder back to the stylesheet. The `display: none` set on load would otherwise
472 + // outrank it, leaving an unloaded widget with neither an iframe nor a placeholder.
473 + const placeholder = widgetWrapper.querySelector( '.likes-widget-placeholder' );
474 + if ( placeholder ) {
475 + placeholder.style.display = '';
476 + }
523 477
524 478 // Remove it from the list of loaded widgets.
525 479 jetpackCommentLikesLoadedWidgets.splice( i, 1 );
526 480
@@ -538,8 +492,33 @@
538 492 };
539 493 };
540 494
541 495 var jetpackOnScrollStopped = jetpackWidgetsDelayedExec( 250, JetpackLikesWidgetQueueHandler );
496 +
497 +// Scrolling is not the only thing that brings a widget into range. A stylesheet that lands late
498 +// reflows the page without firing a scroll event, and the queue would never look at that widget
499 +// again, leaving it on "Loading…" until the reader happens to scroll.
500 +var jetpackLikesWidgetObserver =
501 + typeof IntersectionObserver === 'function'
502 + ? new IntersectionObserver( jetpackOnScrollStopped, {
503 + rootMargin: `${ jetpackLikesLookAhead }px`,
504 + // jetpackIsScrolledIntoView() wants the widget fully inside the band, so ask to be
505 + // told when it gets there - crossing into partial overlap alone would not load it.
506 + threshold: [ 0, 1 ],
507 + } )
508 + : null;
509 +
510 +// Observing an element twice is a no-op, so every queue pass can call this to pick up widgets
511 +// added after load.
512 +function jetpackObserveUnloadedWidgets() {
513 + if ( ! jetpackLikesWidgetObserver ) {
514 + return;
515 + }
516 +
517 + document
518 + .querySelectorAll( 'div.jetpack-likes-widget-unloaded' )
519 + .forEach( widget => jetpackLikesWidgetObserver.observe( widget ) );
520 +}
542 521
543 522 // Load initial batch of widgets, prior to any scrolling events.
544 523 JetpackLikesWidgetQueueHandler();
545 524