PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.7
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.7
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 +137 -102 13.0.2 → 16.3-a.7 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
@@ -235,37 +206,24 @@
235 206 return;
236 207 }
237 208
238 209 const element = document.createElement( 'li' );
239 - if ( newLayout ) {
240 - element.innerHTML = `
241 - <a href="${ encodeURI( liker.profile_URL ) }" rel="nofollow" target="_parent" class="wpl-liker">
242 - <img src="${ encodeURI( liker.avatar_URL ) }"
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 }"
243 216 alt=""
244 217 style="width: 28px; height: 28px;" />
245 218 <span></span>
246 - </a>
247 - `;
248 - } else {
249 - element.innerHTML = `
250 - <a href="${ encodeURI( liker.profile_URL ) }" rel="nofollow" target="_parent" class="wpl-liker">
251 - <img src="${ encodeURI( liker.avatar_URL ) }"
252 - alt=""
253 - style="width: 30px; height: 30px; padding-right: 3px;" />
254 - </a>
255 - `;
256 - }
219 + </a>`;
257 220
258 - list.append( element );
259 -
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();
@@ -279,55 +237,76 @@
279 237 } );
280 238 }
281 239 } );
282 240
283 - const containerStyle = getComputedStyle( container );
284 - const isRtl = containerStyle.direction === 'rtl';
241 + const positionPopup = function () {
242 + const containerStyle = getComputedStyle( container );
243 + const isRtl = containerStyle.direction === 'rtl';
285 244
286 - const el = document.querySelector( `*[name='${ data.parent }']` );
287 - const rect = el.getBoundingClientRect();
288 - const win = el.ownerDocument.defaultView;
289 - const offset = {
290 - top: rect.top + win.pageYOffset,
291 - left: rect.left + win.pageXOffset,
292 - };
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 + };
293 252
294 - if ( newLayout ) {
253 + let containerLeft;
295 254 container.style.top = offset.top + data.position.top - 1 + 'px';
296 255
297 256 if ( isRtl ) {
298 257 const visibleAvatarsCount = data && data.likers ? Math.min( data.likers.length, 5 ) : 0;
299 258 // 24px is the width of the avatar + 4px is the padding between avatars
300 - container.style.left =
301 - offset.left + data.position.left + 24 * visibleAvatarsCount + 4 + 'px';
259 + containerLeft = offset.left + data.position.left + 24 * visibleAvatarsCount + 4;
302 260 container.style.transform = 'translateX(-100%)';
303 261 } else {
304 - container.style.left = offset.left + data.position.left + 'px';
262 + containerLeft = offset.left + data.position.left;
305 263 }
306 - } else {
307 - container.style.left = offset.left + data.position.left - 10 + 'px';
308 - container.style.top = offset.top + data.position.top - 33 + 'px';
309 - }
264 + container.style.left = containerLeft + 'px';
310 265
311 - const rowLength = Math.floor( data.width / 37 );
312 - let height = Math.ceil( data.likers.length / rowLength ) * 37 + 13;
313 - if ( height > 204 ) {
314 - height = 204;
315 - }
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';
316 270
317 - if ( ! newLayout ) {
318 - // Avatars + padding
319 - const containerWidth = rowLength * 37 - 7;
320 - container.style.height = height + 'px';
321 - container.style.width = containerWidth + '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;
277 + }
322 278
323 - const listWidth = rowLength * 37;
324 - list.style.width = listWidth + 'px';
325 - }
279 + // Set the container left
280 + container.style.left = containerLeft + 'px';
281 + container.setAttribute( 'aria-hidden', 'false' );
282 + };
326 283
327 - container.style.display = 'block';
328 - container.setAttribute( 'aria-hidden', 'false' );
284 + positionPopup();
329 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();
330 309 }
331 310 }
332 311 }
333 312
@@ -338,8 +317,15 @@
338 317
339 318 if ( container ) {
340 319 container.style.display = 'none';
341 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 + }
342 328 }
343 329 }
344 330
345 331 document.addEventListener( 'click', hideLikersPopover );
@@ -347,8 +333,14 @@
347 333 function JetpackLikesWidgetQueueHandler() {
348 334 var wrapperID;
349 335
350 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' ] );
351 343 setTimeout( JetpackLikesWidgetQueueHandler, 500 );
352 344 return;
353 345 }
354 346
@@ -354,8 +346,10 @@
354 346
355 347 // Restore widgets to initial unloaded state when they are scrolled out of view.
356 348 jetpackUnloadScrolledOutWidgets();
357 349
350 + jetpackObserveUnloadedWidgets();
351 +
358 352 var unloadedWidgetsInView = jetpackGetUnloadedWidgetsInView();
359 353
360 354 if ( unloadedWidgetsInView.length > 0 ) {
361 355 // Grab any unloaded widgets for a batch request
@@ -419,13 +413,26 @@
419 413 wrapper.classList.remove( 'jetpack-likes-widget-unloaded' );
420 414 wrapper.classList.add( 'jetpack-likes-widget-loading' );
421 415
422 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 +
423 424 JetpackLikesPostMessage(
424 425 { event: 'loadLikeWidget', name: e.target.name, width: e.target.width },
425 426 window.frames[ 'likes-master' ]
426 427 );
427 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 +
428 435 wrapper.classList.remove( 'jetpack-likes-widget-loading' );
429 436 wrapper.classList.add( 'jetpack-likes-widget-loaded' );
430 437 } );
431 438 }
@@ -459,12 +466,15 @@
459 466 widgetWrapper.classList.remove( 'jetpack-likes-widget-loaded' );
460 467 widgetWrapper.classList.remove( 'jetpack-likes-widget-loading' );
461 468 widgetWrapper.classList.add( 'jetpack-likes-widget-unloaded' );
462 469
463 - // Bring back the loading placeholder into view.
464 - widgetWrapper
465 - .querySelectorAll( '.comment-likes-widget-placeholder' )
466 - .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 + }
467 477
468 478 // Remove it from the list of loaded widgets.
469 479 jetpackCommentLikesLoadedWidgets.splice( i, 1 );
470 480
@@ -482,8 +492,33 @@
482 492 };
483 493 };
484 494
485 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 +}
486 521
487 522 // Load initial batch of widgets, prior to any scrolling events.
488 523 JetpackLikesWidgetQueueHandler();
489 524