1/* global wpcom_reblog */ 2 3var jetpackLikesWidgetBatch = []; 4var 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. 10var jetpackLikesLookAhead = 2000; // pixels 11 12// Keeps track of loaded comment likes widget so we can unload them when they are scrolled out of view. 13var jetpackCommentLikesLoadedWidgets = []; 14 15var jetpackLikesDocReadyPromise = new Promise( resolve => { 16 if ( document.readyState !== 'loading' ) { 17 resolve(); 18 } else { 19 window.addEventListener( 'DOMContentLoaded', () => resolve() ); 20 } 21} ); 22 23function 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 47function 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 92function 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 313window.addEventListener( 'message', JetpackLikesMessageListener ); 314 315function 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 331document.addEventListener( 'click', hideLikersPopover ); 332 333function 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 jetpackObserveUnloadedWidgets(); 351 352 var unloadedWidgetsInView = jetpackGetUnloadedWidgetsInView(); 353 354 if ( unloadedWidgetsInView.length > 0 ) { 355 // Grab any unloaded widgets for a batch request 356 JetpackLikesBatchHandler(); 357 } 358 359 for ( var i = 0, length = unloadedWidgetsInView.length; i <= length - 1; i++ ) { 360 wrapperID = unloadedWidgetsInView[ i ].id; 361 362 if ( ! wrapperID ) { 363 continue; 364 } 365 366 jetpackLoadLikeWidgetIframe( wrapperID ); 367 } 368} 369 370function jetpackLoadLikeWidgetIframe( wrapperID ) { 371 if ( typeof wrapperID === 'undefined' ) { 372 return; 373 } 374 375 const wrapper = document.querySelector( '#' + wrapperID ); 376 wrapper.querySelectorAll( 'iframe' ).forEach( iFrame => iFrame.remove() ); 377 378 const placeholder = wrapper.querySelector( '.likes-widget-placeholder' ); 379 380 // Post like iframe 381 if ( placeholder && placeholder.classList.contains( 'post-likes-widget-placeholder' ) ) { 382 const postLikesFrame = document.createElement( 'iframe' ); 383 384 postLikesFrame.classList.add( 'post-likes-widget', 'jetpack-likes-widget' ); 385 postLikesFrame.name = wrapper.dataset.name; 386 postLikesFrame.src = wrapper.dataset.src; 387 postLikesFrame.height = '55px'; 388 postLikesFrame.width = '100%'; 389 postLikesFrame.frameBorder = '0'; 390 postLikesFrame.scrolling = 'no'; 391 postLikesFrame.title = wrapper.dataset.title; 392 393 placeholder.after( postLikesFrame ); 394 } 395 396 // Comment like iframe 397 if ( placeholder.classList.contains( 'comment-likes-widget-placeholder' ) ) { 398 const commentLikesFrame = document.createElement( 'iframe' ); 399 400 commentLikesFrame.class = 'comment-likes-widget-frame jetpack-likes-widget-frame'; 401 commentLikesFrame.name = wrapper.dataset.name; 402 commentLikesFrame.src = wrapper.dataset.src; 403 commentLikesFrame.height = '18px'; 404 commentLikesFrame.width = '100%'; 405 commentLikesFrame.frameBorder = '0'; 406 commentLikesFrame.scrolling = 'no'; 407 408 wrapper.querySelector( '.comment-like-feedback' ).after( commentLikesFrame ); 409 410 jetpackCommentLikesLoadedWidgets.push( commentLikesFrame ); 411 } 412 413 wrapper.classList.remove( 'jetpack-likes-widget-unloaded' ); 414 wrapper.classList.add( 'jetpack-likes-widget-loading' ); 415 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 424 JetpackLikesPostMessage( 425 { event: 'loadLikeWidget', name: e.target.name, width: e.target.width }, 426 window.frames[ 'likes-master' ] 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 435 wrapper.classList.remove( 'jetpack-likes-widget-loading' ); 436 wrapper.classList.add( 'jetpack-likes-widget-loaded' ); 437 } ); 438} 439 440function jetpackGetUnloadedWidgetsInView() { 441 const unloadedWidgets = document.querySelectorAll( 'div.jetpack-likes-widget-unloaded' ); 442 443 return [ ...unloadedWidgets ].filter( item => jetpackIsScrolledIntoView( item ) ); 444} 445 446function jetpackIsScrolledIntoView( element ) { 447 const top = element.getBoundingClientRect().top; 448 const bottom = element.getBoundingClientRect().bottom; 449
450 // Allow some slack above and bellow the fold with jetpackLikesLookAhead, 451 // with the aim of hiding the transition from unloaded to loaded widget from users. 452 return top + jetpackLikesLookAhead >= 0 && bottom <= window.innerHeight + jetpackLikesLookAhead; 453} 454 455function jetpackUnloadScrolledOutWidgets() { 456 for ( let i = jetpackCommentLikesLoadedWidgets.length - 1; i >= 0; i-- ) { 457 const currentWidgetIframe = jetpackCommentLikesLoadedWidgets[ i ]; 458 459 if ( ! jetpackIsScrolledIntoView( currentWidgetIframe ) ) { 460 const widgetWrapper = 461 currentWidgetIframe && 462 currentWidgetIframe.parentElement && 463 currentWidgetIframe.parentElement.parentElement; 464 465 // Restore parent class to 'unloaded' so this widget can be picked up by queue manager again if needed. 466 widgetWrapper.classList.remove( 'jetpack-likes-widget-loaded' ); 467 widgetWrapper.classList.remove( 'jetpack-likes-widget-loading' ); 468 widgetWrapper.classList.add( 'jetpack-likes-widget-unloaded' ); 469 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 } 477 478 // Remove it from the list of loaded widgets. 479 jetpackCommentLikesLoadedWidgets.splice( i, 1 ); 480 481 // Remove comment like widget iFrame. 482 currentWidgetIframe.remove(); 483 } 484 } 485} 486 487var jetpackWidgetsDelayedExec = function ( after, fn ) { 488 var timer; 489 return function () { 490 clearTimeout( timer ); 491 timer = setTimeout( fn, after ); 492 }; 493}; 494 495var 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. 500var 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. 512function jetpackObserveUnloadedWidgets() { 513 if ( ! jetpackLikesWidgetObserver ) { 514 return; 515 } 516 517 document 518 .querySelectorAll( 'div.jetpack-likes-widget-unloaded' ) 519 .forEach( widget => jetpackLikesWidgetObserver.observe( widget ) ); 520} 521 522// Load initial batch of widgets, prior to any scrolling events. 523JetpackLikesWidgetQueueHandler(); 524 525// Add event listener to execute queue handler after scroll. 526window.addEventListener( 'scroll', jetpackOnScrollStopped, true );
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.