/**
* 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 = `';
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);
}
})();
;
!function(e){var o={};function n(t){if(o[t])return o[t].exports;var r=o[t]={i:t,l:!1,exports:{}};return e[t].call(r.exports,r,r.exports,n),r.l=!0,r.exports}n.m=e,n.c=o,n.d=function(e,o,t){n.o(e,o)||Object.defineProperty(e,o,{enumerable:!0,get:t})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,o){if(1&o&&(e=n(e)),8&o)return e;if(4&o&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(n.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&o&&"string"!=typeof e)for(var r in e)n.d(t,r,function(o){return e[o]}.bind(null,r));return t},n.n=function(e){var o=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(o,"a",o),o},n.o=function(e,o){return Object.prototype.hasOwnProperty.call(e,o)},n.p="",n(n.s=273)}({273:function(e,o){!function(){"use strict";let e,o;function n(e){return new Promise((o,n)=>{const t=document.createElement("script");t.onload=()=>o(),t.onerror=e=>n(e),t.src=e,document.body.appendChild(t)})}function t(){if(e)return e;const o=window.wpcom_coblocks_js;return e=n(o.coblocks_lightbox_js),e}function r(){if(o)return o;window.wpcomJQueryUsageLoggerDisabled=!0;const e=window.wpcom_coblocks_js,t=window.jQuery?Promise.resolve():n(e.jquery_core_js).then(()=>n(e.jquery_migrate_js)),r=t.then(()=>window.imagesLoaded?Promise.resolve():n(e.imagesloaded_js)),c=t.then(()=>window.Masonry?Promise.resolve():n(e.masonry_js)),u=Promise.all([t,r,c]).then(()=>n(e.coblocks_masonry_js));return o=u,o}function c(){document.querySelector(".wp-block-coblocks-gallery-masonry")&&r(),document.querySelector(".has-lightbox")&&t(),document.body.classList.contains("block-editor-page")&&(r(),t())}"loading"!==document.readyState?c():document.addEventListener("DOMContentLoaded",c)}()}});;
(function() {
/**
* Debounce
*
* @param {Function} func
* @param {number} wait
* @param {boolean} immediate
*/
function debounce(func, wait, immediate) {
'use strict';
var timeout;
wait = (typeof wait !== 'undefined') ? wait : 20;
immediate = (typeof immediate !== 'undefined') ? immediate : true;
return function() {
var context = this, args = arguments;
var later = function() {
timeout = null;
if (!immediate) {
func.apply(context, args);
}
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) {
func.apply(context, args);
}
};
}
/**
* Get page header height and use it for top-margin on
* site-content when above mobile breakpoint
*/
function pageHeaderHeight() {
var body = document.body;
if ( document.documentElement.clientWidth < 560 ) {
document.getElementById( "primary" ).style.marginTop = 0;
body.classList.remove( 'wp-sticky-header' );
} else {
var header = document.getElementById( 'masthead' );
var content = document.getElementById( 'primary' );
content.style.marginTop = header.offsetHeight + "px";
if ( document.documentElement.clientHeight / 5 - header.offsetHeight >= 0 ) {
body.classList.add( 'wp-sticky-header' );
} else {
body.classList.remove( 'wp-sticky-header' );
}
}
};
/**
* Run our function every time the window resizes
*/
var isResizing = false;
window.addEventListener( 'resize',
debounce( function() {
if ( isResizing ) {
return;
}
isResizing = true;
setTimeout( function() {
pageHeaderHeight();
isResizing = false;
}, 150 );
} )
);
/**
* Run our page header height function
*/
pageHeaderHeight();
})();
;