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 ( e ) { 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 ( e ) { 42 return; 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 ( err ) { 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 they are enabled for the page 129 if ( document.body.classList.contains( 'jetpack-reblog-enabled' ) ) { 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, data.post_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 element.innerHTML = ` 213 <a href="${ encodeURI( liker.profile_URL ) }" rel="nofollow" target="_parent" class="wpl-liker"> 214 <img src="${ encodeURI( liker.avatar_URL ) }" 215 alt="" 216 style="width: 28px; height: 28px;" /> 217 <span></span> 218 </a> 219 `; 220 221 222 // Add some extra attributes through native methods, to ensure strings are sanitized. 223 element.classList.add( liker.css_class ); 224 element.querySelector( 'img' ).alt = data.avatarAltTitle.replace( '%s', liker.name ); 225 element.querySelector( 'span' ).innerText = liker.name; 226 227 if ( index === data.likers.length - 1 ) { 228 element.addEventListener( 'keydown', ( e ) => { 229 if ( e.key === 'Tab' && ! e.shiftKey ) { 230 e.preventDefault(); 231 hideLikersPopover(); 232 233 JetpackLikesPostMessage( 234 { event: 'focusLikesCount', parent: data.parent }, 235 window.frames[ 'likes-master' ] 236 ); 237 } 238 } ); 239 } 240 } ); 241 242 const positionPopup = function() { 243 const containerStyle = getComputedStyle(container); 244 const isRtl = containerStyle.direction === 'rtl'; 245 246 const el = document.querySelector( `*[name='${ data.parent }']` ); 247 const rect = el.getBoundingClientRect(); 248 const win = el.ownerDocument.defaultView; 249 250 const offset = { 251 top: rect.top + win.pageYOffset, 252 left: rect.left + win.pageXOffset, 253 }; 254 255 // don't display yet or we get skewed window.innerWidth later 256 container.style.display = 'none'; 257 258 let containerLeft = 0; 259 container.style.top = offset.top + data.position.top - 1 + 'px'; 260 261 if ( isRtl ) { 262 const visibleAvatarsCount = data && data.likers ? Math.min( data.likers.length, 5 ) : 0; 263 // 24px is the width of the avatar + 4px is the padding between avatars 264 containerLeft = offset.left + data.position.left + 24 * visibleAvatarsCount + 4; 265 container.style.transform = 'translateX(-100%)'; 266 } else { 267 containerLeft = offset.left + data.position.left; 268 } 269 container.style.left = containerLeft + 'px'; 270 271 // Container width - padding
272 const initContainerWidth = data.width - 20; 273 const rowLength = Math.floor( initContainerWidth / 37 ); 274 // # of rows + (avatar + avatar padding) + text above + container padding 275 let height = Math.ceil( data.likers.length / rowLength ) * 37 + 17 + 22; 276 if ( height > 204 ) { 277 height = 204; 278 } 279 280 // If the popup is overflows viewport width, we should show it on the next line 281 // Push it offscreen to calculated rendered width 282 const windowWidth = win.innerWidth; 283 container.style.left = '-9999px'; 284 container.style.display = 'block'; 285 286 // If the popup exceeds the viewport width, 287 // flip the position of the popup. 288 const containerWidth = container.offsetWidth; 289 const containerRight = containerLeft + containerWidth; 290 291 if ( containerRight > windowWidth && ! isRtl) { 292 containerLeft = rect.left + rect.width - containerWidth; 293 } else if ( containerLeft - containerWidth < 0 && isRtl ) { 294 container.style.transform = 'none'; 295 containerLeft = rect.left; 296 } 297 // Set the container left 298 container.style.left = containerLeft + 'px'; 299 300 container.setAttribute( 'aria-hidden', 'false' ); 301 } 302 303 positionPopup(); 304 container.focus(); 305 306 const debounce = function( func, wait ) { 307 var timeout; 308 return function() { 309 var context = this; 310 var args = arguments; 311 clearTimeout( timeout ); 312 timeout = setTimeout( function() { 313 func.apply( context, args ); 314 }, wait ); 315 }; 316 }; 317 318 const debouncedPositionPopup = debounce( positionPopup, 100 ); 319 320 // Keep a reference of this function in the element itself 321 // so that we can destroy it later 322 container.__resizeHandler = debouncedPositionPopup; 323 324 // When window is resized, resize the popup. 325 window.addEventListener( "resize", debouncedPositionPopup ); 326 } 327 } 328} 329 330window.addEventListener( 'message', JetpackLikesMessageListener ); 331 332function hideLikersPopover() { 333 const container = document.querySelector( '#likes-other-gravatars' ); 334 335 if ( container ) { 336 container.style.display = 'none'; 337 container.setAttribute( 'aria-hidden', 'true' ); 338 339 // Remove the resize event listener and cleanup. 340 const resizeHandler = container.__resizeHandler; 341 if ( resizeHandler ) { 342 window.removeEventListener( "resize", resizeHandler ); 343 delete container.__resizeHandler; 344 } 345 } 346} 347 348document.addEventListener( 'click', hideLikersPopover ); 349 350function JetpackLikesWidgetQueueHandler() { 351 var wrapperID; 352 353 if ( ! jetpackLikesMasterReady ) { 354 setTimeout( JetpackLikesWidgetQueueHandler, 500 ); 355 return; 356 } 357 358 // Restore widgets to initial unloaded state when they are scrolled out of view. 359 jetpackUnloadScrolledOutWidgets(); 360 361 var unloadedWidgetsInView = jetpackGetUnloadedWidgetsInView(); 362 363 if ( unloadedWidgetsInView.length > 0 ) { 364 // Grab any unloaded widgets for a batch request 365 JetpackLikesBatchHandler(); 366 } 367 368 for ( var i = 0, length = unloadedWidgetsInView.length; i <= length - 1; i++ ) { 369 wrapperID = unloadedWidgetsInView[ i ].id; 370 371 if ( ! wrapperID ) { 372 continue; 373 } 374 375 jetpackLoadLikeWidgetIframe( wrapperID ); 376 } 377} 378 379function jetpackLoadLikeWidgetIframe( wrapperID ) { 380 if ( typeof wrapperID === 'undefined' ) { 381 return; 382 } 383 384 const wrapper = document.querySelector( '#' + wrapperID ); 385 wrapper.querySelectorAll( 'iframe' ).forEach( iFrame => iFrame.remove() ); 386 387 const placeholder = wrapper.querySelector( '.likes-widget-placeholder' ); 388 389 // Post like iframe 390 if ( placeholder && placeholder.classList.contains( 'post-likes-widget-placeholder' ) ) { 391 const postLikesFrame = document.createElement( 'iframe' ); 392 393 postLikesFrame.classList.add( 'post-likes-widget', 'jetpack-likes-widget' ); 394 postLikesFrame.name = wrapper.dataset.name; 395 postLikesFrame.src = wrapper.dataset.src; 396 postLikesFrame.height = '55px'; 397 postLikesFrame.width = '100%'; 398 postLikesFrame.frameBorder = '0'; 399 postLikesFrame.scrolling = 'no'; 400 postLikesFrame.title = wrapper.dataset.title; 401 402 placeholder.after( postLikesFrame ); 403 } 404 405 // Comment like iframe 406 if ( placeholder.classList.contains( 'comment-likes-widget-placeholder' ) ) { 407 const commentLikesFrame = document.createElement( 'iframe' ); 408 409 commentLikesFrame.class = 'comment-likes-widget-frame jetpack-likes-widget-frame'; 410 commentLikesFrame.name = wrapper.dataset.name; 411 commentLikesFrame.src = wrapper.dataset.src; 412 commentLikesFrame.height = '18px'; 413 commentLikesFrame.width = '100%'; 414 commentLikesFrame.frameBorder = '0'; 415 commentLikesFrame.scrolling = 'no'; 416 417 wrapper.querySelector( '.comment-like-feedback' ).after( commentLikesFrame ); 418 419 jetpackCommentLikesLoadedWidgets.push( commentLikesFrame ); 420 } 421 422 wrapper.classList.remove( 'jetpack-likes-widget-unloaded' ); 423 wrapper.classList.add( 'jetpack-likes-widget-loading' ); 424 425 wrapper.querySelector( 'iframe' ).addEventListener( 'load', e => { 426 JetpackLikesPostMessage( 427 { event: 'loadLikeWidget', name: e.target.name, width: e.target.width }, 428 window.frames[ 'likes-master' ] 429 ); 430 431 wrapper.classList.remove( 'jetpack-likes-widget-loading' ); 432 wrapper.classList.add( 'jetpack-likes-widget-loaded' ); 433 } ); 434} 435 436function jetpackGetUnloadedWidgetsInView() { 437 const unloadedWidgets = document.querySelectorAll( 'div.jetpack-likes-widget-unloaded' ); 438 439 return [ ...unloadedWidgets ].filter( item => jetpackIsScrolledIntoView( item ) ); 440} 441 442function jetpackIsScrolledIntoView( element ) { 443 const top = element.getBoundingClientRect().top; 444 const bottom = element.getBoundingClientRect().bottom; 445
446 // Allow some slack above and bellow the fold with jetpackLikesLookAhead, 447 // with the aim of hiding the transition from unloaded to loaded widget from users. 448 return top + jetpackLikesLookAhead >= 0 && bottom <= window.innerHeight + jetpackLikesLookAhead; 449} 450 451function jetpackUnloadScrolledOutWidgets() { 452 for ( let i = jetpackCommentLikesLoadedWidgets.length - 1; i >= 0; i-- ) { 453 const currentWidgetIframe = jetpackCommentLikesLoadedWidgets[ i ]; 454 455 if ( ! jetpackIsScrolledIntoView( currentWidgetIframe ) ) { 456 const widgetWrapper = 457 currentWidgetIframe && 458 currentWidgetIframe.parentElement && 459 currentWidgetIframe.parentElement.parentElement; 460 461 // Restore parent class to 'unloaded' so this widget can be picked up by queue manager again if needed. 462 widgetWrapper.classList.remove( 'jetpack-likes-widget-loaded' ); 463 widgetWrapper.classList.remove( 'jetpack-likes-widget-loading' ); 464 widgetWrapper.classList.add( 'jetpack-likes-widget-unloaded' ); 465 466 // Bring back the loading placeholder into view. 467 widgetWrapper 468 .querySelectorAll( '.comment-likes-widget-placeholder' ) 469 .forEach( item => ( item.style.display = 'block' ) ); 470 471 // Remove it from the list of loaded widgets. 472 jetpackCommentLikesLoadedWidgets.splice( i, 1 ); 473 474 // Remove comment like widget iFrame. 475 currentWidgetIframe.remove(); 476 } 477 } 478} 479 480var jetpackWidgetsDelayedExec = function ( after, fn ) { 481 var timer; 482 return function () { 483 clearTimeout( timer ); 484 timer = setTimeout( fn, after ); 485 }; 486}; 487 488var jetpackOnScrollStopped = jetpackWidgetsDelayedExec( 250, JetpackLikesWidgetQueueHandler ); 489 490// Load initial batch of widgets, prior to any scrolling events. 491JetpackLikesWidgetQueueHandler(); 492 493// Add event listener to execute queue handler after scroll. 494window.addEventListener( 'scroll', jetpackOnScrollStopped, true ); 495; 496/*! This file is auto-generated */ 497window.addComment=function(v){var I,C,h,E=v.document,b={commentReplyClass:"comment-reply-link",commentReplyTitleId:"reply-title",cancelReplyId:"cancel-comment-reply-link",commentFormId:"commentform",temporaryFormId:"wp-temp-form-div",parentIdFieldId:"comment_parent",postIdFieldId:"comment_post_ID"},e=v.MutationObserver||v.WebKitMutationObserver||v.MozMutationObserver,i="querySelector"in E&&"addEventListener"in v,n=!!E.documentElement.dataset;function t(){o(),e&&new e(d).observe(E.body,{childList:!0,subtree:!0})}function o(e){if(i&&(I=g(b.cancelReplyId),C=g(b.commentFormId),I)){I.addEventListener("click",l);function t(e){if((e.metaKey||e.ctrlKey)&&13===e.keyCode&&"a"!==E.activeElement.tagName.toLowerCase())return C.removeEventListener("keydown",t),e.preventDefault(),C.submit.click(),!1}C&&C.addEventListener("keydown",t);for(var n=function(e){var t=b.commentReplyClass;e&&e.childNodes||(e=E);e=E.getElementsByClassName?e.getElementsByClassName(t):e.querySelectorAll("."+t);return e}(e),o=0,d=n.length;o<d;o++)n[o].addEventListener("click",r)}}function l(e){var t,n,o=g(b.temporaryFormId);o&&h&&(g(b.parentIdFieldId).value="0",t=o.textContent,o.parentNode.replaceChild(h,o),this.style.display="none",n=(o=(o=g(b.commentReplyTitleId))&&o.firstChild)&&o.nextSibling,o&&o.nodeType===Node.TEXT_NODE&&t&&(n&&"A"===n.nodeName&&n.id!==b.cancelReplyId&&(n.style.display=""),o.textContent=t),e.preventDefault())}function r(e){var t=g(b.commentReplyTitleId),t=t&&t.firstChild.textContent,n=this,o=a(n,"belowelement"),d=a(n,"commentid"),i=a(n,"respondelement"),l=a(n,"postid"),n=a(n,"replyto")||t;o&&d&&i&&l&&!1===v.addComment.moveForm(o,d,i,l,n)&&e.preventDefault()}function d(e){for(var t=e.length;t--;)if(e[t].addedNodes.length)return void o()}function a(e,t){return n?e.dataset[t]:e.getAttribute("data-"+t)}function g(e){return E.getElementById(e)}return i&&"loading"!==E.readyState?t():i&&v.addEventListener("DOMContentLoaded",t,!1),{init:o,moveForm:function(e,t,n,o,d){var i,l,r,a,m,c,s,e=g(e),n=(h=g(n),g(b.parentIdFieldId)),y=g(b.postIdFieldId),p=g(b.commentReplyTitleId),u=(p=p&&p.firstChild)&&p.nextSibling;if(e&&h&&n){void 0===d&&(d=p&&p.textContent),a=h,m=b.temporaryFormId,c=g(m),s=(s=g(b.commentReplyTitleId))?s.firstChild.textContent:"",c||((c=E.createElement("div")).id=m,c.style.display="none",c.textContent=s,a.parentNode.insertBefore(c,a)),o&&y&&(y.value=o),n.value=t,I.style.display="",e.parentNode.insertBefore(h,e.nextSibling),p&&p.nodeType===Node.TEXT_NODE&&(u&&"A"===u.nodeName&&u.id!==b.cancelReplyId&&(u.style.display="none"),p.textContent=d),I.onclick=function(){return!1};try{for(var f=0;f<C.elements.length;f++)if(i=C.elements[f],l=!1,"getComputedStyle"in v?r=v.getComputedStyle(i):E.documentElement.currentStyle&&(r=i.currentStyle),(i.offsetWidth<=0&&i.offsetHeight<=0||"hidden"===r.visibility)&&(l=!0),"hidden"
497!==i.type&&!i.disabled&&!l){i.focus();break}}catch(e){}return!1}}}}(window);; 498/** 499 * Comment Likes - JavaScript 500 * 501 * This handles liking and unliking comments, as well as viewing who has 502 * liked a particular comment. 503 * 504 * @dependency Swipe (dynamically loaded when needed) 505 * 506 * @package Comment_Likes 507 * @subpackage JavaScript 508 */ 509(function () { 510 function init() { 511 let extWin; 512 let extWinCheck; 513 let commentLikeEvent; 514 515 // Only run once. 516 if (window.comment_likes_loaded) { 517 return; 518 } 519 window.comment_likes_loaded = true; 520 521 // Client-side cache of who liked a particular comment to avoid 522 // having to hit the server multiple times for the same data. 523 const commentLikeCache = {}; 524 525 let swipeLibPromise; 526 527 // Load the Swipe library, if it's not already loaded. 528 function swipeLibLoader() { 529 if (!swipeLibPromise) { 530 swipeLibPromise = new Promise((resolve, reject) => { 531 if (window.Swipe) { 532 resolve(window.Swipe); 533 } else { 534 const swipeScript = document.createElement('script'); 535 swipeScript.src = comment_like_text.swipeUrl; 536 swipeScript.async = true; 537 document.body.appendChild(swipeScript); 538 swipeScript.addEventListener('load', () => resolve(window.Swipe)); 539 swipeScript.addEventListener('error', error => reject(error)); 540 } 541 }); 542 } 543 return swipeLibPromise; 544 } 545 546 /** 547 * Parse the comment ID from a comment like link. 548 */ 549 function getCommentId(link) { 550 const commentId = 551 link && link.getAttribute('href') && link.getAttribute('href').split('like_comment='); 552 return commentId[1].split('&_wpnonce=')[0]; 553 } 554 555 /** 556 * Handle an ajax action on the comment like link. 557 */ 558 function handleLinkAction(link, action, commentId, callback) { 559 const nonce = 560 link && link.getAttribute('href') && link.getAttribute('href').split('_wpnonce=')[1]; 561 562 fetch('/wp-admin/admin-ajax.php', { 563 method: 'POST', 564 body: new URLSearchParams({ 565 action: action, 566 _wpnonce: nonce, 567 like_comment: commentId, 568 blog_id: Number(link.dataset.blog), 569 }), 570 headers: { 571 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 572 'X-Requested-With': 'XMLHttpRequest', 573 Accept: 'application/json', 574 'cache-control': 'no-cache', 575 pragma: 'no-cache', 576 }, 577 }) 578 .then(response => response.json()) 579 .then(callback); 580 } 581 582 function startPolling() { 583 // Append cookie polling login iframe to this window to wait for user to finish logging in (or cancel) 584 const loginIframe = document.createElement('iframe'); 585 loginIframe.id = 'wp-login-polling-iframe'; 586 loginIframe.src = 'https://wordpress.com/public.api/connect/?iframe=true'; 587 document.body.appendChild(loginIframe); 588 loginIframe.style.display = 'none'; 589 } 590 591 function stopPolling() { 592 const iframe = document.querySelector('#wp-login-polling-iframe'); 593 if (iframe) { 594 iframe.remove(); 595 } 596 } 597 598 function hide(el) { 599 if (el && el.style) { 600 el.style.display = 'none'; 601 } 602 } 603 604 function show(el) { 605 if (el && el.style) { 606 el.style.removeProperty('display'); 607 } 608 } 609 610 // Overlay used for displaying comment like info. 611 class Overlay { 612 constructor() { 613 // Overlay element. 614 this.el = document.createElement('div'); 615 this.el.classList.add('comment-likes-overlay'); 616 document.body.appendChild(this.el); 617 hide(this.el); 618 619 this.el.addEventListener('mouseenter', () => { 620 // Don't hide the overlay if the user is mousing over it. 621 overlay.cancelHide(); 622 }); 623 624 this.el.addEventListener('mouseleave', () => overlay.requestHide()); 625 626 // Inner contents of overlay. 627 this.innerEl = null; 628 629 // Instance of the Swipe library. 630 this.swipe = null; 631 632 // Timeout used for hiding the overlay. 633 this.hideTimeout = null; 634 } 635 636 // Initialise the overlay for use, removing any old content. 637 clear() { 638 // Unload any previous instance of Swipe (to avoid leaking a global 639 // event handler). This is done before clearing the contents of the 640 // overlay because Swipe expects the slides to still be present. 641 if (this.swipe) { 642 this.swipe.kill(); 643 this.swipe = null; 644 } 645 this.el.innerHTML = ''; 646 this.innerEl = document.createElement('div'); 647 this.innerEl.classList.add('inner'); 648 this.el.appendChild(this.innerEl); 649 } 650 651 /** 652 * Construct a list (<ul>) of user (gravatar, name) details. 653 * 654 * @param data liker data returned from the server 655 * @param klass CSS class to apply to the <ul> element 656 * @param start index of user to start at 657 * @param length number of users to include in the list 658 * 659 * @return A container element with the list 660 */ 661 getUserBits(data, klass, start, length) { 662 start = start || 0; 663 let last = start + (length || data.length); 664 last = last > data.length ? data.length : last; 665 const container = document.createElement('div'); 666 container.classList.add('liker-list'); 667 let html = `<ul class="${klass || ''}">`; 668 for (let i = start; i < last; ++i) { 669 const user = data[i]; 670 html += ` 671 <li> 672 <a rel="nofollow" title="${user.display_name_esc}" href="${user.profile_url_esc}"> 673 <img src="${user.avatar_url_esc}" alt="${user.display_name_esc}" /> 674 <span class="user-name">${user.display_name_esc}</span> 675 </a> 676 </li> 677 `; 678 } 679 html += '</ul>'; 680 container.innerHTML = html; 681 return container; 682 } 683 684 /** 685 * Render the display of who has liked this comment. The type of 686 * display depends on how many people have liked the comment. 687 * If more than 10 people have liked the comment, this function 688 * renders navigation controls and sets up the Swipe library for 689 * changing between pages. 690 * 691 * @param link the element over which the user is hovering 692 * @param data the results retrieved from the server 693 */ 694 showLikes(link, data) { 695 this.clear(); 696 697 link.dataset.likeCount = data.length; 698 if (data.length === 0) { 699 // No likers after all. 700 hide(this.el); 701 return; 702 } 703 704 this.innerEl.style.padding = '12px'; 705 706 if (data.length < 6) { 707 // Only one column needed. 708 this.innerEl.style.maxWidth = '200px'; 709 this.innerEl.innerHTML = ''; 710 this.innerEl.appendChild(this.getUserBits(data, 'single')); 711 this.setPosition(link); 712 } else if (data.length < 11) { 713 // Two columns, but only one page. 714 this.innerEl.innerHTML = ''; 715 this.innerEl.appendChild(this.getUserBits(data, 'double')); 716 this.setPosition(link); 717 } else { 718 // Multiple pages. 719 this.renderLikesWithPagination(data, link); 720 } 721 } 722 723 /**
724 * Render multiple pages of likes with pagination controls. 725 * This function is intended to be called by `showLikes` above. 726 * 727 * @param data the results retrieved from the server 728 */ 729 renderLikesWithPagination(data, link) { 730 swipeLibLoader().then(() => { 731 const page_count = Math.ceil(data.length / 10); 732 // Swipe requires two nested containers. 733 const swipe = document.createElement('div'); 734 swipe.classList.add('swipe'); 735 this.innerEl.appendChild(swipe); 736 737 const wrap = document.createElement('div'); 738 wrap.classList.add('swipe-wrap'); 739 swipe.appendChild(wrap); 740 741 for (let i = 0; i < page_count; ++i) { 742 wrap.appendChild(this.getUserBits(data, 'double', i * 10, 10)); 743 } 744 745 /** 746 * Navigation controls. 747 * This is based on the Newdash controls found in 748 * reader/recommendations-templates.php 749 */ 750 const nav = document.createElement('nav'); 751 nav.classList.add('slider-nav'); 752 753 let navContents = ` 754 <a href="#" class="prev"> 755 <span class="noticon noticon-previous" title="Previous" alt="<"></span> 756 </a> 757 <span class="position"> 758 `; 759 for (let i = 0; i < page_count; ++i) { 760 navContents += `<em data-page="${i}" class="${i === 0 ? 'on' : ''}">•</em>`; 761 } 762 navContents += ` 763 </span> 764 <a href="#" class="next"> 765 <span class="noticon noticon-next" title="Next" alt=">"></span> 766 </a> 767 `; 768 this.innerEl.appendChild(nav); 769 nav.innerHTML = navContents; 770 771 /** Set up Swipe. **/ 772 // Swipe cannot be set up successfully unless its container 773 // is visible, so we show it now. 774 show(this.el); 775 this.setPosition(link); 776 777 this.swipe = new Swipe(swipe, { 778 callback: function (pos) { 779 // Update the pagination indicators. 780 // 781 // If there are exactly two pages, Swipe has a weird 782 // special case where it duplicates both pages and 783 // can return index 2 and 3 even though those aren't 784 // real pages (see swipe.js, line 47). To deal with 785 // this, we use the expression `pos % page_count`. 786 pos = pos % page_count; 787 nav.querySelectorAll('em').forEach(em => { 788 const page = Number(em.dataset.page); 789 em.setAttribute('class', pos === page ? 'on' : ''); 790 }); 791 }, 792 }); 793 794 nav.querySelectorAll('em').forEach(em => { 795 em.addEventListener('click', e => { 796 // Go to the page corresponding to the indicator clicked. 797 this.swipe.slide(Number(em.dataset.page)); 798 e.preventDefault(); 799 }); 800 }); 801 // Previous and next buttons. 802 nav.querySelector('.prev').addEventListener('click', e => { 803 this.swipe.prev(); 804 e.preventDefault(); 805 }); 806 nav.querySelector('.next').addEventListener('click', e => { 807 this.swipe.next(); 808 e.preventDefault(); 809 }); 810 }); 811 } 812 813 /** 814 * Open the overlay and show a loading message. 815 */ 816 showLoadingMessage(link) { 817 this.clear(); 818 this.innerEl.textContent = comment_like_text.loading; 819 this.setPosition(link); 820 } 821 822 /** 823 * Position the overlay near the current comment. 824 * 825 * @param link element near which to position the overlay 826 */ 827 setPosition(link) { 828 // Prepare a down arrow icon for the bottom of the overlay. 829 const icon = document.createElement('span'); 830 this.el.appendChild(icon); 831 icon.classList.add('icon', 'noticon', 'noticon-downarrow'); 832 icon.style.textShadow = '0px 1px 1px rgb(223, 223, 223)'; 833 834 const rect = link.getBoundingClientRect(); 835 const win = document.defaultView; 836 const offset = { 837 top: rect.top + win.scrollY, 838 left: rect.left + win.scrollX, 839 }; 840 841 // Take measurements with the element fully visible. 842 show(this.el); 843 let left = offset.left - (this.el.offsetWidth - link.offsetWidth) / 2; 844 left = left < 5 ? 5 : left; 845 let top = offset.top - this.el.offsetHeight + 5; 846 hide(this.el); 847 848 const adminBar = document.querySelector('#wpadminbar'); 849 850 // Check if the overlay would appear off the screen. 851 if (top < win.scrollY + ((adminBar && adminBar.offsetHeight) || 0)) { 852 // We'll display the overlay beneath the link instead. 853 top = offset.top + link.offsetHeight; 854 // Instead of using the down arrow icon, use an up arrow. 855 icon.remove(); 856 this.el.prepend(icon); 857 icon.classList.remove('noticon-downarrow'); 858 icon.classList.add('noticon-uparrow'); 859 icon.style.textShadow = '0px -1px 1px rgb(223, 223, 223)'; 860 icon.style.verticalAlign = 'bottom'; 861 } 862 863 this.el.style.left = `${left}px`; 864 this.el.style.top = `${top}px`; 865 show(this.el); 866 867 // The height of the arrow icon differs slightly between browsers, 868 // so we compute the margin here to make sure it isn't disjointed 869 // from the overlay. 870 icon.style.marginTop = `${icon.scrollHeight - 26}px`; 871 icon.style.marginBottom = `${20 - icon.scrollHeight}px`; 872 873 // Position the arrow to be horizontally centred on the link. 874 icon.style.paddingLeft = `${ 875 offset.left - left + (link.offsetWidth - icon.scrollWidth) / 2 876 }px`; 877 } 878 879 /** 880 * Return whether the overlay is visible. 881 */ 882 isVisible() { 883 return this.el.style.getPropertyValue('display') !== 'none'; 884 } 885 886 /** 887 * Request that the overlay be hidden after a short delay. 888 */ 889 requestHide() { 890 if (this.hideTimeout !== null) { 891 return; 892 } 893 this.hideTimeout = setTimeout(() => { 894 hide(this.el); 895 this.clear(); 896 }, 300); 897 } 898 899 /** 900 * Cancel a request to hide the overlay. 901 */ 902 cancelHide() { 903 if (this.hideTimeout !== null) { 904 clearTimeout(this.hideTimeout); 905 this.hideTimeout = null; 906 } 907 } 908 } 909 910 // Overlay used for displaying comment like info.
911 const overlay = new Overlay(); 912 913 // The most recent comment for which the user has requested to see 914 // who liked it. 915 var relevantComment; 916 917 // Precache after this timeout. 918 var precacheTimeout = null; 919 920 /** 921 * Fetch the like data for a particular comment. 922 */ 923 function fetchLikeData(link, commentId) { 924 commentLikeCache[commentId] = null; 925 926 const container = link && link.parentElement && link.parentElement.parentElement; 927 const star = container.querySelector('a.comment-like-link'); 928 star && 929 handleLinkAction(star, 'view_comment_likes', commentId, data => { 930 // Populate the cache. 931 commentLikeCache[commentId] = data; 932 933 // Only show the overlay if the user is interested. 934 if (overlay.isVisible() && relevantComment === commentId) { 935 overlay.showLikes(link, data); 936 } 937 }); 938 } 939 940 function readCookie(c) { 941 const nameEQ = c + '='; 942 const cookieStrings = document.cookie.split(';'); 943 944 for (let i = 0; i < cookieStrings.length; i++) { 945 let cookieString = cookieStrings[i]; 946 while (cookieString.charAt(0) === ' ') { 947 cookieString = cookieString.substring(1, cookieString.length); 948 } 949 if (cookieString.indexOf(nameEQ) === 0) { 950 const chunk = cookieString.substring(nameEQ.length, cookieString.length); 951 const pairs = chunk.split('&'); 952 const cookieData = {}; 953 for (let num = pairs.length - 1; num >= 0; num--) { 954 const pair = pairs[num].split('='); 955 cookieData[pair[0]] = decodeURIComponent(pair[1]); 956 } 957 return cookieData; 958 } 959 } 960 return null; 961 } 962 963 function getServiceData() { 964 const data = readCookie('wpc_wpc'); 965 if (data === null || typeof data.access_token === 'undefined' || !data.access_token) { 966 return false; 967 } 968 return data; 969 } 970 971 function readMessage(msg) { 972 const event = msg.data; 973 974 if (typeof event.event === 'undefined') { 975 return; 976 } 977 978 if (event.event === 'login' && event.success) { 979 extWinCheck = setInterval(function () { 980 if (!extWin || extWin.closed) { 981 clearInterval(extWinCheck); 982 if (getServiceData()) { 983 // Load page in an iframe to get the current comment nonce 984 const nonceIframe = document.createElement('iframe'); 985 nonceIframe.id = 'wp-login-comment-nonce-iframe'; 986 nonceIframe.style.display = 'none'; 987 nonceIframe.src = commentLikeEvent + ''; 988 document.body.appendChild(nonceIframe); 989 990 const commentLikeId = (commentLikeEvent + '') 991 .split('like_comment=')[1] 992 .split('&_wpnonce=')[0]; 993 let c; 994 995 // Set a 5 second timeout to redirect to the comment page without doing the Like as a fallback 996 const commentLikeTimeout = setTimeout(() => { 997 window.location = commentLikeEvent; 998 }, 5000); 999 1000 // Check for a new nonced redirect and use that if available before timing out 1001 const commentLikeCheck = setInterval(() => { 1002 const iframe = document.querySelector('#wp-login-comment-nonce-iframe'); 1003 if (iframe) { 1004 c = iframe.querySelector(`#comment-like-${commentLikeId} .comment-like-link`); 1005 } 1006 if (c && typeof c.href !== 'undefined') { 1007 clearTimeout(commentLikeTimeout); 1008 clearInterval(commentLikeCheck); 1009 window.location = c.href; 1010 } 1011 }, 100); 1012 } 1013 } 1014 }, 100); 1015 1016 if (extWin) { 1017 if (!extWin.closed) { 1018 extWin.close(); 1019 } 1020 extWin = false; 1021 } 1022 1023 stopPolling(); 1024 } 1025 } 1026 1027 if (typeof window.postMessage !== 'undefined') { 1028 window.addEventListener('message', e => { 1029 let message = e && e.data; 1030 if (typeof message === 'string') { 1031 try { 1032 message = JSON.parse(message); 1033 } catch (err) { 1034 return; 1035 } 1036 } 1037 1038 const type = message && message.type; 1039 if (type === 'loginMessage') { 1040 readMessage(message); 1041 } 1042 }); 1043 } 1044 1045 document.body.addEventListener('click', e => { 1046 let target = e.target; 1047 1048 // Don't do anything when clicking on the "X people" link. 1049 if (target.matches('p.comment-likes a.view-likers')) { 1050 e.preventDefault(); 1051 return; 1052 } 1053 1054 // Retrieve the surrounding paragraph to the star, if it hasn't been liked. 1055 const notLikedPar = target.closest('p.comment-not-liked'); 1056 1057 // Return if not clicking on star or surrounding paragraph. 1058 if (!target.matches('a.comment-like-link') && !notLikedPar) { 1059 return; 1060 } 1061 1062 // When a comment hasn't been liked, make the text clickable, too. 1063 if (notLikedPar) { 1064 target = notLikedPar.querySelector('a.comment-like-link'); 1065 if (!target) { 1066 return; 1067 } 1068 } 1069 1070 if (target.classList.contains('needs-login')) { 1071 e.preventDefault(); 1072 commentLikeEvent = target; 1073 if (extWin) { 1074 if (!extWin.closed) { 1075 extWin.close(); 1076 } 1077 extWin = false;
1078 } 1079 1080 stopPolling(); 1081 1082 const url = 'https://wordpress.com/public.api/connect/?action=request&service=wordpress'; 1083 extWin = window.open( 1084 url, 1085 'likeconn', 1086 'status=0,toolbar=0,location=1,menubar=0,directories=0,resizable=1,scrollbars=1,height=560,width=500' 1087 ); 1088 1089 startPolling(); 1090 1091 return false; 1092 } 1093 1094 // Record that the user likes or does not like this comment. 1095 const commentId = getCommentId(target); 1096 target.classList.add('loading'); 1097 1098 let commentEl = document.querySelector(`p#comment-like-${commentId}`); 1099 // Determine whether to like or unlike based on whether the comment is 1100 // currently liked. 1101 const action = 1102 commentEl && commentEl.dataset.liked === 'comment-liked' 1103 ? 'unlike_comment' 1104 : 'like_comment'; 1105 handleLinkAction(target, action, commentId, data => { 1106 // Invalidate the like cache for this comment. 1107 delete commentLikeCache[commentId]; 1108 1109 const countEl = document.querySelector(`#comment-like-count-${data.context}`); 1110 if (countEl) { 1111 countEl.innerHTML = data.display; 1112 } 1113 1114 commentEl = document.querySelector(`p#comment-like-${data.context}`); 1115 if (action === 'like_comment') { 1116 commentEl.classList.remove('comment-not-liked'); 1117 commentEl.classList.add('comment-liked'); 1118 commentEl.dataset.liked = 'comment-liked'; 1119 } else { 1120 commentEl.classList.remove('comment-liked'); 1121 commentEl.classList.add('comment-not-liked'); 1122 commentEl.dataset.liked = 'comment-not-liked'; 1123 } 1124 1125 // Prefetch new data for this comment (if there are likers left). 1126 const parent = target.closest('.comment-likes'); 1127 const link = parent && parent.querySelector('a.view-likers'); 1128 if (link) { 1129 fetchLikeData(link, commentId); 1130 } 1131 1132 target.classList.remove('loading'); 1133 }); 1134 e.preventDefault(); 1135 e.stopPropagation(); 1136 }); 1137 1138 document.body.addEventListener( 1139 'mouseenter', 1140 function (e) { 1141 if (!e.target.matches('p.comment-likes a.view-likers')) { 1142 return; 1143 } 1144 // Show the user a list of who has liked this comment. 1145 1146 const link = e.target; 1147 if (Number(link.dataset.likeCount || 0) === 0) { 1148 // No one has liked this comment. 1149 return; 1150 } 1151 1152 // Don't hide the overlay. 1153 overlay.cancelHide(); 1154 1155 // Get the comment ID. 1156 const container = link.parentElement && link.parentElement.parentElement; 1157 const star = container && container.querySelector('a.comment-like-link'); 1158 const commentId = star && getCommentId(star); 1159 relevantComment = commentId; 1160 1161 // Check if the list of likes for this comment is already in 1162 // the cache. 1163 if (commentId in commentLikeCache) { 1164 const entry = commentLikeCache[commentId];
1165 // Only display the likes if the ajax request is 1166 // actually done. 1167 if (entry !== null) { 1168 overlay.showLikes(link, entry); 1169 } else { 1170 // Make sure the overlay is visible (in case 1171 // the user moved the mouse away while loading 1172 // but then came back before it finished 1173 // loading). 1174 overlay.showLoadingMessage(link); 1175 } 1176 return; 1177 } 1178 1179 // Position the "Loading..." overlay. 1180 overlay.showLoadingMessage(link); 1181 1182 // Fetch the data. 1183 fetchLikeData(link, commentId); 1184 }, 1185 true 1186 ); 1187 1188 document.body.addEventListener( 1189 'mouseleave', 1190 e => { 1191 if (!e.target.matches('p.comment-likes a.view-likers')) { 1192 return; 1193 } 1194 // User has moved cursor away - hide the overlay. 1195 overlay.requestHide(); 1196 }, 1197 true 1198 ); 1199 1200 document.body.addEventListener( 1201 'mouseenter', 1202 e => { 1203 if (!e.target.matches('.comment') || !e.target.querySelector('a.comment-like-link')) { 1204 return; 1205 } 1206 // User is moving over a comment - precache the comment like data.
1207 if (precacheTimeout !== null) { 1208 clearTimeout(precacheTimeout); 1209 precacheTimeout = null; 1210 } 1211 1212 const star = e.target.querySelector('a.comment-like-link'); 1213 const parent = star.closest('.comment-likes'); 1214 const link = parent && parent.querySelector('a.view-likers'); 1215 if (!link || Number(link.dataset.likeCount || 0) === 0) { 1216 // No likes. 1217 return; 1218 } 1219 const commentId = getCommentId(star); 1220 if (commentId in commentLikeCache) { 1221 // Already in cache. 1222 return; 1223 } 1224 1225 precacheTimeout = setTimeout(() => { 1226 precacheTimeout = null; 1227 if (commentId in commentLikeCache) { 1228 // Was cached in the interim. 1229 return; 1230 } 1231 fetchLikeData(link, commentId); 1232 }, 1000); 1233 }, 1234 true 1235 ); 1236 } 1237 1238 if (document.readyState !== 'loading') { 1239 init(); 1240 } else { 1241 document.addEventListener('DOMContentLoaded', init); 1242 } 1243})(); 1244;
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.