/* global wpcom, jetpackCarouselStrings, DocumentTouch */
/* eslint-disable no-shadow */
/////////////////////////////////////
/////////////////////////////////////
var util = ( function () {
var noop = function () {};
function texturize( text ) {
// Ensure we get a string.
text = text.replace( /'/g, '’' ).replace( /'/g, '’' );
.replace( /"/g, '”' )
.replace( /"/g, '”' )
.replace( /"/g, '”' )
.replace( /[\u201D]/g, '”' );
// Untexturize allowed HTML tags params double-quotes.
text = text.replace( /([\w]+)=&#[\d]+;(.+?)&#[\d]+;/g, '$1="$2"' );
function applyReplacements( text, replacements ) {
return text.replace( /{(\d+)}/g, function ( match, number ) {
return typeof replacements[ number ] !== 'undefined' ? replacements[ number ] : match;
function getBackgroundImage( imgEl ) {
var canvas = document.createElement( 'canvas' ),
context = canvas.getContext && canvas.getContext( '2d' );
context.filter = 'blur(20px) ';
context.drawImage( imgEl, 0, 0 );
var url = canvas.toDataURL( 'image/png' );
applyReplacements: applyReplacements,
getBackgroundImage: getBackgroundImage,
/////////////////////////////////////
// DOM-related utility functions
/////////////////////////////////////
var domUtil = ( function () {
// Helper matches function (not a polyfill), compatible with IE 11.
function matches( el, sel ) {
if ( Element.prototype.matches ) {
return el.matches( sel );
if ( Element.prototype.msMatchesSelector ) {
return el.msMatchesSelector( sel );
// Helper closest parent node function (not a polyfill) based on
// https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill
function closest( el, sel ) {
return el.closest( sel );
if ( matches( current, sel ) ) {
current = current.parentElement || current.parentNode;
} while ( current !== null && current.nodeType === 1 );
el.style.display = 'none';
// Everything we show and hide in Carousel is currently a block,
// so we can make this really straightforward.
el.style.display = 'block';
function fade( el, start, end, callback ) {
// Prepare for transition.
// Ensure the item is in the render tree, in its initial state.
el.style.removeProperty( 'display' );
el.style.opacity = start;
el.style.pointerEvents = 'none';
var animate = function ( t0, duration ) {
var t = performance.now();
var ratio = diff / duration;
el.style.opacity = start + ( end - start ) * ratio;
requestAnimationFrame( () => animate( t0, duration ) );
el.style.removeProperty( 'pointer-events' );
requestAnimationFrame( function () {
// Double rAF for browser compatibility.
requestAnimationFrame( function () {
animate( performance.now(), 200 );
function fadeIn( el, callback ) {
callback = callback || util.noop;
fade( el, 0, 1, callback );
function fadeOut( el, callback ) {
callback = callback || util.noop;
fade( el, 1, 0, function () {
el.style.display = 'none';
function emitEvent( el, type, detail ) {
e = new CustomEvent( type, {
e = document.createEvent( 'CustomEvent' );
e.initCustomEvent( type, true, true, detail || null );
// From: https://easings.net/#easeInOutQuad
function easeInOutQuad( num ) {
return num < 0.5 ? 2 * num * num : 1 - Math.pow( -2 * num + 2, 2 ) / 2;
function getFooterClearance( container ) {
var footer = container.querySelector( '.jp-carousel-info-footer' );
var infoArea = container.querySelector( '.jp-carousel-info-extra' );
var contentArea = container.querySelector( '.jp-carousel-info-content-wrapper' );
if ( footer && infoArea && contentArea ) {
var styles = window.getComputedStyle( infoArea );
var padding = parseInt( styles.paddingTop, 10 ) + parseInt( styles.paddingBottom, 10 );
padding = isNaN( padding ) ? 0 : padding;
return contentArea.offsetHeight + footer.offsetHeight + padding;
'ontouchstart' in window || ( window.DocumentTouch && document instanceof DocumentTouch )
function scrollToElement( el, container, callback ) {
if ( ! el || ! container ) {
// For iOS Safari compatibility, use JS to set the minimum height.
var infoArea = container.querySelector( '.jp-carousel-info-extra' );
// 64px is the same height as `.jp-carousel-info-footer` in the CSS.
infoArea.style.minHeight = window.innerHeight - 64 + 'px';
var startTime = Date.now();
var originalPosition = container.scrollTop;
var targetPosition = Math.max(
el.offsetTop - Math.max( 0, window.innerHeight - getFooterClearance( container ) )
var distance = targetPosition - container.scrollTop;
distance = Math.min( distance, container.scrollHeight - window.innerHeight );
var progress = easeInOutQuad( ( now - startTime ) / duration );
progress = progress > 1 ? 1 : progress;
var newVal = progress * distance;
container.scrollTop = originalPosition + newVal;
if ( now <= startTime + duration && isScrolling ) {
return requestAnimationFrame( runScroll );
infoArea.style.minHeight = '';
container.removeEventListener( 'wheel', stopScroll );
// Allow scroll to be cancelled by user interaction.
container.addEventListener( 'wheel', stopScroll );
function getJSONAttribute( el, attr ) {
if ( ! el || ! el.hasAttribute( attr ) ) {
return JSON.parse( el.getAttribute( attr ) );
function convertToPlainText( html ) {
var dummy = document.createElement( 'div' );
dummy.textContent = html;
function stripHTML( text ) {
return text.replace( /<[^>]*>?/gm, '' );
scrollToElement: scrollToElement,
getJSONAttribute: getJSONAttribute,
convertToPlainText: convertToPlainText,
/////////////////////////////////////
// Carousel implementation
/////////////////////////////////////
var lastKnownLocationHash = '';
var isUserTyping = false;
'div.gallery, div.tiled-gallery, ul.wp-block-gallery, ul.blocks-gallery-grid, ' +
'figure.wp-block-gallery.has-nested-images, div.wp-block-jetpack-tiled-gallery, a.single-image-gallery';
// Selector for items within a gallery or tiled gallery.
var galleryItemSelector =
'.gallery-item, .tiled-gallery-item, .blocks-gallery-item, ' + ' .tiled-gallery__item';
// Selector for all items including single images.
var itemSelector = galleryItemSelector + ', .wp-block-image';
typeof wpcom !== 'undefined' && wpcom.carousel && wpcom.carousel.stat
typeof wpcom !== 'undefined' && wpcom.carousel && wpcom.carousel.pageview
? wpcom.carousel.pageview
function handleKeyboardEvent( e ) {
carousel.overlay.scrollTop -= 100;
carousel.overlay.scrollTop += 100;
function disableKeyboardNavigation() {
function enableKeyboardNavigation() {
function calculatePadding() {
var baseScreenPadding = 110;
screenPadding = baseScreenPadding;
if ( window.innerWidth <= 760 ) {
screenPadding = Math.round( ( window.innerWidth / 760 ) * baseScreenPadding );
if ( screenPadding < 40 && domUtil.isTouch() ) {
function makeGalleryImageAccessible( img ) {
img.ariaLabel = jetpackCarouselStrings.image_label;
function initializeCarousel() {
if ( ! carousel.overlay ) {
carousel.overlay = document.querySelector( '.jp-carousel-overlay' );
carousel.container = carousel.overlay.querySelector( '.jp-carousel-wrap' );
carousel.gallery = carousel.container.querySelector( '.jp-carousel' );
carousel.info = carousel.overlay.querySelector( '.jp-carousel-info' );
carousel.caption = carousel.info.querySelector( '.jp-carousel-caption' );
carousel.commentField = carousel.overlay.querySelector(
'#jp-carousel-comment-form-comment-field'
carousel.emailField = carousel.overlay.querySelector(
'#jp-carousel-comment-form-email-field'
carousel.authorField = carousel.overlay.querySelector(
'#jp-carousel-comment-form-author-field'
carousel.urlField = carousel.overlay.querySelector( '#jp-carousel-comment-form-url-field' );
].forEach( function ( field ) {
field.addEventListener( 'focus', disableKeyboardNavigation );
field.addEventListener( 'blur', enableKeyboardNavigation );
carousel.overlay.addEventListener( 'click', function ( e ) {
var isTargetCloseHint = !! domUtil.closest( target, '.jp-carousel-close-hint' );
var isSmallScreen = !! window.matchMedia( '(max-device-width: 760px)' ).matches;
if ( target === carousel.overlay ) {
} else if ( isTargetCloseHint ) {
} else if ( target.classList.contains( 'jp-carousel-image-download' ) ) {
stat( 'download_original_click' );
} else if ( target.classList.contains( 'jp-carousel-comment-login' ) ) {
handleCommentLoginClick( e );
} else if ( domUtil.closest( target, '#jp-carousel-comment-form-container' ) ) {
handleCommentFormClick( e );
domUtil.closest( target, '.jp-carousel-photo-icons-container' ) ||
target.classList.contains( 'jp-carousel-photo-title' )
handleFooterElementClick( e );
window.addEventListener( 'keydown', handleKeyboardEvent );
carousel.overlay.addEventListener( 'jp_carousel.afterOpen', function () {
enableKeyboardNavigation();
// Don't show navigation if there's only one image.
if ( carousel.slides.length <= 1 ) {
// Show dot pagination if slide count is <= 5, otherwise show n/total.
if ( carousel.slides.length <= 5 ) {
domUtil.show( carousel.info.querySelector( '.jp-swiper-pagination' ) );
domUtil.show( carousel.info.querySelector( '.jp-carousel-pagination' ) );
carousel.overlay.addEventListener( 'jp_carousel.beforeClose', function () {
disableKeyboardNavigation();
// Fixes some themes where closing carousel brings view back to top.
document.documentElement.style.removeProperty( 'height' );
// If we disable the swiper (because there's only one image)
// we have to re-enable it here again as Swiper doesn't, for some reason,
// show the navigation buttons again after reinitialization.
domUtil.hide( carousel.info.querySelector( '.jp-swiper-pagination' ) );
domUtil.hide( carousel.info.querySelector( '.jp-carousel-pagination' ) );
carousel.overlay.addEventListener( 'jp_carousel.afterClose', function () {
// don't force the browser back when the carousel closes.
if ( window.history.pushState ) {
window.location.pathname + window.location.search
window.location.href = '';
lastKnownLocationHash = '';
// Prevent native browser zooming
carousel.overlay.addEventListener( 'touchstart', function ( e ) {
if ( e.touches.length > 1 ) {