/* global wpcom_reblog */
var jetpackLikesWidgetBatch = [];
var jetpackLikesMasterReady = false;
// Due to performance problems on pages with a large number of widget iframes that need to be loaded,
// we are limiting the processing at any instant to unloaded widgets that are currently in viewport,
// plus this constant that will allow processing of widgets above and bellow the current fold.
// This aim of it is to improve the UX and hide the transition from unloaded to loaded state from users.
var jetpackLikesLookAhead = 2000; // pixels
// Keeps track of loaded comment likes widget so we can unload them when they are scrolled out of view.
var jetpackCommentLikesLoadedWidgets = [];
var jetpackLikesDocReadyPromise = new Promise( resolve => {
if ( document.readyState !== 'loading' ) {
resolve();
} else {
window.addEventListener( 'DOMContentLoaded', () => resolve() );
}
} );
function JetpackLikesPostMessage( message, target ) {
if ( typeof message === 'string' ) {
try {
message = JSON.parse( message );
} catch ( e ) {
return;
}
}
if ( target && typeof target.postMessage === 'function' ) {
try {
target.postMessage(
JSON.stringify( {
type: 'likesMessage',
data: message,
} ),
'*'
);
} catch ( e ) {
return;
}
}
}
function JetpackLikesBatchHandler() {
const requests = [];
document.querySelectorAll( 'div.jetpack-likes-widget-unloaded' ).forEach( widget => {
if ( jetpackLikesWidgetBatch.indexOf( widget.id ) > -1 ) {
return;
}
if ( ! jetpackIsScrolledIntoView( widget ) ) {
return;
}
jetpackLikesWidgetBatch.push( widget.id );
var regex = /like-(post|comment)-wrapper-(\d+)-(\d+)-(\w+)/,
match = regex.exec( widget.id ),
info;
if ( ! match || match.length !== 5 ) {
return;
}
info = {
blog_id: match[ 2 ],
width: widget.width,
};
if ( 'post' === match[ 1 ] ) {
info.post_id = match[ 3 ];
} else if ( 'comment' === match[ 1 ] ) {
info.comment_id = match[ 3 ];
}
info.obj_id = match[ 4 ];
requests.push( info );
} );
if ( requests.length > 0 ) {
JetpackLikesPostMessage(
{ event: 'initialBatch', requests: requests },
window.frames[ 'likes-master' ]
);
}
}
function JetpackLikesMessageListener( event ) {
let message = event && event.data;
if ( typeof message === 'string' ) {
try {
message = JSON.parse( message );
} catch ( err ) {
return;
}
}
const type = message && message.type;
const data = message && message.data;
if ( type !== 'likesMessage' || typeof data.event === 'undefined' ) {
return;
}
// We only allow messages from one origin
const allowedOrigin = 'https://widgets.wp.com';
if ( allowedOrigin !== event.origin ) {
return;
}
switch ( data.event ) {
case 'masterReady':
jetpackLikesDocReadyPromise.then( () => {
jetpackLikesMasterReady = true;
const stylesData = {
event: 'injectStyles',
};
const sdTextColor = document.querySelector( '.sd-text-color' );
const sdLinkColor = document.querySelector( '.sd-link-color' );
const sdTextColorStyles = ( sdTextColor && getComputedStyle( sdTextColor ) ) || {};
const sdLinkColorStyles = ( sdLinkColor && getComputedStyle( sdLinkColor ) ) || {};
// enable reblogs if they are enabled for the page
if ( document.body.classList.contains( 'jetpack-reblog-enabled' ) ) {
JetpackLikesPostMessage( { event: 'reblogsEnabled' }, window.frames[ 'likes-master' ] );
}
stylesData.textStyles = {
color: sdTextColorStyles[ 'color' ],
fontFamily: sdTextColorStyles[ 'font-family' ],
fontSize: sdTextColorStyles[ 'font-size' ],
direction: sdTextColorStyles[ 'direction' ],
fontWeight: sdTextColorStyles[ 'font-weight' ],
fontStyle: sdTextColorStyles[ 'font-style' ],
textDecoration: sdTextColorStyles[ 'text-decoration' ],
};
stylesData.linkStyles = {
color: sdLinkColorStyles[ 'color' ],
fontFamily: sdLinkColorStyles[ 'font-family' ],
fontSize: sdLinkColorStyles[ 'font-size' ],
textDecoration: sdLinkColorStyles[ 'text-decoration' ],
fontWeight: sdLinkColorStyles[ 'font-weight' ],
fontStyle: sdLinkColorStyles[ 'font-style' ],
};
JetpackLikesPostMessage( stylesData, window.frames[ 'likes-master' ] );
JetpackLikesBatchHandler();
} );
break;
// We're keeping this for planned future follow ups.
// @see: https://github.com/Automattic/jetpack/pull/42361#discussion_r1995338815
case 'showLikeWidget':
break;
// We're keeping this for planned future follow ups.
// @see: https://github.com/Automattic/jetpack/pull/42361#discussion_r1995338815
case 'showCommentLikeWidget':
break;
case 'killCommentLikes':
// If kill switch for comment likes is enabled remove all widgets wrappers and `Loading...` placeholders.
document
.querySelectorAll( '.jetpack-comment-likes-widget-wrapper' )
.forEach( wrapper => wrapper.remove() );
break;
case 'clickReblogFlair':
if ( wpcom_reblog && typeof wpcom_reblog.toggle_reblog_box_flair === 'function' ) {
wpcom_reblog.toggle_reblog_box_flair( data.obj_id, data.post_id );
}
break;
case 'hideOtherGravatars': {
hideLikersPopover();
break;
}
case 'showOtherGravatars': {
const container = document.querySelector( '#likes-other-gravatars' );
if ( ! container ) {
break;
}
const list = container.querySelector( 'ul' );
container.style.display = 'none';
list.innerHTML = '';
container
.querySelectorAll( '.likes-text span' )
.forEach( item => ( item.textContent = data.totalLikesLabel ) );
( data.likers || [] ).forEach( async ( liker, index ) => {
if ( liker.profile_URL.substr( 0, 4 ) !== 'http' ) {
// We only display gravatars with http or https schema
return;
}
const element = document.createElement( 'li' );
list.append( element );
element.innerHTML = `
`;
// Add some extra attributes through native methods, to ensure strings are sanitized.
element.classList.add( liker.css_class );
element.querySelector( 'img' ).alt = data.avatarAltTitle.replace( '%s', liker.name );
element.querySelector( 'span' ).innerText = liker.name;
if ( index === data.likers.length - 1 ) {
element.addEventListener( 'keydown', ( e ) => {
if ( e.key === 'Tab' && ! e.shiftKey ) {
e.preventDefault();
hideLikersPopover();
JetpackLikesPostMessage(
{ event: 'focusLikesCount', parent: data.parent },
window.frames[ 'likes-master' ]
);
}
} );
}
} );
const positionPopup = function() {
const containerStyle = getComputedStyle(container);
const isRtl = containerStyle.direction === 'rtl';
const el = document.querySelector( `*[name='${ data.parent }']` );
const rect = el.getBoundingClientRect();
const win = el.ownerDocument.defaultView;
const offset = {
top: rect.top + win.pageYOffset,
left: rect.left + win.pageXOffset,
};
// don't display yet or we get skewed window.innerWidth later
container.style.display = 'none';
let containerLeft = 0;
container.style.top = offset.top + data.position.top - 1 + 'px';
if ( isRtl ) {
const visibleAvatarsCount = data && data.likers ? Math.min( data.likers.length, 5 ) : 0;
// 24px is the width of the avatar + 4px is the padding between avatars
containerLeft = offset.left + data.position.left + 24 * visibleAvatarsCount + 4;
container.style.transform = 'translateX(-100%)';
} else {
containerLeft = offset.left + data.position.left;
}
container.style.left = containerLeft + 'px';
// Container width - padding
const initContainerWidth = data.width - 20;
const rowLength = Math.floor( initContainerWidth / 37 );
// # of rows + (avatar + avatar padding) + text above + container padding
let height = Math.ceil( data.likers.length / rowLength ) * 37 + 17 + 22;
if ( height > 204 ) {
height = 204;
}
// If the popup is overflows viewport width, we should show it on the next line
// Push it offscreen to calculated rendered width
const windowWidth = win.innerWidth;
container.style.left = '-9999px';
container.style.display = 'block';
// If the popup exceeds the viewport width,
// flip the position of the popup.
const containerWidth = container.offsetWidth;
const containerRight = containerLeft + containerWidth;
if ( containerRight > windowWidth && ! isRtl) {
containerLeft = rect.left + rect.width - containerWidth;
} else if ( containerLeft - containerWidth < 0 && isRtl ) {
container.style.transform = 'none';
containerLeft = rect.left;
}
// Set the container left
container.style.left = containerLeft + 'px';
container.setAttribute( 'aria-hidden', 'false' );
}
positionPopup();
container.focus();
const debounce = function( func, wait ) {
var timeout;
return function() {
var context = this;
var args = arguments;
clearTimeout( timeout );
timeout = setTimeout( function() {
func.apply( context, args );
}, wait );
};
};
const debouncedPositionPopup = debounce( positionPopup, 100 );
// Keep a reference of this function in the element itself
// so that we can destroy it later
container.__resizeHandler = debouncedPositionPopup;
// When window is resized, resize the popup.
window.addEventListener( "resize", debouncedPositionPopup );
}
}
}
window.addEventListener( 'message', JetpackLikesMessageListener );
function hideLikersPopover() {
const container = document.querySelector( '#likes-other-gravatars' );
if ( container ) {
container.style.display = 'none';
container.setAttribute( 'aria-hidden', 'true' );
// Remove the resize event listener and cleanup.
const resizeHandler = container.__resizeHandler;
if ( resizeHandler ) {
window.removeEventListener( "resize", resizeHandler );
delete container.__resizeHandler;
}
}
}
document.addEventListener( 'click', hideLikersPopover );
function JetpackLikesWidgetQueueHandler() {
var wrapperID;
if ( ! jetpackLikesMasterReady ) {
setTimeout( JetpackLikesWidgetQueueHandler, 500 );
return;
}
// Restore widgets to initial unloaded state when they are scrolled out of view.
jetpackUnloadScrolledOutWidgets();
var unloadedWidgetsInView = jetpackGetUnloadedWidgetsInView();
if ( unloadedWidgetsInView.length > 0 ) {
// Grab any unloaded widgets for a batch request
JetpackLikesBatchHandler();
}
for ( var i = 0, length = unloadedWidgetsInView.length; i <= length - 1; i++ ) {
wrapperID = unloadedWidgetsInView[ i ].id;
if ( ! wrapperID ) {
continue;
}
jetpackLoadLikeWidgetIframe( wrapperID );
}
}
function jetpackLoadLikeWidgetIframe( wrapperID ) {
if ( typeof wrapperID === 'undefined' ) {
return;
}
const wrapper = document.querySelector( '#' + wrapperID );
wrapper.querySelectorAll( 'iframe' ).forEach( iFrame => iFrame.remove() );
const placeholder = wrapper.querySelector( '.likes-widget-placeholder' );
// Post like iframe
if ( placeholder && placeholder.classList.contains( 'post-likes-widget-placeholder' ) ) {
const postLikesFrame = document.createElement( 'iframe' );
postLikesFrame.classList.add( 'post-likes-widget', 'jetpack-likes-widget' );
postLikesFrame.name = wrapper.dataset.name;
postLikesFrame.src = wrapper.dataset.src;
postLikesFrame.height = '55px';
postLikesFrame.width = '100%';
postLikesFrame.frameBorder = '0';
postLikesFrame.scrolling = 'no';
postLikesFrame.title = wrapper.dataset.title;
placeholder.after( postLikesFrame );
}
// Comment like iframe
if ( placeholder.classList.contains( 'comment-likes-widget-placeholder' ) ) {
const commentLikesFrame = document.createElement( 'iframe' );
commentLikesFrame.class = 'comment-likes-widget-frame jetpack-likes-widget-frame';
commentLikesFrame.name = wrapper.dataset.name;
commentLikesFrame.src = wrapper.dataset.src;
commentLikesFrame.height = '18px';
commentLikesFrame.width = '100%';
commentLikesFrame.frameBorder = '0';
commentLikesFrame.scrolling = 'no';
wrapper.querySelector( '.comment-like-feedback' ).after( commentLikesFrame );
jetpackCommentLikesLoadedWidgets.push( commentLikesFrame );
}
wrapper.classList.remove( 'jetpack-likes-widget-unloaded' );
wrapper.classList.add( 'jetpack-likes-widget-loading' );
wrapper.querySelector( 'iframe' ).addEventListener( 'load', e => {
JetpackLikesPostMessage(
{ event: 'loadLikeWidget', name: e.target.name, width: e.target.width },
window.frames[ 'likes-master' ]
);
wrapper.classList.remove( 'jetpack-likes-widget-loading' );
wrapper.classList.add( 'jetpack-likes-widget-loaded' );
} );
}
function jetpackGetUnloadedWidgetsInView() {
const unloadedWidgets = document.querySelectorAll( 'div.jetpack-likes-widget-unloaded' );
return [ ...unloadedWidgets ].filter( item => jetpackIsScrolledIntoView( item ) );
}
function jetpackIsScrolledIntoView( element ) {
const top = element.getBoundingClientRect().top;
const bottom = element.getBoundingClientRect().bottom;
// Allow some slack above and bellow the fold with jetpackLikesLookAhead,
// with the aim of hiding the transition from unloaded to loaded widget from users.
return top + jetpackLikesLookAhead >= 0 && bottom <= window.innerHeight + jetpackLikesLookAhead;
}
function jetpackUnloadScrolledOutWidgets() {
for ( let i = jetpackCommentLikesLoadedWidgets.length - 1; i >= 0; i-- ) {
const currentWidgetIframe = jetpackCommentLikesLoadedWidgets[ i ];
if ( ! jetpackIsScrolledIntoView( currentWidgetIframe ) ) {
const widgetWrapper =
currentWidgetIframe &&
currentWidgetIframe.parentElement &&
currentWidgetIframe.parentElement.parentElement;
// Restore parent class to 'unloaded' so this widget can be picked up by queue manager again if needed.
widgetWrapper.classList.remove( 'jetpack-likes-widget-loaded' );
widgetWrapper.classList.remove( 'jetpack-likes-widget-loading' );
widgetWrapper.classList.add( 'jetpack-likes-widget-unloaded' );
// Bring back the loading placeholder into view.
widgetWrapper
.querySelectorAll( '.comment-likes-widget-placeholder' )
.forEach( item => ( item.style.display = 'block' ) );
// Remove it from the list of loaded widgets.
jetpackCommentLikesLoadedWidgets.splice( i, 1 );
// Remove comment like widget iFrame.
currentWidgetIframe.remove();
}
}
}
var jetpackWidgetsDelayedExec = function ( after, fn ) {
var timer;
return function () {
clearTimeout( timer );
timer = setTimeout( fn, after );
};
};
var jetpackOnScrollStopped = jetpackWidgetsDelayedExec( 250, JetpackLikesWidgetQueueHandler );
// Load initial batch of widgets, prior to any scrolling events.
JetpackLikesWidgetQueueHandler();
// Add event listener to execute queue handler after scroll.
window.addEventListener( 'scroll', jetpackOnScrollStopped, true );
;
/**
* Comment Likes - JavaScript
*
* This handles liking and unliking comments, as well as viewing who has
* liked a particular comment.
*
* @dependency Swipe (dynamically loaded when needed)
*
* @package Comment_Likes
* @subpackage JavaScript
*/
(function () {
function init() {
let extWin;
let extWinCheck;
let commentLikeEvent;
// Only run once.
if (window.comment_likes_loaded) {
return;
}
window.comment_likes_loaded = true;
// Client-side cache of who liked a particular comment to avoid
// having to hit the server multiple times for the same data.
const commentLikeCache = {};
let swipeLibPromise;
// Load the Swipe library, if it's not already loaded.
function swipeLibLoader() {
if (!swipeLibPromise) {
swipeLibPromise = new Promise((resolve, reject) => {
if (window.Swipe) {
resolve(window.Swipe);
} else {
const swipeScript = document.createElement('script');
swipeScript.src = comment_like_text.swipeUrl;
swipeScript.async = true;
document.body.appendChild(swipeScript);
swipeScript.addEventListener('load', () => resolve(window.Swipe));
swipeScript.addEventListener('error', error => reject(error));
}
});
}
return swipeLibPromise;
}
/**
* Parse the comment ID from a comment like link.
*/
function getCommentId(link) {
const commentId =
link && link.getAttribute('href') && link.getAttribute('href').split('like_comment=');
return commentId[1].split('&_wpnonce=')[0];
}
/**
* Handle an ajax action on the comment like link.
*/
function handleLinkAction(link, action, commentId, callback) {
const nonce =
link && link.getAttribute('href') && link.getAttribute('href').split('_wpnonce=')[1];
fetch('/wp-admin/admin-ajax.php', {
method: 'POST',
body: new URLSearchParams({
action: action,
_wpnonce: nonce,
like_comment: commentId,
blog_id: Number(link.dataset.blog),
}),
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'X-Requested-With': 'XMLHttpRequest',
Accept: 'application/json',
'cache-control': 'no-cache',
pragma: 'no-cache',
},
})
.then(response => response.json())
.then(callback);
}
function startPolling() {
// Append cookie polling login iframe to this window to wait for user to finish logging in (or cancel)
const loginIframe = document.createElement('iframe');
loginIframe.id = 'wp-login-polling-iframe';
loginIframe.src = 'https://wordpress.com/public.api/connect/?iframe=true';
document.body.appendChild(loginIframe);
loginIframe.style.display = 'none';
}
function stopPolling() {
const iframe = document.querySelector('#wp-login-polling-iframe');
if (iframe) {
iframe.remove();
}
}
function hide(el) {
if (el && el.style) {
el.style.display = 'none';
}
}
function show(el) {
if (el && el.style) {
el.style.removeProperty('display');
}
}
// Overlay used for displaying comment like info.
class Overlay {
constructor() {
// Overlay element.
this.el = document.createElement('div');
this.el.classList.add('comment-likes-overlay');
document.body.appendChild(this.el);
hide(this.el);
this.el.addEventListener('mouseenter', () => {
// Don't hide the overlay if the user is mousing over it.
overlay.cancelHide();
});
this.el.addEventListener('mouseleave', () => overlay.requestHide());
// Inner contents of overlay.
this.innerEl = null;
// Instance of the Swipe library.
this.swipe = null;
// Timeout used for hiding the overlay.
this.hideTimeout = null;
}
// Initialise the overlay for use, removing any old content.
clear() {
// Unload any previous instance of Swipe (to avoid leaking a global
// event handler). This is done before clearing the contents of the
// overlay because Swipe expects the slides to still be present.
if (this.swipe) {
this.swipe.kill();
this.swipe = null;
}
this.el.innerHTML = '';
this.innerEl = document.createElement('div');
this.innerEl.classList.add('inner');
this.el.appendChild(this.innerEl);
}
/**
* Construct a list (
) of user (gravatar, name) details.
*
* @param data liker data returned from the server
* @param klass CSS class to apply to the
element
* @param start index of user to start at
* @param length number of users to include in the list
*
* @return A container element with the list
*/
getUserBits(data, klass, start, length) {
start = start || 0;
let last = start + (length || data.length);
last = last > data.length ? data.length : last;
const container = document.createElement('div');
container.classList.add('liker-list');
let html = `
`;
for (let i = start; i < last; ++i) {
const user = data[i];
html += `
';
container.innerHTML = html;
return container;
}
/**
* Render the display of who has liked this comment. The type of
* display depends on how many people have liked the comment.
* If more than 10 people have liked the comment, this function
* renders navigation controls and sets up the Swipe library for
* changing between pages.
*
* @param link the element over which the user is hovering
* @param data the results retrieved from the server
*/
showLikes(link, data) {
this.clear();
link.dataset.likeCount = data.length;
if (data.length === 0) {
// No likers after all.
hide(this.el);
return;
}
this.innerEl.style.padding = '12px';
if (data.length < 6) {
// Only one column needed.
this.innerEl.style.maxWidth = '200px';
this.innerEl.innerHTML = '';
this.innerEl.appendChild(this.getUserBits(data, 'single'));
this.setPosition(link);
} else if (data.length < 11) {
// Two columns, but only one page.
this.innerEl.innerHTML = '';
this.innerEl.appendChild(this.getUserBits(data, 'double'));
this.setPosition(link);
} else {
// Multiple pages.
this.renderLikesWithPagination(data, link);
}
}
/**
* Render multiple pages of likes with pagination controls.
* This function is intended to be called by `showLikes` above.
*
* @param data the results retrieved from the server
*/
renderLikesWithPagination(data, link) {
swipeLibLoader().then(() => {
const page_count = Math.ceil(data.length / 10);
// Swipe requires two nested containers.
const swipe = document.createElement('div');
swipe.classList.add('swipe');
this.innerEl.appendChild(swipe);
const wrap = document.createElement('div');
wrap.classList.add('swipe-wrap');
swipe.appendChild(wrap);
for (let i = 0; i < page_count; ++i) {
wrap.appendChild(this.getUserBits(data, 'double', i * 10, 10));
}
/**
* Navigation controls.
* This is based on the Newdash controls found in
* reader/recommendations-templates.php
*/
const nav = document.createElement('nav');
nav.classList.add('slider-nav');
let navContents = `
`;
for (let i = 0; i < page_count; ++i) {
navContents += `•`;
}
navContents += `
`;
this.innerEl.appendChild(nav);
nav.innerHTML = navContents;
/** Set up Swipe. **/
// Swipe cannot be set up successfully unless its container
// is visible, so we show it now.
show(this.el);
this.setPosition(link);
this.swipe = new Swipe(swipe, {
callback: function (pos) {
// Update the pagination indicators.
//
// If there are exactly two pages, Swipe has a weird
// special case where it duplicates both pages and
// can return index 2 and 3 even though those aren't
// real pages (see swipe.js, line 47). To deal with
// this, we use the expression `pos % page_count`.
pos = pos % page_count;
nav.querySelectorAll('em').forEach(em => {
const page = Number(em.dataset.page);
em.setAttribute('class', pos === page ? 'on' : '');
});
},
});
nav.querySelectorAll('em').forEach(em => {
em.addEventListener('click', e => {
// Go to the page corresponding to the indicator clicked.
this.swipe.slide(Number(em.dataset.page));
e.preventDefault();
});
});
// Previous and next buttons.
nav.querySelector('.prev').addEventListener('click', e => {
this.swipe.prev();
e.preventDefault();
});
nav.querySelector('.next').addEventListener('click', e => {
this.swipe.next();
e.preventDefault();
});
});
}
/**
* Open the overlay and show a loading message.
*/
showLoadingMessage(link) {
this.clear();
this.innerEl.textContent = comment_like_text.loading;
this.setPosition(link);
}
/**
* Position the overlay near the current comment.
*
* @param link element near which to position the overlay
*/
setPosition(link) {
// Prepare a down arrow icon for the bottom of the overlay.
const icon = document.createElement('span');
this.el.appendChild(icon);
icon.classList.add('icon', 'noticon', 'noticon-downarrow');
icon.style.textShadow = '0px 1px 1px rgb(223, 223, 223)';
const rect = link.getBoundingClientRect();
const win = document.defaultView;
const offset = {
top: rect.top + win.scrollY,
left: rect.left + win.scrollX,
};
// Take measurements with the element fully visible.
show(this.el);
let left = offset.left - (this.el.offsetWidth - link.offsetWidth) / 2;
left = left < 5 ? 5 : left;
let top = offset.top - this.el.offsetHeight + 5;
hide(this.el);
const adminBar = document.querySelector('#wpadminbar');
// Check if the overlay would appear off the screen.
if (top < win.scrollY + ((adminBar && adminBar.offsetHeight) || 0)) {
// We'll display the overlay beneath the link instead.
top = offset.top + link.offsetHeight;
// Instead of using the down arrow icon, use an up arrow.
icon.remove();
this.el.prepend(icon);
icon.classList.remove('noticon-downarrow');
icon.classList.add('noticon-uparrow');
icon.style.textShadow = '0px -1px 1px rgb(223, 223, 223)';
icon.style.verticalAlign = 'bottom';
}
this.el.style.left = `${left}px`;
this.el.style.top = `${top}px`;
show(this.el);
// The height of the arrow icon differs slightly between browsers,
// so we compute the margin here to make sure it isn't disjointed
// from the overlay.
icon.style.marginTop = `${icon.scrollHeight - 26}px`;
icon.style.marginBottom = `${20 - icon.scrollHeight}px`;
// Position the arrow to be horizontally centred on the link.
icon.style.paddingLeft = `${
offset.left - left + (link.offsetWidth - icon.scrollWidth) / 2
}px`;
}
/**
* Return whether the overlay is visible.
*/
isVisible() {
return this.el.style.getPropertyValue('display') !== 'none';
}
/**
* Request that the overlay be hidden after a short delay.
*/
requestHide() {
if (this.hideTimeout !== null) {
return;
}
this.hideTimeout = setTimeout(() => {
hide(this.el);
this.clear();
}, 300);
}
/**
* Cancel a request to hide the overlay.
*/
cancelHide() {
if (this.hideTimeout !== null) {
clearTimeout(this.hideTimeout);
this.hideTimeout = null;
}
}
}
// Overlay used for displaying comment like info.
const overlay = new Overlay();
// The most recent comment for which the user has requested to see
// who liked it.
var relevantComment;
// Precache after this timeout.
var precacheTimeout = null;
/**
* Fetch the like data for a particular comment.
*/
function fetchLikeData(link, commentId) {
commentLikeCache[commentId] = null;
const container = link && link.parentElement && link.parentElement.parentElement;
const star = container.querySelector('a.comment-like-link');
star &&
handleLinkAction(star, 'view_comment_likes', commentId, data => {
// Populate the cache.
commentLikeCache[commentId] = data;
// Only show the overlay if the user is interested.
if (overlay.isVisible() && relevantComment === commentId) {
overlay.showLikes(link, data);
}
});
}
function readCookie(c) {
const nameEQ = c + '=';
const cookieStrings = document.cookie.split(';');
for (let i = 0; i < cookieStrings.length; i++) {
let cookieString = cookieStrings[i];
while (cookieString.charAt(0) === ' ') {
cookieString = cookieString.substring(1, cookieString.length);
}
if (cookieString.indexOf(nameEQ) === 0) {
const chunk = cookieString.substring(nameEQ.length, cookieString.length);
const pairs = chunk.split('&');
const cookieData = {};
for (let num = pairs.length - 1; num >= 0; num--) {
const pair = pairs[num].split('=');
cookieData[pair[0]] = decodeURIComponent(pair[1]);
}
return cookieData;
}
}
return null;
}
function getServiceData() {
const data = readCookie('wpc_wpc');
if (data === null || typeof data.access_token === 'undefined' || !data.access_token) {
return false;
}
return data;
}
function readMessage(msg) {
const event = msg.data;
if (typeof event.event === 'undefined') {
return;
}
if (event.event === 'login' && event.success) {
extWinCheck = setInterval(function () {
if (!extWin || extWin.closed) {
clearInterval(extWinCheck);
if (getServiceData()) {
// Load page in an iframe to get the current comment nonce
const nonceIframe = document.createElement('iframe');
nonceIframe.id = 'wp-login-comment-nonce-iframe';
nonceIframe.style.display = 'none';
nonceIframe.src = commentLikeEvent + '';
document.body.appendChild(nonceIframe);
const commentLikeId = (commentLikeEvent + '')
.split('like_comment=')[1]
.split('&_wpnonce=')[0];
let c;
// Set a 5 second timeout to redirect to the comment page without doing the Like as a fallback
const commentLikeTimeout = setTimeout(() => {
window.location = commentLikeEvent;
}, 5000);
// Check for a new nonced redirect and use that if available before timing out
const commentLikeCheck = setInterval(() => {
const iframe = document.querySelector('#wp-login-comment-nonce-iframe');
if (iframe) {
c = iframe.querySelector(`#comment-like-${commentLikeId} .comment-like-link`);
}
if (c && typeof c.href !== 'undefined') {
clearTimeout(commentLikeTimeout);
clearInterval(commentLikeCheck);
window.location = c.href;
}
}, 100);
}
}
}, 100);
if (extWin) {
if (!extWin.closed) {
extWin.close();
}
extWin = false;
}
stopPolling();
}
}
if (typeof window.postMessage !== 'undefined') {
window.addEventListener('message', e => {
let message = e && e.data;
if (typeof message === 'string') {
try {
message = JSON.parse(message);
} catch (err) {
return;
}
}
const type = message && message.type;
if (type === 'loginMessage') {
readMessage(message);
}
});
}
document.body.addEventListener('click', e => {
let target = e.target;
// Don't do anything when clicking on the "X people" link.
if (target.matches('p.comment-likes a.view-likers')) {
e.preventDefault();
return;
}
// Retrieve the surrounding paragraph to the star, if it hasn't been liked.
const notLikedPar = target.closest('p.comment-not-liked');
// Return if not clicking on star or surrounding paragraph.
if (!target.matches('a.comment-like-link') && !notLikedPar) {
return;
}
// When a comment hasn't been liked, make the text clickable, too.
if (notLikedPar) {
target = notLikedPar.querySelector('a.comment-like-link');
if (!target) {
return;
}
}
if (target.classList.contains('needs-login')) {
e.preventDefault();
commentLikeEvent = target;
if (extWin) {
if (!extWin.closed) {
extWin.close();
}
extWin = false;
}
stopPolling();
const url = 'https://wordpress.com/public.api/connect/?action=request&service=wordpress';
extWin = window.open(
url,
'likeconn',
'status=0,toolbar=0,location=1,menubar=0,directories=0,resizable=1,scrollbars=1,height=560,width=500'
);
startPolling();
return false;
}
// Record that the user likes or does not like this comment.
const commentId = getCommentId(target);
target.classList.add('loading');
let commentEl = document.querySelector(`p#comment-like-${commentId}`);
// Determine whether to like or unlike based on whether the comment is
// currently liked.
const action =
commentEl && commentEl.dataset.liked === 'comment-liked'
? 'unlike_comment'
: 'like_comment';
handleLinkAction(target, action, commentId, data => {
// Invalidate the like cache for this comment.
delete commentLikeCache[commentId];
const countEl = document.querySelector(`#comment-like-count-${data.context}`);
if (countEl) {
countEl.innerHTML = data.display;
}
commentEl = document.querySelector(`p#comment-like-${data.context}`);
if (action === 'like_comment') {
commentEl.classList.remove('comment-not-liked');
commentEl.classList.add('comment-liked');
commentEl.dataset.liked = 'comment-liked';
} else {
commentEl.classList.remove('comment-liked');
commentEl.classList.add('comment-not-liked');
commentEl.dataset.liked = 'comment-not-liked';
}
// Prefetch new data for this comment (if there are likers left).
const parent = target.closest('.comment-likes');
const link = parent && parent.querySelector('a.view-likers');
if (link) {
fetchLikeData(link, commentId);
}
target.classList.remove('loading');
});
e.preventDefault();
e.stopPropagation();
});
document.body.addEventListener(
'mouseenter',
function (e) {
if (!e.target.matches('p.comment-likes a.view-likers')) {
return;
}
// Show the user a list of who has liked this comment.
const link = e.target;
if (Number(link.dataset.likeCount || 0) === 0) {
// No one has liked this comment.
return;
}
// Don't hide the overlay.
overlay.cancelHide();
// Get the comment ID.
const container = link.parentElement && link.parentElement.parentElement;
const star = container && container.querySelector('a.comment-like-link');
const commentId = star && getCommentId(star);
relevantComment = commentId;
// Check if the list of likes for this comment is already in
// the cache.
if (commentId in commentLikeCache) {
const entry = commentLikeCache[commentId];
// Only display the likes if the ajax request is
// actually done.
if (entry !== null) {
overlay.showLikes(link, entry);
} else {
// Make sure the overlay is visible (in case
// the user moved the mouse away while loading
// but then came back before it finished
// loading).
overlay.showLoadingMessage(link);
}
return;
}
// Position the "Loading..." overlay.
overlay.showLoadingMessage(link);
// Fetch the data.
fetchLikeData(link, commentId);
},
true
);
document.body.addEventListener(
'mouseleave',
e => {
if (!e.target.matches('p.comment-likes a.view-likers')) {
return;
}
// User has moved cursor away - hide the overlay.
overlay.requestHide();
},
true
);
document.body.addEventListener(
'mouseenter',
e => {
if (!e.target.matches('.comment') || !e.target.querySelector('a.comment-like-link')) {
return;
}
// User is moving over a comment - precache the comment like data.
if (precacheTimeout !== null) {
clearTimeout(precacheTimeout);
precacheTimeout = null;
}
const star = e.target.querySelector('a.comment-like-link');
const parent = star.closest('.comment-likes');
const link = parent && parent.querySelector('a.view-likers');
if (!link || Number(link.dataset.likeCount || 0) === 0) {
// No likes.
return;
}
const commentId = getCommentId(star);
if (commentId in commentLikeCache) {
// Already in cache.
return;
}
precacheTimeout = setTimeout(() => {
precacheTimeout = null;
if (commentId in commentLikeCache) {
// Was cached in the interim.
return;
}
fetchLikeData(link, commentId);
}, 1000);
},
true
);
}
if (document.readyState !== 'loading') {
init();
} else {
document.addEventListener('DOMContentLoaded', init);
}
})();
;
/**
* File skip-link-focus-fix.js.
*
* Helps with accessibility for keyboard only users.
*
* Learn more: https://git.io/vWdr2
*/
(function() {
var isIe = /(trident|msie)/i.test( navigator.userAgent );
if ( isIe && document.getElementById && window.addEventListener ) {
window.addEventListener( 'hashchange', function() {
var id = location.hash.substring( 1 ),
element;
if ( ! ( /^[A-z0-9_-]+$/.test( id ) ) ) {
return;
}
element = document.getElementById( id );
if ( element ) {
if ( ! ( /^(?:a|select|input|button|textarea)$/i.test( element.tagName ) ) ) {
element.tabIndex = -1;
}
element.focus();
}
}, false );
}
})();
;
/**
* File navigation.js.
*
* Handles toggling the navigation menu for small screens and enables TAB key
* navigation support for dropdown menus.
*/
( function( $ ) {
var body, menuToggle, siteMenu, siteNavigation;
function initMainNavigation( container ) {
// Add dropdown toggle that displays child menu items.
var dropdownToggle = $( '', {
'class': 'dropdown-toggle',
'aria-expanded': false
} ).append( $( '', {
'class': 'screen-reader-text',
text: screenReaderText.expand
} ) );
container.find( '.menu-item-has-children > a' ).after( dropdownToggle );
// Toggle buttons and submenu items with active children menu items.
container.find( '.current-menu-ancestor > button' ).addClass( 'toggled-on' );
container.find( '.current-menu-ancestor > .sub-menu' ).addClass( 'toggled-on' );
// Add menu items with submenus to aria-haspopup="true".
container.find( '.menu-item-has-children' ).attr( 'aria-haspopup', 'true' );
container.find( '.dropdown-toggle' ).click( function( e ) {
var _this = $( this ),
screenReaderSpan = _this.find( '.screen-reader-text' );
e.preventDefault();
_this.toggleClass( 'toggled-on' );
_this.next( '.children, .sub-menu' ).toggleClass( 'toggled-on' );
// jscs:disable
_this.attr( 'aria-expanded', _this.attr( 'aria-expanded' ) === 'false' ? 'true' : 'false' );
// jscs:enable
screenReaderSpan.text( screenReaderSpan.text() === screenReaderText.expand ? screenReaderText.collapse : screenReaderText.expand );
} );
}
initMainNavigation( $( '#site-navigation' ) );
siteMenu = $( '#menu-primary' );
siteNavigation = $( '#site-navigation' );
menuToggle = siteNavigation.find( '.menu-toggle' );
// Enable menuToggle.
( function() {
// Return early if menuToggle is missing.
if ( ! menuToggle.length ) {
return;
}
// Add an initial values for the attribute.
menuToggle.add( siteNavigation ).attr( 'aria-expanded', 'false' );
menuToggle.on( 'click.apostrophe', function() {
$( this ).add( siteMenu ).add( siteNavigation ).toggleClass( 'toggled-on' );
// jscs:disable
$( this ).add( siteMenu ).add( siteNavigation ).attr( 'aria-expanded', $( this ).add( siteNavigation ).attr( 'aria-expanded' ) === 'false' ? 'true' : 'false' );
// jscs:enable
} );
} )();
// Fix sub-menus for touch devices and better focus for hidden submenu items for accessibility.
( function() {
if ( ! siteNavigation.length || ! siteNavigation.children().length ) {
return;
}
// Toggle `focus` class to allow submenu access on tablets.
function toggleFocusClassTouchScreen() {
if ( window.innerWidth >= 840 ) {
$( document.body ).on( 'touchstart.apostrophe', function( e ) {
if ( ! $( e.target ).closest( '.main-navigation li' ).length ) {
$( '.main-navigation li' ).removeClass( 'focus' );
}
} );
siteNavigation.find( '.menu-item-has-children > a' ).on( 'touchstart.apostrophe', function( e ) {
var el = $( this ).parent( 'li' );
if ( ! el.hasClass( 'focus' ) ) {
e.preventDefault();
el.toggleClass( 'focus' );
el.siblings( '.focus' ).removeClass( 'focus' );
}
} );
} else {
siteNavigation.find( '.menu-item-has-children > a' ).unbind( 'touchstart.apostrophe' );
}
}
if ( 'ontouchstart' in window ) {
$( window ).on( 'resize.apostrophe', toggleFocusClassTouchScreen );
toggleFocusClassTouchScreen();
}
siteNavigation.find( 'a' ).on( 'focus.apostrophe blur.apostrophe', function() {
$( this ).parents( '.menu-item' ).toggleClass( 'focus' );
} );
} )();
// Add the default ARIA attributes for the menu toggle and the navigations.
function onResizeARIA() {
if ( window.innerWidth < 840 ) {
if ( menuToggle.hasClass( 'toggled-on' ) ) {
menuToggle.attr( 'aria-expanded', 'true' );
siteMenu.attr( 'aria-expanded', 'true' );
siteNavigation.attr( 'aria-expanded', 'true' );
} else {
menuToggle.attr( 'aria-expanded', 'false' );
siteMenu.attr( 'aria-expanded', 'false' );
siteNavigation.attr( 'aria-expanded', 'false' );
}
} else {
menuToggle.removeAttr( 'aria-expanded' );
siteMenu.removeAttr( 'aria-expanded' );
siteNavigation.removeAttr( 'aria-expanded' );
}
}
$( document ).ready( function() {
body = $( document.body );
$( window )
.on( 'load.apostrophe', onResizeARIA )
.on( 'resize.apostrophe', onResizeARIA );
} );
} )( jQuery );
;
/**
* apostrophe.js
*
* Handles the basic JS functionality for our theme.
*/
( function($) {
/*
* Count the number of images in each row in order to create the proper clearings.
* For the most part, this isn't necessary, but there are certain instances where
* the grid breaks at the wrong spot, making the whole layout look funky.
* To fix that, we're counting where we are in the grid layout based on the space
* each article uses, and adding a clearing class each time we drop to a new row.
*/
function countGrid() {
var $body = $( 'body' );
var $container = $( '#main #posts-wrapper' );
var $button = $( '.menu-toggle' );
// Only initiate script if the mobile menu toggle isn't showing and we're on a page that uses the grid.
// Return otherwise.
if ( ( 0 !== $button.offsetWidth && 0 !== $button.offsetHeight ) &&
! ( $body.hasClass( 'archive' ) || $body.hasClass( 'blog' ) || $body.hasClass( 'search' ) ) ) {
return;
}
var $articles = $container.find( '.hentry' );
// This variable is used to count how far across the grid we are.
var gridcount = 0;
var gridcount_increment;
$articles.each( function() {
// If the article is featured, it takes up two blocks. If not, it takes up one.
if ( $( this ).hasClass( 'apostrophe-featured' ) ) {
gridcount_increment = 2;
} else {
gridcount_increment = 1;
}
// If we're at a newline in the grid, add a class to clear the previous line, and reset the grid count.
if ( 3 < gridcount + gridcount_increment ) {
gridcount = 0;
$( this ).addClass( 'clear' );
}
gridcount = gridcount + gridcount_increment;
} );
} // countGrid
// Set width of h2 elements inside the grid to make sure post titles don't overflow the container, and we're breaking long, one-word titles correctly.
function blockWidth() {
var $body = $( 'body' );
if ( $body.hasClass( 'archive' ) || $body.hasClass( 'blog' ) || $body.hasClass( 'search' ) ) {
if ( $( window ).innerWidth() > 767 ) {
$( '.hentry' ).each( function() {
$( this ).find( '.entry-title' ).width( $( this ).width() );
} );
} else {
$( '.hentry' ).each( function() {
$( this ).find( '.entry-title' ).attr( 'style', '' );
} );
}
}
}
// Count grid blocks on page load
$( document ).on( 'ready', function() {
countGrid();
blockWidth();
});
// ...and on each subsequent Infinite Scroll load, as well.
$( document ).on( 'post-load', function() {
countGrid();
blockWidth();
});
$( window ).resize( function() {
blockWidth();
});
} )(jQuery);
;