1/* global subscriptionData */ 2document.addEventListener( 'DOMContentLoaded', function () { 3 const modal = document.getElementsByClassName( 'jetpack-subscription-modal' )[ 0 ]; 4 5 if ( ! modal ) { 6 return; 7 } 8 9 const close = document.getElementsByClassName( 'jetpack-subscription-modal__close' )[ 0 ]; 10 11 let redirectUrl = ''; 12 let hasLoaded = false; 13 14 function reloadOnCloseSubscriptionModal( customUrl ) { 15 const destinationUrl = customUrl ? new URL( customUrl ) : new URL( redirectUrl ); 16 17 // Prevent redirect to external sites. 18 if ( destinationUrl.hostname !== window.location.hostname ) { 19 return; 20 } 21 22 try { 23 localStorage.setItem( 24 'jetpack-subscription-modal-on-comment-scroll-to', 25 destinationUrl.hash 26 ); 27 } catch { 28 // Ok if we can't set it. 29 } 30 31 // Add cache-busting parameter 32 destinationUrl.searchParams.set( '_ctn', Date.now() ); 33 window.location.href = destinationUrl.toString(); 34 } 35 36 function JetpackSubscriptionModalOnCommentMessageListener( event ) { 37 let message = event && event.data; 38 if ( typeof message === 'string' ) { 39 try { 40 message = JSON.parse( message ); 41 } catch { 42 return; 43 } 44 } 45 46 const type = message && message.type; 47 const data = message && message.data; 48 49 if ( type !== 'subscriptionModalShow' || typeof data.url === 'undefined' ) { 50 return; 51 } 52 53 if ( subscriptionData.homeUrl !== event.origin ) { 54 return; 55 } 56 57 if ( data.email ) { 58 const emailInput = document.querySelector( 59 '.jetpack-subscription-modal__modal-content input[type="email"]' 60 ); 61 if ( ! emailInput ) { 62 reloadOnCloseSubscriptionModal( data.url ); 63 return; 64 } 65 66 const appSource = document.querySelector( 67 '.jetpack-subscription-modal__modal-content input[name="app_source"]' 68 ); 69 if ( ! appSource ) { 70 reloadOnCloseSubscriptionModal( data.url ); 71 return; 72 } 73 74 emailInput.value = data.email; 75 if ( data.is_logged_in ) { 76 emailInput.setAttribute( 'readonly', 'readonly' ); 77 appSource.value = 'atomic-subscription-modal-li'; 78 } 79 } 80 81 if ( ! hasLoaded ) { 82 try { 83 const storedCount = parseInt( 84 sessionStorage.getItem( 'jetpack-subscription-modal-shown-count' ) 85 ); 86 const showCount = ( isNaN( storedCount ) ? 0 : storedCount ) + 1; 87 sessionStorage.setItem( 'jetpack-subscription-modal-shown-count', showCount ); 88 89 if ( showCount > 5 ) { 90 new Image().src = 91 document.location.protocol + 92 '//pixel.wp.com/g.gif?v=wpcom-no-pv&x_jetpack-subscribe-modal-comm=hidden_views_limit&r=' + 93 Math.random(); 94 95 reloadOnCloseSubscriptionModal( data.url ); 96 return; 97 } 98 } catch { 99 // Ignore any errors. 100 } 101 102 new Image().src = 103 document.location.protocol + 104 '//pixel.wp.com/g.gif?v=wpcom-no-pv&x_jetpack-subscribe-modal-comm=showed&r=' + 105 Math.random(); 106 107 modal.classList.toggle( 'open' ); 108 hasLoaded = true; 109 redirectUrl = data.url; 110 } 111 } 112 113 window.addEventListener( 'message', JetpackSubscriptionModalOnCommentMessageListener ); 114 115 if ( close ) { 116 close.onclick = function ( event ) { 117 event.preventDefault(); 118 modal.classList.toggle( 'open' ); 119 reloadOnCloseSubscriptionModal(); 120 }; 121 } 122 123 window.onclick = function ( event ) { 124 if ( event.target === modal ) { 125 modal.style.display = 'none'; 126 reloadOnCloseSubscriptionModal(); 127 } 128 }; 129 130 window.addEventListener( 'load', () => { 131 // Scroll to the last comment. 132 const subscriptionScroll = localStorage.getItem( 133 'jetpack-subscription-modal-on-comment-scroll-to' 134 ); 135 136 if ( subscriptionScroll ) { 137 window.location.hash = subscriptionScroll; 138 localStorage.removeItem( 'jetpack-subscription-modal-on-comment-scroll-to' ); 139 140 const comment = document.querySelector( subscriptionScroll ); 141 if ( comment ) { 142 comment.scrollIntoView( { block: 'center', behavior: 'smooth' } ); 143 } 144 } 145 } ); 146} ); 147; 148/* global wpcom_reblog */ 149 150var jetpackLikesWidgetBatch = []; 151var jetpackLikesMasterReady = false; 152 153// Due to performance problems on pages with a large number of widget iframes that need to be loaded, 154// we are limiting the processing at any instant to unloaded widgets that are currently in viewport, 155// plus this constant that will allow processing of widgets above and bellow the current fold. 156// This aim of it is to improve the UX and hide the transition from unloaded to loaded state from users. 157var jetpackLikesLookAhead = 2000; // pixels 158 159// Keeps track of loaded comment likes widget so we can unload them when they are scrolled out of view. 160var jetpackCommentLikesLoadedWidgets = []; 161 162var jetpackLikesDocReadyPromise = new Promise( resolve => { 163 if ( document.readyState !== 'loading' ) { 164 resolve(); 165 } else { 166 window.addEventListener( 'DOMContentLoaded', () => resolve() ); 167 } 168} ); 169 170function JetpackLikesPostMessage( message, target ) { 171 if ( typeof message === 'string' ) { 172 try { 173 message = JSON.parse( message ); 174 } catch { 175 return; 176 } 177 } 178 179 if ( target && typeof target.postMessage === 'function' ) { 180 try { 181 target.postMessage( 182 JSON.stringify( { 183 type: 'likesMessage', 184 data: message, 185 } ), 186 '*' 187 ); 188 } catch { 189 // Ignore error 190 } 191 } 192} 193 194function JetpackLikesBatchHandler() { 195 const requests = []; 196 document.querySelectorAll( 'div.jetpack-likes-widget-unloaded' ).forEach( widget => { 197 if ( jetpackLikesWidgetBatch.indexOf( widget.id ) > -1 ) { 198 return; 199 } 200 201 if ( ! jetpackIsScrolledIntoView( widget ) ) { 202 return; 203 } 204 205 jetpackLikesWidgetBatch.push( widget.id ); 206 207 var regex = /like-(post|comment)-wrapper-(\d+)-(\d+)-(\w+)/, 208 match = regex.exec( widget.id ), 209 info; 210 211 if ( ! match || match.length !== 5 ) { 212 return; 213 } 214 215 info = { 216 blog_id: match[ 2 ], 217 width: widget.width, 218 }; 219 220 if ( 'post' === match[ 1 ] ) { 221 info.post_id = match[ 3 ]; 222 } else if ( 'comment' === match[ 1 ] ) { 223 info.comment_id = match[ 3 ]; 224 } 225 226 info.obj_id = match[ 4 ]; 227 228 requests.push( info ); 229 } ); 230 231 if ( requests.length > 0 ) { 232 JetpackLikesPostMessage( 233 { event: 'initialBatch', requests: requests }, 234 window.frames[ 'likes-master' ] 235 ); 236 } 237} 238 239function JetpackLikesMessageListener( event ) { 240 let message = event && event.data; 241 if ( typeof message === 'string' ) { 242 try { 243 message = JSON.parse( message ); 244 } catch { 245 return; 246 } 247 } 248 249 const type = message && message.type; 250 const data = message && message.data; 251 252 if ( type !== 'likesMessage' || typeof data.event === 'undefined' ) { 253 return; 254 } 255 256 // We only allow messages from one origin 257 const allowedOrigin = 'https://widgets.wp.com'; 258 if ( allowedOrigin !== event.origin ) { 259 return; 260 } 261 262 switch ( data.event ) { 263 case 'masterReady': 264 jetpackLikesDocReadyPromise.then( () => { 265 jetpackLikesMasterReady = true; 266 267 const stylesData = { 268 event: 'injectStyles', 269 }; 270 const sdTextColor = document.querySelector( '.sd-text-color' ); 271 const sdLinkColor = document.querySelector( '.sd-link-color' ); 272 const sdTextColorStyles = ( sdTextColor && getComputedStyle( sdTextColor ) ) || {}; 273 const sdLinkColorStyles = ( sdLinkColor && getComputedStyle( sdLinkColor ) ) || {}; 274 275 // enable reblogs if we're on a single post page 276 if ( document.body.classList.contains( 'single' ) ) { 277 JetpackLikesPostMessage( { event: 'reblogsEnabled' }, window.frames[ 'likes-master' ] ); 278 } 279 280 stylesData.textStyles = { 281 color: sdTextColorStyles.color,
282 fontFamily: sdTextColorStyles[ 'font-family' ], 283 fontSize: sdTextColorStyles[ 'font-size' ], 284 direction: sdTextColorStyles.direction, 285 fontWeight: sdTextColorStyles[ 'font-weight' ], 286 fontStyle: sdTextColorStyles[ 'font-style' ], 287 textDecoration: sdTextColorStyles[ 'text-decoration' ], 288 }; 289 290 stylesData.linkStyles = { 291 color: sdLinkColorStyles.color, 292 fontFamily: sdLinkColorStyles[ 'font-family' ], 293 fontSize: sdLinkColorStyles[ 'font-size' ], 294 textDecoration: sdLinkColorStyles[ 'text-decoration' ], 295 fontWeight: sdLinkColorStyles[ 'font-weight' ], 296 fontStyle: sdLinkColorStyles[ 'font-style' ], 297 }; 298 299 JetpackLikesPostMessage( stylesData, window.frames[ 'likes-master' ] ); 300 301 JetpackLikesBatchHandler(); 302 } ); 303 304 break; 305 306 // We're keeping this for planned future follow ups. 307 // @see: https://github.com/Automattic/jetpack/pull/42361#discussion_r1995338815 308 case 'showLikeWidget': 309 break; 310 311 // We're keeping this for planned future follow ups. 312 // @see: https://github.com/Automattic/jetpack/pull/42361#discussion_r1995338815 313 case 'showCommentLikeWidget': 314 break; 315 316 case 'killCommentLikes': 317 // If kill switch for comment likes is enabled remove all widgets wrappers and `Loading...` placeholders. 318 document 319 .querySelectorAll( '.jetpack-comment-likes-widget-wrapper' ) 320 .forEach( wrapper => wrapper.remove() ); 321 break; 322 323 case 'clickReblogFlair': 324 if ( wpcom_reblog && typeof wpcom_reblog.toggle_reblog_box_flair === 'function' ) { 325 wpcom_reblog.toggle_reblog_box_flair( data.obj_id ); 326 } 327 break; 328 329 case 'hideOtherGravatars': { 330 hideLikersPopover(); 331 break; 332 } 333 334 case 'showOtherGravatars': { 335 const container = document.querySelector( '#likes-other-gravatars' ); 336 337 if ( ! container ) { 338 break; 339 } 340 341 const list = container.querySelector( 'ul' ); 342 343 container.style.display = 'none'; 344 list.innerHTML = ''; 345 346 container 347 .querySelectorAll( '.likes-text span' ) 348 .forEach( item => ( item.textContent = data.totalLikesLabel ) ); 349 350 ( data.likers || [] ).forEach( async ( liker, index ) => { 351 if ( liker.profile_URL.substr( 0, 4 ) !== 'http' ) { 352 // We only display gravatars with http or https schema 353 return; 354 } 355 356 const element = document.createElement( 'li' ); 357 list.append( element ); 358 359 const profileLink = encodeURI( liker.profile_URL ); 360 const avatarLink = encodeURI( liker.avatar_URL ); 361 element.innerHTML = `<a href="${ profileLink }" rel="nofollow" target="_parent" class="wpl-liker"> 362 <img src="${ avatarLink }" 363 alt="" 364 style="width: 28px; height: 28px;" /> 365 <span></span> 366 </a>`; 367 368 // Add some extra attributes through native methods, to ensure strings are sanitized. 369 element.classList.add( liker.css_class ); 370 element.querySelector( 'img' ).alt = data.avatarAltTitle.replace( '%s', liker.name ); 371 element.querySelector( 'span' ).innerText = liker.name; 372 373 if ( index === data.likers.length - 1 ) { 374 element.addEventListener( 'keydown', e => { 375 if ( e.key === 'Tab' && ! e.shiftKey ) { 376 e.preventDefault(); 377 hideLikersPopover(); 378 379 JetpackLikesPostMessage( 380 { event: 'focusLikesCount', parent: data.parent }, 381 window.frames[ 'likes-master' ] 382 ); 383 } 384 } ); 385 } 386 } ); 387 388 const positionPopup = function () { 389 const containerStyle = getComputedStyle( container ); 390 const isRtl = containerStyle.direction === 'rtl'; 391 392 const el = document.querySelector( `*[name='${ data.parent }']` ); 393 const rect = el.getBoundingClientRect(); 394 const win = el.ownerDocument.defaultView; 395 const offset = { 396 top: rect.top + win.pageYOffset, 397 left: rect.left + win.pageXOffset, 398 }; 399 400 let containerLeft; 401 container.style.top = offset.top + data.position.top - 1 + 'px'; 402 403 if ( isRtl ) { 404 const visibleAvatarsCount = data && data.likers ? Math.min( data.likers.length, 5 ) : 0; 405 // 24px is the width of the avatar + 4px is the padding between avatars 406 containerLeft = offset.left + data.position.left + 24 * visibleAvatarsCount + 4; 407 container.style.transform = 'translateX(-100%)'; 408 } else { 409 containerLeft = offset.left + data.position.left; 410 } 411 container.style.left = containerLeft + 'px'; 412 413 // If the popup overflows viewport width, we should show it on the next line. 414 // Push it offscreen to calculated rendered width. 415 container.style.left = '-9999px'; 416 container.style.display = 'block'; 417 418 // If the popup exceeds the viewport width, 419 // flip the position of the popup. 420 const containerWidth = container.offsetWidth; 421 const containerRight = containerLeft + containerWidth; 422 if ( containerRight > win.innerWidth ) { 423 containerLeft = rect.right - containerWidth; 424 } 425 426 // Set the container left 427 container.style.left = containerLeft + 'px'; 428 container.setAttribute( 'aria-hidden', 'false' ); 429 }; 430 431 positionPopup(); 432 container.focus(); 433 434 const debounce = function ( func, wait ) { 435 var timeout; 436 return function () { 437 var context = this; 438 var args = arguments; 439 clearTimeout( timeout ); 440 timeout = setTimeout( function () { 441 func.apply( context, args ); 442 }, wait ); 443 }; 444 }; 445 446 const debouncedPositionPopup = debounce( positionPopup, 100 ); 447 448 // Keep a reference of this function in the element itself 449 // so that we can destroy it later 450 container.__resizeHandler = debouncedPositionPopup; 451 452 // When window is resized, resize the popup. 453 window.addEventListener( 'resize', debouncedPositionPopup ); 454 455 container.focus(); 456 } 457 } 458} 459 460window.addEventListener( 'message', JetpackLikesMessageListener ); 461 462function hideLikersPopover() { 463 const container = document.querySelector( '#likes-other-gravatars' ); 464 465 if ( container ) { 466 container.style.display = 'none'; 467 container.setAttribute( 'aria-hidden', 'true' ); 468 469 // Remove the resize event listener and cleanup. 470 const resizeHandler = container.__resizeHandler; 471 if ( resizeHandler ) { 472 window.removeEventListener( 'resize', resizeHandler ); 473 delete container.__resizeHandler; 474 } 475 } 476} 477 478document.addEventListener( 'click', hideLikersPopover ); 479 480function JetpackLikesWidgetQueueHandler() { 481 var wrapperID; 482 483 if ( ! jetpackLikesMasterReady ) { 484 // The master iframe emits `masterReady` a single time, when it finishes loading. On 485 // script-heavy pages it can finish loading â and emit â before this script attaches its 486 // message listener above, so that one event is missed and the queue never starts. Ping the 487 // master iframe while we wait so it re-emits `masterReady` once both it and our listener 488 // are ready. 489 JetpackLikesPostMessage( { event: 'queryMasterReady' }, window.frames[ 'likes-master' ] ); 490 setTimeout( JetpackLikesWidgetQueueHandler, 500 ); 491 return; 492 } 493 494 // Restore widgets to initial unloaded state when they are scrolled out of view. 495 jetpackUnloadScrolledOutWidgets(); 496 497 jetpackObserveUnloadedWidgets(); 498 499 var unloadedWidgetsInView = jetpackGetUnloadedWidgetsInView(); 500 501 if ( unloadedWidgetsInView.length > 0 ) { 502 // Grab any unloaded widgets for a batch request 503 JetpackLikesBatchHandler(); 504 } 505 506 for ( var i = 0, length = unloadedWidgetsInView.length; i <= length - 1; i++ ) { 507 wrapperID = unloadedWidgetsInView[ i ].id; 508 509 if ( ! wrapperID ) { 510 continue; 511 } 512 513 jetpackLoadLikeWidgetIframe( wrapperID ); 514 } 515} 516 517function jetpackLoadLikeWidgetIframe( wrapperID ) { 518 if ( typeof wrapperID === 'undefined' ) { 519 return; 520 } 521 522 const wrapper = document.querySelector( '#' + wrapperID ); 523 wrapper.querySelectorAll( 'iframe' ).forEach( iFrame => iFrame.remove() ); 524 525 const placeholder = wrapper.querySelector( '.likes-widget-placeholder' ); 526 527 // Post like iframe 528 if ( placeholder && placeholder.classList.contains( 'post-likes-widget-placeholder' ) ) { 529 const postLikesFrame = document.createElement( 'iframe' ); 530 531 postLikesFrame.classList.add( 'post-likes-widget', 'jetpack-likes-widget' ); 532 postLikesFrame.name = wrapper.dataset.name; 533 postLikesFrame.src = wrapper.dataset.src; 534 postLikesFrame.height = '55px'; 535 postLikesFrame.width = '100%'; 536 postLikesFrame.frameBorder = '0'; 537 postLikesFrame.scrolling = 'no'; 538 postLikesFrame.title = wrapper.dataset.title; 539 540 placeholder.after( postLikesFrame ); 541 } 542 543 // Comment like iframe 544 if ( placeholder.classList.contains( 'comment-likes-widget-placeholder' ) ) { 545 const commentLikesFrame = document.createElement( 'iframe' ); 546 547 commentLikesFrame.class = 'comment-likes-widget-frame jetpack-likes-widget-frame'; 548 commentLikesFrame.name = wrapper.dataset.name; 549 commentLikesFrame.src = wrapper.dataset.src; 550 commentLikesFrame.height = '18px'; 551 commentLikesFrame.width = '100%'; 552 commentLikesFrame.frameBorder = '0'; 553 commentLikesFrame.scrolling = 'no'; 554 555 wrapper.querySelector( '.comment-like-feedback' ).after( commentLikesFrame ); 556 557 jetpackCommentLikesLoadedWidgets.push( commentLikesFrame ); 558 } 559 560 wrapper.classList.remove( 'jetpack-likes-widget-unloaded' ); 561 wrapper.classList.add( 'jetpack-likes-widget-loading' ); 562 563 wrapper.querySelector( 'iframe' ).addEventListener( 'load', e => { 564 // A widget scrolled out of view mid-load has its iframe dropped, and a reloaded one 565 // replaces it, either of which leaves this closure holding a detached iframe. Acting on 566 // it would hide a placeholder that is now the only thing the widget has left to show. 567 if ( ! wrapper.contains( e.target ) ) { 568 return; 569 } 570 571 JetpackLikesPostMessage( 572 { event: 'loadLikeWidget', name: e.target.name, width: e.target.width }, 573 window.frames[ 'likes-master' ] 574 ); 575 576 // The stylesheet is otherwise the only thing that hides the placeholder, so one that never 577 // applies leaves "Loadingâ¦" sitting over a widget that loaded fine. 578 if ( placeholder ) { 579 placeholder.style.display = 'none'; 580 } 581 582 wrapper.classList.remove( 'jetpack-likes-widget-loading' ); 583 wrapper.classList.add( 'jetpack-likes-widget-loaded' ); 584 } ); 585} 586 587function jetpackGetUnloadedWidgetsInView() { 588 const unloadedWidgets = document.querySelectorAll( 'div.jetpack-likes-widget-unloaded' ); 589 590 return [ ...unloadedWidgets ].filter( item => jetpackIsScrolledIntoView( item ) ); 591} 592 593function jetpackIsScrolledIntoView( element ) { 594 const top = element.getBoundingClientRect().top; 595 const bottom = element.getBoundingClientRect().bottom; 596
597 // Allow some slack above and bellow the fold with jetpackLikesLookAhead, 598 // with the aim of hiding the transition from unloaded to loaded widget from users. 599 return top + jetpackLikesLookAhead >= 0 && bottom <= window.innerHeight + jetpackLikesLookAhead; 600} 601 602function jetpackUnloadScrolledOutWidgets() { 603 for ( let i = jetpackCommentLikesLoadedWidgets.length - 1; i >= 0; i-- ) { 604 const currentWidgetIframe = jetpackCommentLikesLoadedWidgets[ i ]; 605 606 if ( ! jetpackIsScrolledIntoView( currentWidgetIframe ) ) { 607 const widgetWrapper = 608 currentWidgetIframe && 609 currentWidgetIframe.parentElement && 610 currentWidgetIframe.parentElement.parentElement; 611 612 // Restore parent class to 'unloaded' so this widget can be picked up by queue manager again if needed. 613 widgetWrapper.classList.remove( 'jetpack-likes-widget-loaded' ); 614 widgetWrapper.classList.remove( 'jetpack-likes-widget-loading' ); 615 widgetWrapper.classList.add( 'jetpack-likes-widget-unloaded' ); 616 617 // An empty string removes the inline declaration rather than setting one, handing the 618 // placeholder back to the stylesheet. The `display: none` set on load would otherwise 619 // outrank it, leaving an unloaded widget with neither an iframe nor a placeholder. 620 const placeholder = widgetWrapper.querySelector( '.likes-widget-placeholder' ); 621 if ( placeholder ) { 622 placeholder.style.display = ''; 623 } 624 625 // Remove it from the list of loaded widgets. 626 jetpackCommentLikesLoadedWidgets.splice( i, 1 ); 627 628 // Remove comment like widget iFrame. 629 currentWidgetIframe.remove(); 630 } 631 } 632} 633 634var jetpackWidgetsDelayedExec = function ( after, fn ) { 635 var timer; 636 return function () { 637 clearTimeout( timer ); 638 timer = setTimeout( fn, after ); 639 }; 640}; 641 642var jetpackOnScrollStopped = jetpackWidgetsDelayedExec( 250, JetpackLikesWidgetQueueHandler ); 643 644// Scrolling is not the only thing that brings a widget into range. A stylesheet that lands late 645// reflows the page without firing a scroll event, and the queue would never look at that widget 646// again, leaving it on "Loadingâ¦" until the reader happens to scroll. 647var jetpackLikesWidgetObserver = 648 typeof IntersectionObserver === 'function' 649 ? new IntersectionObserver( jetpackOnScrollStopped, { 650 rootMargin: `${ jetpackLikesLookAhead }px`, 651 // jetpackIsScrolledIntoView() wants the widget fully inside the band, so ask to be 652 // told when it gets there - crossing into partial overlap alone would not load it. 653 threshold: [ 0, 1 ], 654 } ) 655 : null; 656 657// Observing an element twice is a no-op, so every queue pass can call this to pick up widgets 658// added after load. 659function jetpackObserveUnloadedWidgets() { 660 if ( ! jetpackLikesWidgetObserver ) { 661 return; 662 } 663 664 document 665 .querySelectorAll( 'div.jetpack-likes-widget-unloaded' ) 666 .forEach( widget => jetpackLikesWidgetObserver.observe( widget ) ); 667} 668 669// Load initial batch of widgets, prior to any scrolling events. 670JetpackLikesWidgetQueueHandler(); 671 672// Add event listener to execute queue handler after scroll. 673window.addEventListener( 'scroll', jetpackOnScrollStopped, true ); 674;
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.