PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.1
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.1
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 13.9.2 14.0.1 All 504 releases
← All changes | modules/likes/queuehandler.js +173 -76 12.7.316.3-a.1 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
@@ -199,10 +178,16 @@
199 178 wpcom_reblog.toggle_reblog_box_flair( data.obj_id );
200 179 }
201 180 break;
202 181
182 + case 'hideOtherGravatars': {
183 + hideLikersPopover();
184 + break;
185 + }
186 +
203 187 case 'showOtherGravatars': {
204 188 const container = document.querySelector( '#likes-other-gravatars' );
189 +
205 190 if ( ! container ) {
206 191 break;
207 192 }
208 193
@@ -212,11 +197,11 @@
212 197 list.innerHTML = '';
213 198
214 199 container
215 200 .querySelectorAll( '.likes-text span' )
216 - .forEach( item => ( item.textContent = data.total ) );
201 + .forEach( item => ( item.textContent = data.totalLikesLabel ) );
217 202
218 - ( data.likers || [] ).forEach( liker => {
203 + ( data.likers || [] ).forEach( async ( liker, index ) => {
219 204 if ( liker.profile_URL.substr( 0, 4 ) !== 'http' ) {
220 205 // We only display gravatars with http or https schema
221 206 return;
222 207 }
@@ -221,54 +206,107 @@
221 206 return;
222 207 }
223 208
224 209 const element = document.createElement( 'li' );
225 - element.innerHTML = `
226 - <a href="${ encodeURI( liker.profile_URL ) }" rel="nofollow" target="_parent" class="wpl-liker">
227 - <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 }"
228 216 alt=""
229 - style="width: 30px; height: 30px; padding-right: 3px;" />
230 - </a>
231 - `;
217 + style="width: 28px; height: 28px;" />
218 + <span></span>
219 + </a>`;
232 220
233 - list.append( element );
234 -
235 221 // Add some extra attributes through native methods, to ensure strings are sanitized.
236 222 element.classList.add( liker.css_class );
237 - element.querySelector( 'img' ).alt = liker.name;
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 + }
238 239 } );
239 240
240 - const el = document.querySelector( `*[name='${ data.parent }']` );
241 - const rect = el.getBoundingClientRect();
242 - const win = el.ownerDocument.defaultView;
243 - const offset = {
244 - top: rect.top + win.pageYOffset,
245 - left: rect.left + win.pageXOffset,
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' );
246 282 };
247 283
248 - container.style.left = offset.left + data.position.left - 10 + 'px';
249 - container.style.top = offset.top + data.position.top - 33 + 'px';
284 + positionPopup();
285 + container.focus();
250 286
251 - const rowLength = Math.floor( data.width / 37 );
252 - let height = Math.ceil( data.likers.length / rowLength ) * 37 + 13;
253 - if ( height > 204 ) {
254 - height = 204;
255 - }
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 + };
256 298
257 - const containerWidth = rowLength * 37 - 7;
258 - container.style.height = height + 'px';
259 - container.style.width = containerWidth + 'px';
299 + const debouncedPositionPopup = debounce( positionPopup, 100 );
260 300
261 - const listWidth = rowLength * 37;
262 - list.style.width = listWidth + 'px';
301 + // Keep a reference of this function in the element itself
302 + // so that we can destroy it later
303 + container.__resizeHandler = debouncedPositionPopup;
263 304
264 - container.style.display = 'block';
305 + // When window is resized, resize the popup.
306 + window.addEventListener( 'resize', debouncedPositionPopup );
265 307
266 - const scrollbarWidth = list.offsetWidth - list.clientWidth;
267 - if ( scrollbarWidth > 0 ) {
268 - container.style.width = containerWidth + scrollbarWidth + 'px';
269 - list.style.width = listWidth + scrollbarWidth + 'px';
270 - }
308 + container.focus();
271 309 }
272 310 }
273 311 }
274 312
@@ -273,20 +311,36 @@
273 311 }
274 312
275 313 window.addEventListener( 'message', JetpackLikesMessageListener );
276 314
277 -document.addEventListener( 'click', e => {
315 +function hideLikersPopover() {
278 316 const container = document.querySelector( '#likes-other-gravatars' );
279 317
280 - if ( container && ! container.contains( e.target ) ) {
318 + if ( container ) {
281 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 + }
282 328 }
283 -} );
329 +}
284 330
331 +document.addEventListener( 'click', hideLikersPopover );
332 +
285 333 function JetpackLikesWidgetQueueHandler() {
286 334 var wrapperID;
287 335
288 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' ] );
289 343 setTimeout( JetpackLikesWidgetQueueHandler, 500 );
290 344 return;
291 345 }
292 346
@@ -292,8 +346,10 @@
292 346
293 347 // Restore widgets to initial unloaded state when they are scrolled out of view.
294 348 jetpackUnloadScrolledOutWidgets();
295 349
350 + jetpackObserveUnloadedWidgets();
351 +
296 352 var unloadedWidgetsInView = jetpackGetUnloadedWidgetsInView();
297 353
298 354 if ( unloadedWidgetsInView.length > 0 ) {
299 355 // Grab any unloaded widgets for a batch request
@@ -357,13 +413,26 @@
357 413 wrapper.classList.remove( 'jetpack-likes-widget-unloaded' );
358 414 wrapper.classList.add( 'jetpack-likes-widget-loading' );
359 415
360 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 +
361 424 JetpackLikesPostMessage(
362 425 { event: 'loadLikeWidget', name: e.target.name, width: e.target.width },
363 426 window.frames[ 'likes-master' ]
364 427 );
365 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 +
366 435 wrapper.classList.remove( 'jetpack-likes-widget-loading' );
367 436 wrapper.classList.add( 'jetpack-likes-widget-loaded' );
368 437 } );
369 438 }
@@ -397,12 +466,15 @@
397 466 widgetWrapper.classList.remove( 'jetpack-likes-widget-loaded' );
398 467 widgetWrapper.classList.remove( 'jetpack-likes-widget-loading' );
399 468 widgetWrapper.classList.add( 'jetpack-likes-widget-unloaded' );
400 469
401 - // Bring back the loading placeholder into view.
402 - widgetWrapper
403 - .querySelectorAll( '.comment-likes-widget-placeholder' )
404 - .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 + }
405 477
406 478 // Remove it from the list of loaded widgets.
407 479 jetpackCommentLikesLoadedWidgets.splice( i, 1 );
408 480
@@ -420,8 +492,33 @@
420 492 };
421 493 };
422 494
423 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 +}
424 521
425 522 // Load initial batch of widgets, prior to any scrolling events.
426 523 JetpackLikesWidgetQueueHandler();
427 524